First, it's a tool based on javah that generates JNI code to improve it's usability. StaticJNI is a foreign function interface (FFI) for java that answers the real need of invoking Java code from C. It uses opaque types and callback functions tailored to the program needs to offer a static FFI in C, without the use of and additional interface description language. It relies on standard Java, uses annotations and generates standard C code to be frictionless.
Since StaticJNI helps you to use the Java Native Interface (JNI) more easily, the best way to use it is at the start of your project using JNI. It will save you a lot of code and do the exception gestion for you. If your project is already started, you can still convert it with StaticJNI, but it will be harder if you aldready did some special architecture in your JNI code.
To specify the jvm to use, uncomment and change the line "# boot.java.home = ..." in "make/build.properties" to indicate the root of the jvm, for example: "boot.java.home = /usr/lib/jvm/java-7-openjdk-amd64/". You may need to update the makefiles of the tests in test/tools/staticjni/*/Makefile. To compile tools and tests, then run tests, call: ./staticjni_test.sh
All StaticJNI code is in "src/share/com/sun/tools/javah/" and the annotations are in "src/share/net/xymus/staticjni/".
To see code using StaticJNI, consult the tests located in "test/tools/staticjni/". The main code of all tests are in the .java files and the according .c file.
Before talking about how to use StaticJNI's annotations, you need to understand how different files are generated. Lets explain this with the simple exemple in "test/tools/staticjni/simple" : There is one Java class annotated with StaticJNI. When you run StaticJNI on this class, 3 files are generated :
"Simple.class" is the source file which is used to generate other files. "Simple_frontier.c" and "Simple_frontier.h" are implementing the callbacks to Java to make them easy to use and they define specific types corresponding to Java types. "Simple.h" contains the signatures of the natives method presents in Simple class. All you have to do is create a "Simple.c" file where you include "Simple.h" and implements your native code.
Now lets see how you can declare your differents Java callbacks using StaticJNI's annotations. 5 annotations are proposed by StaticJNI :
*@NativeCall *@NativeCalls *@NativeNew *@NativeNews *@NativeSuperCall
Theses annotations are used to generate C code for java callbacks.
They are both used to declare the Java functions that you want to use in your C implementation of the native method you annotate. If you use theses functions multiple times in differents natives methods, you don't need to declare them multiple times. Lets take a look again at the Simple example :
@NativeCall( "javaMeth" )
private native void foo();
@NativeCalls( {"foo", "i"} )
private native int bar( int a, int b, char c );
Here, we declare methods of the Simple class so we just need to use their name as parameter of the annotation. Note that the access to the "i" attribute of the simple class only need the name of the attribute.
Generated code in "Simple_frontier.h" :
jint Simple_javaMeth( Simple self, jint arg0 );
void Simple_foo( Simple self );
void set_Simple_i( Simple self, jint in_value );
jint get_Simple_i( Simple self );
Generated code in "Simple.h" :
void Simple_foo__impl( Simple );
jint Simple_bar_impl( Simple, jint, jint, jchar );
To use the functions generated by StaticJNI, all you have to do is create an implementation file "Simple.c" and include "Simple.h", then implements the functions from "Simple.h", you can use "Simple_frontier.h"'s functions in your implementation code. Use of generated functions in the implementation C code in "Simple.c" :
void Simple_foo_impl( Simple self)
{
jint v = Simple_javaMeth( self, 123 );
printf( "foo %d\n", v );
}
jint Simple_bar_impl( Simple self, jint a, jint b, jchar c )
{
printf( "bar intro\n" );
Simple_foo( self );
printf( "bar %d %d %c\n", a, b, c );
return a + b;
}
As you can see, making callbacks to Java using JNI is way more easy and "sexy" using StaticJNI.
Note : When you're using a method from another class, you need to specify the complete path of the class in the annotation, then a space, and the name of the method you want to use : @NativeCalls( {"android.graphics.Bitmap createBitmap", "android.graphics.Bitmap.Config valueOf"} ) private native Bitmap jniGetBitmapFromStoredBitmapData(ByteBuffer handler);
Note : When you want to use an inner Class method, specify the path of the class with only "." character. This previous code is a good example, the "Config" class is an inner class of "Bitmap".
You may have noticed that in theses examples, when you declare the method you want to use, you don't specify it's signature. If you do so, the first method with the same name found when introspecting the class will be taken as the callback you wanted to declare, no matter what is signature is.If you want to use a specific method with a specific signature, you have to declare it like this :
@NativeCalls( {"android.graphics.Bitmap createBitmap(int,int,android.graphics.Bitmap.Config)", "android.graphics.Bitmap.Config valueOf"} )
private native Bitmap jniGetBitmapFromStoredBitmapData(ByteBuffer handler);
It is important to respect the syntax : "(type,type,type)" with no spaces and the entire path when the parameter is not a primitive type.
Theses annotations are used to declare calls to Java Constructors from C. You can use them the same way as the @NativeCall and @NativeCalls annotations except that you just need to specify the path of the class you want to instanciate from C.Lets see the use of theses annotations with the complex example :
@NativeNew( "complex.Sub (int)" )
public native Sub playWithConstructors();
Generated code in "complex_Complex_frontier.h" :
Sub new_Sub( jint arg0 );
Implementation of playWithConstructors() in "complex_Complex.c" :
Sub Complex_playWithConstructors__impl( Complex self )
{
return new_Sub( 123 );
}
This one is used to generate a call to super from C code.
Example :
@NativeSuperCall
public native int hashCode();
Generated code in "complex_Complex_frontier.h" :
jint super_Complex_hashCode( Complex self );
Implementation :
int Complex_hashCode__impl( Complex self )
{
jint s = super_Complex_hashCode( self );
return 2 * s;
}
Now that you know how StaticJNI works, here is how to use it in your projects :
- Copy the folder "src/share/classes/net" in the sources of your project so you can import StaticJNI annotations
- Modify your java source files where you are using native methods : "import net.xymus.staticjni.*" to use annotations, then annotate your natives methods the way you need.
- Compile your Java source to bytecode
- Create a Makefile on this template :
"CLASSPATH={Path to your compiled classes} JAVAH=java -cp {StaticJNI folder path}/dist/lib/javah.jar com.sun.tools.javah.Main default: frontier
frontier: ${JAVAH} -classpath ${CLASSPATH}:. -staticjni {Classes concerned by code generation}"
- Run the Makefile, the files will be generated in the current folder
- Copy the generated files in the appropried folder where you will implement your native functions
- Implement the C code of the native methods
- Compile the C code into a library suitable for the JVM
- Execute your Application.
If you want to go further and see what StaticJNI is generating, here is the functions generated for the Simple example :
jint Simple_javaMeth( Simple self, jint arg0 ) {
jclass jclass = (*thread_env)->GetObjectClass( thread_env, (jobject)self );
if ( jclass == 0 ) {
(*thread_env)->FatalError( thread_env, "Cannot find class for <Callback Simple, javaMeth>" );
}
jmethodID jmeth = (*thread_env)->GetMethodID( thread_env, jclass, "javaMeth", "(I)I" );
if ( jmeth == 0 ) {
(*thread_env)->FatalError( thread_env, "Cannot find method: javaMeth" );
}
jint rval = (*thread_env)->CallIntMethod( thread_env, (jobject)self, jmeth, arg0 );
return rval;
}
void Simple_foo( Simple self ) {
jclass jclass = (*thread_env)->GetObjectClass( thread_env, (jobject)self );
if ( jclass == 0 ) {
(*thread_env)->FatalError( thread_env, "Cannot find class for <Callback Simple, foo>" );
}
jmethodID jmeth = (*thread_env)->GetMethodID( thread_env, jclass, "foo", "()V" );
if ( jmeth == 0 ) {
(*thread_env)->FatalError( thread_env, "Cannot find method: foo" );
}
(*thread_env)->CallVoidMethod( thread_env, (jobject)self, jmeth );
}
void set_Simple_i( Simple self, jint in_value ) {
jclass jclass = (*thread_env)->GetObjectClass( thread_env, (jobject)self );
if ( jclass == 0 ) {
(*thread_env)->FatalError( thread_env, "Cannot find class for <FieldCallback Simple, i>" );
}
jfieldID jfield = (*thread_env)->GetFieldID( thread_env, jclass, "i", "I" );
if ( jfield == 0 ) {
(*thread_env)->FatalError( thread_env, "Cannot find field: i\n" );
}
jint value = in_value;
(*thread_env)->SetIntField( thread_env, (jobject)self, jfield, value );
}
jint get_Simple_i( Simple self ) {
jclass jclass = (*thread_env)->GetObjectClass( thread_env, (jobject)self );
if ( jclass == 0 ) {
(*thread_env)->FatalError( thread_env, "Cannot find class for <FieldCallback Simple, i>\n" );
}
jfieldID jfield = (*thread_env)->GetFieldID( thread_env, jclass, "i", "I" );
if ( jfield == 0 ) {
(*thread_env)->FatalError( thread_env, "Cannot find field: i\n" );
}
jint rval = (*thread_env)->GetIntField( thread_env, (jobject)self, jfield );
return rval;
}
As you can see, the generated code contains all the controls needed when you use the JNI, it saves you some boring work.
If you need to use the JNIEnv variable in your C implementation to do more JNI stuff, it's available as "thread_env".