Skip to content

Representing Java Classes

Eugene Gershnik edited this page Apr 19, 2023 · 9 revisions

Once you declared Java types as is explained in Declaring Java Types you will need to use the corresponding Java "class object" (represented by the jclass type in raw JNI). First of all here is how to get a smart reference to jclass

JNIEnv * env = ...;

//Exact equivalent of JNI's FindClass. Returns null pointer on failure
local_java_ref<jclass> cls = java_runtime::find_class<jSomething>(env);
if (cls)
{
    ...
}

//Exact equivalent of JNI's FindClass. Throws on failure and never returns null. 
local_java_ref<jclass> cls = java_runtime::get_class<jSomething>(env);

This reference can be used to initialize java_class<jSomething> wrapper like this

java_class<jSomething> cls(env, [] (JNIEnv * env) {
    java_runtime::get_class<jSomething>(env)
});

If your class comes from some custom loading method you can do something else in the lambda

java_class<jSomething> cls(env, [] (JNIEnv * env) {
    //Load class here using your custom way
    return ...local_java_ref<jclass> object...;
});

Note that you don't need to pass any class names in. You already declared what the class name is when you defined jSomething. Also note that the loading code is passed to java_class constructor as a lambda. The lambda will be invoked only if the class needs to be loaded and the constructor cannot fetch it more efficiently. All instances of java_class<T> share the same jclass pointer. As long as one instance of java_class<T> exists all newly created ones will be able to reuse the previously loaded one.

Unless you are doing something one time accessing classes on-demand as shown above is problematic, however. You risk spending time locating the class object every time this code is run. If you made a mistake you will only discover it when this code is run which might be a rare or hard to reproduce scenario. A better approach in almost all cases is to use early binding. This essentially mimics how native dynamic libraries are loaded by the OS loader. Everything is located and hooked up at once in JNI_OnLoad call. Let's use this approach for jSomething and jSomethingElse.

//Declare designated types to represent each class object

struct ClassOfSomething : public java_runtime::simple_java_class<jSomething>
{
    ClassOfSomething(JNIEnv * env):
        simple_java_class(env)
    {} 
};

struct ClassOfSomethingElse : public java_runtime::simple_java_class<jSomethingElse>
{
    ClassOfSomethingElse(JNIEnv * env):
        simple_java_class(env)
    {} 
};

//"Table" of all classes we use
typedef java_class_table<ClassOfSomething, ClassOfSomethingElse> java_classes;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
    ...other things...

    //Load everything in the table
    java_classes::init(env);

   
    ...other things...
}

java_runtime::simple_java_class inherits from java_class and can be used anywhere java_class objects are needed. The java_class_table accepts a variable number of template arguments. You can put all the classes you need there. See Initialization for details about how to write JNI_OnLoad. With this in place you can easily get to a given class from anywhere like this

const auto & cls = java_classes::get<ClassOfSomething>();

In addition to convenience the power of this approach comes from the fact that you can also store and initialize methods and fields objects in the custom classes as well as register your native methods implementation. Consider the following Java class.

package com.mystuff;

class Something
{
    void javaMethod();
    native void nativeMethod();
}

This can be represented as follows

DEFINE_JAVA_TYPE(jSomething,       "com.mystuff.Something");

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

    static void JNICALL nativeMethod(JNIEnv * env, jSomething self);

    const java_method<void, jSomething> javaMethod;
};

typedef java_class_table<ClassOfSomething, ...other classes...> java_classes;

...

//Call javaMethod on some object
JNIEnv * env = ...;
jSomething obj = ...;
java_classes::get<ClassOfSomething>().javaMethod(env, obj);

//Implement native method
void JNICALL ClassOfSomething::nativeMethod(JNIEnv * env, jSomething self)
{
    ...
}

When java_classes type is initialized the constructor of ClassOfSomething will run, locating the class object, resolving javaMethod and registering nativeMethod implementation. If any of these will fail an exception will be reported there and then. If all succeed you will be able to easily access the class and all Java methods anywhere in your native code without any more runtime lookups. More details about implementing native Java methods and registering them can be found in Implementing Native Methods.

Also note that the structure of ClassOfSomething is extremely regular corresponding in a straightforward manner to declaration of the Java class Something it represents. This makes it easy to write for any Java class you need in your code. SimpleJNI provides a tool - JniGen to automatically generate code like this from annotations on your Java classes.