0

There is a java application that actively uses C++. C++ code often calls methods in java and java code often calls methods in C++.

There is a cache on the C++ side that stores jweak references on java objects.

At some point in time, the java side call getObj on the C++ side, which should return an object from the cache.

JNIEXPORT jobject JNICALL Java_a_b_c_getObj(JNIEnv* env, jlong id) {
      jweak weak_obj = g_cache.get_obj_by_id(id);
      return env->NewLocalRef(weak_obj);
}

On the java side, this object is captured in the UI code and can even be processed somehow, but after a short time it may be called to finalize, the application crashes.

val obj = getObj(id) as Foo
obj.methodA() // good
// a little bit later
obj.methodB() // error because there was a finalize function that cast to the implementation of the object on the C++ side

Reproduced relatively stable.

It turns out that the garbage collector starts its work before the call to getObj, but the finalizer has not yet been dispatched. When an object fire in java starts to happen, finalize is called and the garbage collector doesn't know that the object is being used again.

Botje
  • 26,269
  • 3
  • 31
  • 41
  • 1
    Share `g_cache` code to understand how the cache works. Remember that you should provide a [minimal reproducible example](http://stackoverflow.com/help/minimal-reproducible-example). – aled Aug 18 '23 at 18:38
  • 2
    This is not a JNI problem as such. If `getObj` were implemented in Java you would have the same problem. The only solution is to rearchitect and avoid finalizers. – Botje Aug 21 '23 at 07:59
  • 1
    See [this recent question](https://stackoverflow.com/questions/76882381/alternative-for-deprecated-finalize-method-in-java-for-jni) for a replacement to finalizers – Botje Aug 21 '23 at 08:39
  • Without the use of finalizers, it's very hard to tell when an object is no longer needed, especially in my case in Jetpack Compose code that makes heavy use of the C++ backend. But I found a very interesting comment in the djinni project: "We don't use JNI WeakGlobalRef objects, because they last longer than is safe - a WeakGlobalRef can still be upgraded to a strong reference even during finalization, which leads to use-after-free. Java WeakRefs provide the right lifetime guarantee." Djinni uses in the same case JNI wrapper around WeakReference. I hope it solves my issue. – comrade_bender Aug 21 '23 at 10:46

1 Answers1

0

In general, Botje is right, don't use the finalize method along with JNI.

But if you still need to finalize then don't use jweak variables created with NewWeakGlobalRef.

Comment from djinni project:

We don't use JNI WeakGlobalRef objects, because they last longer than is safe - a WeakGlobalRef can still be upgraded to a strong reference even during finalization, which leads to use-after-free. Java WeakRefs provide the right lifetime guarantee.

If you can't do without weak references, use java.lang.ref.WeakReference. WeakReference objects in finilize must return null.

A simple C++ WeakReference wrapper:

struct GlobalRefDeleter {
    void operator()(jobject globalRef) noexcept {
        if (globalRef != nullptr) {
            if (JNIEnv* env = getEnv()) {
                env->DeleteGlobalRef(globalRef);
            }
        }
    }
};

template <typename PointerType>
class GlobalRef : public std::unique_ptr<typename std::remove_pointer_t<PointerType>, GlobalRefDeleter> {
    using Parent = std::unique_ptr<typename std::remove_pointer_t<PointerType>, GlobalRefDeleter>;

  public:
    GlobalRef() = default;
    GlobalRef(JNIEnv* env, PointerType localRef) : Parent(static_cast<PointerType>(env->NewGlobalRef(localRef)), GlobalRefDeleter{}) {
    }

    bool isSame(JNIEnv* env, jobject obj) const {
        return env->IsSameObject(obj, Parent::get());
    }
};

struct WeakReferenceJniInfo {
    GlobalRef<jclass> clazz;
    jmethodID constructor;
    jmethodID get;

    explicit WeakReferenceJniInfo(JNIEnv* env)
        : clazz{env, env->FindClass("java/lang/ref/WeakReference")},
          constructor{env->GetMethodID(clazz.get(), "<init>", "(Ljava/lang/Object;)V")},
          get{env->GetMethodID(clazz.get(), "get", "()Ljava/lang/Object;")} {
    }
};

// created in JNI_OnLoad
inline std::unique_ptr<const WeakReferenceJniInfo> g_weakReferenceJniInfo;

class WeakRef {
  public:
    WeakRef() = default;
    WeakRef(JNIEnv* env, jobject obj)
        : _weakRef(std::make_shared<GlobalRef<jobject>>(
              env, env->NewObject(g_weakReferenceJniInfo->clazz.get(), g_weakReferenceJniInfo->constructor, obj))) {
    }

    // Get the object pointed to if it's still strongly reachable or, return null if not.
    // (Analogous to weak_ptr::lock.) Returns a local reference.
    jobject lock(JNIEnv* env) const {
        if (_weakRef == nullptr) {
            return nullptr;
        }
        else {
            return env->CallObjectMethod(_weakRef->get(), g_weakReferenceJniInfo->get);
        }
    }

    bool isSame(JNIEnv* env, jobject obj) const {
        auto local = lock(env);
        auto result = env->IsSameObject(obj, local);
        env->DeleteLocalRef(local);
        return result;
    }

  private:
    std::shared_ptr<GlobalRef<jobject>> _weakRef;
};