Skip to content

Implementing Native Methods

Eugene Gershnik edited this page Oct 9, 2021 · 4 revisions

One of the most common tasks when writing JNI code is to implement native Java methods. SimpleJNI makes it very easy and straightforward. Make sure you read about Declaring Java Types and Representing Java Classes before reading this page; it builds upon information explained there.

There are 2 ways of implementing native methods in raw JNI. One is by giving special names to methods exposed from a shared library. Another is by manually registering implementations of Java methods. The first approach can be used with SimpleJNI, if desired, but it has many known deficiencies and is not generally recommended. SimpleJNI provides direct support for manual registration.

In order to register a native method using SimpleJNI you will need to

  1. Define all the Java types used in method signature as described in Declaring Java Types. For example if the method you are implementing uses java.util.Map as an argument or return value, you will need to do something along the lines of

    DEFINE_JAVA_TYPE(jMap,        "java.util.Map"); 
  2. Declare the C++ implementation using the precise mapping of Java types to C++. The C++ method will have 2 parameters in addition to whatever parameters Java method takes. For static methods these will be:

    JNIEnv *, jclass

    For instance methods these will be

    JNIEnv *, <mapped type you defined>

    For example consider

    class Something
    {
        native boolean instanceMethod(int i, Map m);
        static native boolean staticMethod(int i, Map m);
    }

    These will require the following C++ declarations

    DEFINE_JAVA_TYPE(jMap,        "java.util.Map");
    DEFINE_JAVA_TYPE(jSomething,  "com.mystuff.Something"); 
    
    jboolean instanceMethod(JNIEnv * env, jSomething obj, jint i, jMap m);
    jboolean staticMethod(JNIEnv * env, jclass cls, jint i, jMap m);
  3. Obtain a java_class<type> instance for the type your methods belong to somehow. See Representing Java Classes for various ways to do so.

  4. Register C++ methods. For the example above you would do something like this

    java_class<jSomething> cls = ...;
    
    cls.register_natives(env, {
        bind_native("instanceMethod", instanceMethod),
        bind_native("staticMethod", staticMethod)
    });

    Note that, as usual with SimpleJNI, registering only requires the simple name of Java method. You do not need to to know the encoded signature.

Similar to other reasons given in Representing Java Classes it is usually advantageous to perform the registration together with all other Java resolving from within JNI_OnLoad. You can accomplish this by putting the registration code into the constructor of the C++ representation class. The representation class also provides a good scope to put the native implementations in. For the example above

class ClassOfSomething : public java_runtime::simple_java_class<jSomething>
{
    ClassOfSomething(JNIEnv * env):
        simple_java_class(env)
    {
        register_natives(env, {
            bind_native("instanceMethod", instanceMethod),
            bind_native("staticMethod", staticMethod)
        });
    } 

    static jboolean instanceMethod(JNIEnv * env, jSomething obj, jint i, jMap m);
    static jboolean staticMethod(JNIEnv * env, jclass cls, jint i, jMap m);
};

jboolean ClassOfSomething::instanceMethod(JNIEnv * env, jSomething obj, jint i, jMap m)
{
    //implementation
}
jboolean ClassOfSomething::staticMethod(JNIEnv * env, jclass cls, jint i, jMap m)
{
    //implementation
}

Dealing with overloads

register_natives deals with overloads transparently on the Java side but if you overload C++ implementations you might want to manually disambiguate them. Consider the following Java class

class Something
{
    native void method();
    native void method(int i);
    static native void method(float f);
}

On the C++ side you can use different names like this

class ClassOfSomething : public java_runtime::simple_java_class<jSomething>
{
    ClassOfSomething(JNIEnv * env):
        simple_java_class(env)
    {
        register_natives(env, {
            bind_native("method", instanceMethod1);
            bind_native("method", instanceMethod2);
            bind_native("method", staticMethod);
        });
    } 

    static void instanceMethod1(JNIEnv * env, jSomething obj);
    static void instanceMethod2(JNIEnv * env, jSomething obj, jint);
    static void staticMethod(JNIEnv * env, jclass cls, jfloat);
};

Note that Java name given in registration is the same. SimpleJNI figures out which Java method to use from your C++ method signature. If you overload C++ methods too you will get a compilation error since C++ doesn't know which method you meant to pass on its own. You will need to manually disambiguate function pointers which is exceedingly unpleasant in C++. Here is what the casts would look like

class ClassOfSomething : public java_runtime::simple_java_class<jSomething>
{
    ClassOfSomething(JNIEnv * env):
        simple_java_class(env)
    {
        register_natives(env, {
            bind_native("method", (void (*)(JNIEnv *, jSomething))(method));
            bind_native("method", (void (*)(JNIEnv *, jSomething, jint))(method));
            bind_native("method", (void (*)(JNIEnv *, jclass, jfloat))(method));
        });
    } 

    static void method(JNIEnv * env, jSomething obj);
    static void method(JNIEnv * env, jSomething obj, jint);
    static void method(JNIEnv * env, jclass cls, jfloat);
};

Best practice is to simply use different names and not to deal with this issue.