Back to Home

JNI C++ wrapper for Android: classes and callbacks

The article describes creating Java wrappers for C++ classes in Android via JNI with storing pointers as long. Implemented asynchronous callbacks from native threads using ThreadPool and weak global references. Full code examples and memory management recommendations are provided.

C++ JNI wrappers in Android: from classes to native callbacks
Advertisement 728x90

JNI Wrapper for C++ Classes in Android: Creation and Callbacks

Integrating object-oriented C++ code into an Android app via JNI requires converting it into C functions while storing a pointer to the object. The C++ object's address is saved as a long in a Java class, enabling lifecycle management and multiple instances.

Consider a basic C++ class:

class NativeClass {
public:
    NativeClass() {}
    ~NativeClass() {}
    void fun() {
        __android_log_write(ANDROID_LOG_ERROR, log_tag, "Hello from native");
    }
};

JNI functions for working with the class:

Google AdInline article slot
  • Initialization: jlong Java_com_myprog_example_Native_initNativeClass(JNIEnv* env, jobject thiz) returns (jlong) new NativeClass();
  • Method call: void Java_com_myprog_example_Native_NativeClassFun(JNIEnv env, jobject thiz, jlong cppClass) calls ((NativeClass) cppClass)->fun();
  • Destruction: void Java_com_myprog_example_Native_destroyNativeClass(JNIEnv env, jobject thiz, jlong cppClass) executes delete (NativeClass) cppClass;

Java wrapper:

public class Native { 
    static { System.loadLibrary("Native"); } 
    public static native long initNativeClass();
    public static native void destroyNativeClass(long cppClass);
    public static native void NativeClassFun(long cppClass);
}

public class NativeClass {
    long cppClass;
    NativeClass() {
        this.cppClass = Native.initNativeClass();
    }
    @Override protected void finalize() {
        free();
    }
    private void free() {
        Native.destroyNativeClass(cppClass);
    }
    public void fun() {
        Native.NativeClassFun(cppClass);
    }
}

Implementing Callbacks from Native Code

For asynchronous scenarios, such as network scanning, native code must call Java methods during operation. A Listener interface in C++ and a ThreadPool are used to serialize calls in a single thread.

Extended C++ class with callback support:

Google AdInline article slot
class NativeClass {
public:
    class Listener {
    public:
        virtual void print(const char* str) = 0;
        virtual ~Listener() {}
    };
private:
    bool started = false;
    Listener* listener = nullptr;
    static void worker(NativeClass* nc) {
        while(nc->started) {
            if(nc->listener != nullptr) {
                nc->listener->print("From native");
            }
            sleep(1);
        }
    }
public:
    NativeClass() {}
    ~NativeClass() {
        if(listener != nullptr) delete listener;
    }
    void setListener(Listener* listener) {
        this->listener = listener;
    }
    void scan() {
        started = true;
        std::thread thread = std::thread(worker, this);
        thread.join();
        started = false;
    }
    void stop() {
        started = false;
    }
};

Java interface and wrapper:

public interface NativeListener {
    void print(String str);
}

public class NativeClass {
    long cppClass;
    // constructor, finalize, free as above
    public void scan() { Native.NativeClassScan(cppClass); }
    public void stop() { Native.NativeClassStop(cppClass); }
    public void setListener(NativeListener listener) {
        Native.NativeClassSetListener(cppClass, listener);
    }
}

Key component — implementing Listener in C++ with JNI:

class Listener : public NativeClass::Listener {
private:
    JavaVM* jvm;
    JNIEnv* env;
    jweak listenerRef;
    jmethodID printMethod;
public:
    Listener(JavaVM* jvm, JNIEnv* env, jobject listener) {
        this->jvm = jvm;
        listenerRef = env->NewWeakGlobalRef(listener);
        ThreadPool::execute({
            jvm->AttachCurrentThread(&env, NULL);
            if(env != NULL) {
                jobject localRef = env->NewLocalRef(listenerRef);
                if(localRef != NULL) {
                    jclass clazz = env->GetObjectClass(localRef);
                    printMethod = env->GetMethodID(clazz, "print", "(Ljava/lang/String;)V");
                    env->DeleteLocalRef(localRef);
                }
            }
        });
    }
    ~Listener() {
        ThreadPool::execute({
            env->DeleteWeakGlobalRef(listenerRef);
            jvm->DetachCurrentThread();
        });
    }
    void print(const char* msg) {
        ThreadPool::execute({
            jobject localRef = env->NewLocalRef(listenerRef);
            if(localRef != NULL) {
                jstring jMsg = env->NewStringUTF(msg);
                env->CallVoidMethod(localRef, printMethod, jMsg);
                env->DeleteLocalRef(jMsg);
                env->DeleteLocalRef(localRef);
            }
        });
    }
};

JNI function for setting the listener:

Google AdInline article slot
JNIEXPORT void JNICALL Java_com_myprog_example_Native_NativeClassSetListener(
    JNIEnv* env, jobject thiz, jlong cppClass, jobject listener) {
    ((NativeClass*) cppClass)->setListener(new Listener(jvm, env, listener));
}

Implementation Recommendations

  • Single-threaded ThreadPool: Ensures sequential JNI calls from a fixed thread.
  • Weak references (jweak): Prevent memory leaks; strong local references are created only for the duration of the call.
  • Lifecycle management: The Listener destructor cleans up resources via the ThreadPool.

List of key practices:

  • Always check for NULL on local references before use.
  • Use NewWeakGlobalRef for long-lived references and delete them timely.
  • Call AttachCurrentThread/DetachCurrentThread only within the ThreadPool.
  • For synchronous callbacks, add condition variables.
  • Avoid the UI thread for blocking operations like scan().

Key Takeaways

  • Storing a C++ object pointer as a long in Java provides full object semantics.
  • A single-threaded ThreadPool is critical for stable JNI calls from native threads.
  • Weak global references (jweak) + local refs minimize memory leak risks.
  • Java wrapper finalization automatically cleans up native resources.
  • Asynchronous callbacks are suitable for real-time tasks like network scanning.

— Editorial Team

Advertisement 728x90

Read Next