Posts
Search
Contact
Cookies
About
RSS

Wrapping a returned C struct from inside JNI (and other JNI stuff)

Added 15 Jan 2015, 9:40 p.m. edited 18 Jun 2023, 1:12 a.m.
First off I don't claim to be any kind of JNI guru and frankly I like to touch C as little as possible - C's memory management model is only marginally better than C++'s (shudder) - that should get me a few flames >:) That said I did struggle to find any easy to digest info that did just what I wanted, so I thought I'd document my solution, just to muddy the waters further! When using JNI its important to remember that you've been spoilt rotten by Java's garbage collection, things in C are all together more hairy and its all too easy to forget that passing a local variable back through the "JNI barrier" is gonna cause you nothing but pain... Its common to have a C or C++ objects wrapped in a Java object with its own close or delete method,leaving you responsible for calling it before the Java Object is GC'd. This isn't particularly a very "Java way" to do things and in some cases it can be avoided. It's not uncommon to see C stucts returned from functions and if they are simple structures for example just containing ints and floats (basically you're sure they don't contain references to allocated memory you are responsible for freeing) then they are a very good candidate for encapsulating within a Java class, that can be automagically GC'd The first thing you need is a buffer in your wrapping class, this needs to be created in a particular way to ensure stuff doesn't get mangled, if the struct for example only holds floats, you can write pure Java accessors which directly get or put to a FloatBuffer. Lets look at how the buffer needs to be created.
buff = ByteBuffer.allocateDirect(16).order(ByteOrder.nativeOrder());
In the constructor I'm creating a buffer big enough for 4 Floats, you could argue that you should have a native function that returns the size in bytes of the structure, but in our case we're going to assume that the structure is only going to be four floats....forever.... notice two things its allocated as a "direct" buffer and the byte order is set to the machines current native order, its returned as a float buffer in this case as we'll be directly manipulating it from Java as well as creating it in C. Don't worry if your struct has mixed (native) types consider this code
        bb=ByteBuffer.allocateDirect(8).order(ByteOrder.nativeOrder());
        
        ib = bb.asIntBuffer();
        fb = bb.asFloatBuffer();
        
        System.out.println("ib 0 ="+ib.get(0));
        System.out.println("bb 0 ="+bb.get(0)+","+bb.get(1)+","+bb.get(2)+","+bb.get(3));
        fb.put(0,1.1f);
        System.out.println("ib 0 ="+ib.get(0));
        System.out.println("bb 0 ="+bb.get(0)+","+bb.get(1)+","+bb.get(2)+","+bb.get(3));
Crucially here ByteBuffers asIntBuffer and asFloatBuffer doesn't make a copy of the byte buffer but rather its a different "view" of the same ByteBuffer, so as you can see you have a fair bit of scope for accessing values directly from Java even if there are mixed types in the structure... First lets look at the worst case where there isn't an existing Java object and we need to create one from C
jobject createBogusJobject(JNIEnv *e) {
    jclass cls = (*e)->FindClass(e, "wrapper$Bogus");
    jmethodID constructor = (*e)->GetMethodID(e, cls, "<init>", "()V");    
    jobject bogus = (*e)->NewObject(e, cls, constructor);
    return bogus;
}
There is no error checking here, however assuming that you have the right class and field name, then all should be well... (but it probably is a good idea to check the new object was actually created.) On the Java side of our imaginary wrapper, our Bogus class is a static sub class of a singleton wrapper class (I'll leave it to you to work out why). When actually creating the object the NewObject function needs to know which constructor to call, method signatures are probably not terribly intuitive, but you can use javap to tell you, for example :-
javap -s jni/wrapper\$Bogus.class
will tell you all you need to know, notice that the $ is escaped (and the path separator is a *nix one, yours could be different...!). Now we have our object either new or it could be passed from Java, we need to get a pointer to our object's buffer and copy our C struct into it. Lets see how we do this with an existing Java class
JNIEXPORT void JNICALL Java_wrapper__1_1bogusOp
  (JNIEnv *e, jclass c, jobject jbogus, jfloat x, jfloat y, jfloat z)
{
    Bogus b = aBogusOp(x, y, z);
    Bogus* dp = getBuffPtr(e,jbogus);
    *dp = b;
}
Given a Java Bogus object and some parameters, we can call a native routine that passes back a struct as a return value. Next we need to copy it into our Java object for which we need a pointer to the ByteBuffer (more on that in a moment) By dereferencing the ByteBuffer pointer we can then copy the local bogus variable into the buffer. The last piece of this particular puzzle is to get a pointer to our ByteBuffer.
void* getBuffPtr(JNIEnv *e, jobject jo)
{
    jobject cls = (*e)->GetObjectClass(e, jo);
    jfieldID fid = (*e)->GetFieldID(e, cls,"buff","Ljava/nio/ByteBuffer;");
    jobject jbuff = (*e)->GetObjectField(e, jo, fid);
    return (*e)->GetDirectBufferAddress(e, jbuff);
}
I use this routine for a number of different Classes, for this reason their ByteBuffers must all be called "buff" again we need an arcane field signature but javap will help out. Again there is nothing in the way of error checking here! On the subject of error checking your biggest ally is the Java compiler, its fairly difficult to supply the wrong type of parameter and checking for NULL references will stop many of the more spectacular dummy throwing JNI can do. Remember that its much better to check what you are supplying to JNI in the first place.... with that in mind I use a very simple macro when I need to pass an jobject to C
#define checkJO(JO, MSG) if (JO==NULL) { printf(MSG); fflush(stdout); return; }
The first thing I do is check all user supplied java objects passed to my C JNI function for example
checkJO(jbogus,"\nbogusOp was passed a NULL bogus!\n");
Two things to note, first off the 2 carriage returns, basically we're sharing a terminal with Java's error reporting and even flushing the buffer you can still get any message you emit munged up with a JVM error message such as an uncaught NULL pointer exception. Lastly the macro returns from the function immediately so if you have a none void returning JNI function you'll need a macro that returns NULL which can be tested later so you know there's a problem... Don't forget in this example I'm using an existing Java object to pass back return values but you could easily be returning Java object created in C. Well theres a fair bit to digest there! so probably best we leave it here, don't forget to check out the JNI documentation Enjoy!