Search code examples
androidc++11java-native-interfaceshared-ptrshared-memory

JNI how to manage the life cycle of a c++ buffer


I have an image buffer allocated on the heap in my c++ code that I would like to share with some other c++ objects as well as Java objects through JNI. On the native side im using shared_ptr and I was wondering what is the best way to do so ? my thought is to allocate the buffer on the heap once and share a reference everywhere. I'm taking advantage of smart pointers so that the buffer will be deallocated as soon as all the references go out of scope, but I'm facing an issue when sharing a reference to the java side.

how can I ensure that my java object has a valid reference to the buffer all the time ? how can c++ determine that the reference counter reaches 0 when java object is done using its reference. My concern is to avoid memory leak and also ensure that buffer doesn't get destroyed too soon before getting processed by the java class.

thanks for the help


Solution

  • The general answer is "make the Java object's lifetime influence the lifetime of the C++ object".

    Start with the following Java class:

    class Refholder implements AutoCloseable {
        private long ptr; // the actual pointer
        private long shared_ptr; // a pointer to a shared_ptr keeping `ptr` alive
        public Refholder(long ptr, long shared_ptr) {
            this.ptr = ptr;
            this.shared_ptr = shared_ptr;
        }
        public native void close();
        public void finalize() { close(); }
        // Other methods to access the contents of `ptr` go here.
    };
    

    This will contain both the actual pointer and a pointer to a shared_ptr.

    When you want to hand a reference to Java, use the following:

    jobject passToJava(JNIEnv *env, std::shared_ptr<Foo> instance) {
        jclass cls_Refholder = env->FindClass("Refholder");
        jmethodID ctr_Refholder = env->GetMethodID(cls_Refholder, "<init>", "(JJ)V");
        // This line increases the reference count and remembers where we put the copy
        std::shared_ptr<Foo> *copy = new std::shared_ptr<Foo>(std::move(instance));
        jobject ret = env->NewObject(cls_Refholder, ctr_Refholder, copy->get(), copy);
        return ret;
    }
    

    Finally, the close method is responsible for extracting the shared_ptr and deallocating it:

    JNIEXPORT void Java_Refholder_close(JNIEnv *env, jobject obj) {
        jclass cls_Refholder = env->GetObjectClass(obj);
        jfieldID fld_Refholder_ptr = env->GetFieldID(cls_Refholder, "ptr", "J");
        jfieldID fld_Refholder_shared_ptr = env->GetFieldID(cls_Refholder, "shared_ptr", "J");
    
        std::shared_ptr<Foo> *copy = (std::shared_ptr<Foo>*)env->GetLongField(obj, fld_Refholder_shared_ptr);
        if (!copy)
            return;
    
        env->SetLongField(obj, fld_Refholder_ptr, 0);
        env->SetLongField(obj, fld_Refholder_shared_ptr, 0);
        delete copy;
    }
    

    I have decided to implement both AutoCloseable and finalize because I do not know how your Java code plans to use the reference. If you need deterministic destruction of the C++ object you need to use try-with-resources or explicit calls to the close method. If you do not, at last the finalize will close it.