4

I have a compiled library that I need to use in a project. To keep it short, it's a library for interacting with a specific piece of hardware. What I have is .a and .dll library files, for linux and windows respectively, and a bunch of C++ .h headers with all the public functions and classes described there.

The problem is that the project needs to be in Java, so I need to write a JNI wrapper for this library, and honestly, I've never done that. But that's ok, I'm down to learn the thing.

I've read up a bunch of documentation online, and I figured out passing variables, creating java objects from native code, etc.

What I can't figure out, is how to work with native constructors using JNI? I have no idea what the source code of these constructors are, I only have the headers like this:

namespace RFDevice {

class RFDEVICE_API RFEthernetDetector
{
public:
    //-----------------------------------------------------------------------------
    //  FUNCTION  RFEthernetDetector::RFEthernetDetector
    /// \brief    Default constructor of RFEthernetDetector object.
    ///           
    /// \return   void : N/A
    //-----------------------------------------------------------------------------
    RFEthernetDetector();
    RFEthernetDetector(const WORD wCustomPortNumber);

So basically if I was to write my program in C++ (which I can't), I would do something like

RFEthernetDetector ethernetDetector = new RFEthernerDetector(somePort);

and then work with that object. But... How do I do this in Java using JNI? I don't understand how am I supposed to create a native method for constructor, that would call the constructor from my .a library, and then have some way of working with that specific object? I know how to create java objects from native code - but the thing is I don't have any information about internal structure of the RFEthernetDetector class - only some of it's public fields and public methods.

And I can't seem to find the right articles on the net to help me out. How do I do that?

Update: A bit further clarification.

I create a .java wrapper class like this:

public class RFEthernetDetector
{
    public RFEthernetDetector(int portNumber)
    {
        Init(portNumber);
    }

    public native void Init(int portNumber);            // Void? Or what?
}

then I compile it with -h parameter to generate JNI .h file:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class RFEthernetDetector */

#ifndef _Included_RFEthernetDetector
#define _Included_RFEthernetDetector
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     RFEthernetDetector
 * Method:    Init
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_RFEthernetDetector_Init
  (JNIEnv *, jobject, jint);

#ifdef __cplusplus
}
#endif
#endif

I then create an implementation that will call the functions from my .a library:

#include "RFEthernetDetector.h"     // auto-generated JNI header
#include "RFEthernetDetector_native.h"  // h file that comes with the library, 
                    //contains definition of RFEthernetDetector class
/*
 * Class:     RFEthernetDetector
 * Method:    Init
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_RFEthernetDetector_Init(JNIEnv *env, jobject thisObj, jint value)
{
    RFEthernetDetector *rfeDetector = new RFEthernetDetector(value);    // constructor from the library
    // now how do I access this new object from Java?
    // if I need to later call rfDetector->doSomething() on that exact class instance?
}
Vlad Vyatkin
  • 544
  • 5
  • 16
  • 1
    JNI is a C interface: not C++. You can write in C++ but the bits that interface with Java should be in C. – cup Oct 01 '19 at 05:26
  • Ok, I got it. But that doesn't really answer the question – Vlad Vyatkin Oct 01 '19 at 05:53
  • @cup Surely not? As long as you have `extern "C"`s in the right places (which you should, as `javac -h` generates them), you can use C++ all the way through, no? – HTNW Oct 01 '19 at 06:05
  • Checked it, indeed you can in the JNI implementation use C++. In fact, you wouldn't be able to do things like calling class functions if you couldn't. – Vlad Vyatkin Oct 01 '19 at 06:38
  • Does anything [here](https://docs.oracle.com/en/java/javase/13/docs/specs/jni/functions.html#newobject-newobjecta-newobjectv) fit your needs? – Slaw Oct 01 '19 at 08:04

3 Answers3

5

You would need to build a RFEthernetDetector Java class that, through a pointer, owns a RFEthernetDetector on the C++ side. This is no fun, but inter-language glue never is.

// In this design, the C++ object needs to be explicitly destroyed by calling
// close() on the Java side.
// I think that Eclipse, at least, is configured by default to complain
// if an AutoCloseable is never close()d.
public class RFEthernetDetector implements AutoCloseable {
   private final long cxxThis; // using the "store pointers as longs" convention
   private boolean closed = false;
   public RFEthernetDetector(int port) {
       cxxThis = cxxConstruct(port);
   };
   @Override
   public void close() {
       if(!closed) {
           cxxDestroy(cxxThis);
           closed = true;
       }
   }
   private static native long cxxConstruct(int port);
   private static native void cxxDestroy(long cxxThis);

   // Works fine as a safety net, I suppose...
   @Override
   @Deprecated
   protected void finalize() {
       close();
   }
}

And on the C++ side:

#include "RFEthernetDetector.h"

JNIEXPORT jlong JNICALL Java_RFEthernetDetector_cxxConstruct(JNIEnv *, jclass, jint port) {
    return reinterpret_cast<jlong>(new RFEthernetDetector(port));
}

JNIEXPORT void JNICALL Java_RFEthernetDetector_cxxDestroy(JNIEnv *, jclass, jlong thiz) {
    delete reinterpret_cast<RFEthernetDetector*>(thiz);
    // calling other methods is similar:
    // pass the cxxThis to C++, cast it, and do something through it
}

If all that reinterpret_casting makes you feel uncomfortable, you could choose to instead keep a map around:

#include <map>

std::map<jlong, RFEthernetDetector> references;

JNIEXPORT jlong JNICALL Java_RFEthernetDetector_cxxConstruct(JNIEnv *, jclass, jint port) {
    jlong next = 0;
    auto it = references.begin();
    for(; it != references.end() && it->first == next; it++) next++;
    references.emplace_hint(it, next, port);
    return next;
}

JNIEXPORT void JNICALL Java_RFEthernetDetector_cxxDestroy(JNIEnv *, jclass, jlong thiz) {
    references.erase(thiz);
}
HTNW
  • 27,182
  • 1
  • 32
  • 60
  • That's pretty much what I did in my own solution that I've posted half an hour ago. Is there no other method than basically passing a "long" address every time you want to access an instance of an object? I mean, it works, but it doesn't look like fun, as you put it yourself. – Vlad Vyatkin Oct 01 '19 at 06:35
0

You'll need to build the native class in Java, then run the javah program which will build the stubs that Java expects. You'll then need to map the java stubs to the C++ code to compile and distribute that binding library along with your java program.

https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html

PaulProgrammer
  • 16,175
  • 4
  • 39
  • 56
  • I know all of that. You misunderstand the question. As I said, I figured out how to pass arguments, call native functions from a library, etc. My problem is specifically with CONSTRUCTORS. As in, the native library has a constructor for a class that does something, and creates and inits the class in native code. And then I need to somehow call functions ON THAT NATIVE CLASS INSTANCE, but I obviously can't access it directly from the JVM. How do I make a constructor that creates instance of a C++ class and then work with that instance of C++ (not java) class from java? – Vlad Vyatkin Oct 01 '19 at 05:26
  • Ah, I see your question. I'm not sure the constructor itself can be hooked by JNI. You might need a private static call that you make from within the constructor to create the context. – PaulProgrammer Oct 01 '19 at 05:30
0

So, what I ended up doing for now is basically storing the address in my .java class as "long" variable, and then have Init() method return the address of C++ instance as jlong.

Then, when I need to call something on that instance of a class - I pass the address as an additional argument and do the transformation in the C code like this (tested on test class / custom .so):

//constructor wrapper
JNIEXPORT jlong JNICALL Java_Test_Greetings(JNIEnv *env, jobject thisObj, jint value)
{
    Greetings *greetings = new Greetings(value);
    return (jlong)greetings;
}

JNIEXPORT void JNICALL Java_Test_HelloWorld(JNIEnv *env, jobject thisObj, jlong reference)
{
    Greetings *greetings;
    greetings = (Greetings*)reference;
    greetings->helloValue();
}

I have no idea if that's the correct way to do it, but it works... would appreciate if someone tells me how wrong I am.

Vlad Vyatkin
  • 544
  • 5
  • 16