Aims to provide best practices for maintaining the ownership and lifecycle of logically paired Java / C++ objects (e.g. if you have a Foo object in Java and one in C++ that are meant to represent the same concept).
Either the Java or C++ object should own the lifecycle of the corresponding object in the other language. The initially created object (either in C++ or Java) should be the object that creates and owns the corresponding other object.
It is the responsibility of the initial object to handle destruction / teardown of the corresponding other object.
Because Java objects are garbage collected and finalizers are prohibited in Chromium (link), an explicit destroy / teardown method on the Java object is required to prevent leaking the corresponding C++ object. The destroy / teardown method on the Java object would call an appropriate function on the C++ object (via JNI) to trigger the deletion of the C++ object. At this point, the Java object should reset its pointer reference to the C++ object to prevent any calls to the now destroyed C++ instance.
For C++ objects, utilizing the appropriate smart java references (link, code ref) will ensure corresponding Java objects can be garbage collected. But if the Java object requires cleaning up dependencies, the C++ object should call a corresponding teardown method on the Java object in its destructor.
Even in cases where the Java object does not have dependencies requiring clean up, the C++ object should notify the Java object that is has gone away. Then the Java object can reset its pointer reference to the C++ object and prevent any calls to the already destroyed object.
There should be one Java object per native object (and vice versa) to keep the lifecycle simple and easily understood.
For example, there is one BookmarkModel per Chrome profile in C++, and therefore, there should only be one BookmarkModel instance per Profile in Java.
Where possible, keep the business logic in either C++ or Java, and have the other object simply act as a shim to the other.
To facilitate cross-platform development, C++ is the preferred place for business logic that could be shared in the future.
The code of the Java and C++ object should be colocated to ensure consistent layering and dependencies..
If the C++ object is in //components/[foo], then the corresponding Java object should also reside in //components/[foo].
The C++ code shared across platforms and the corresponding Java class should be as close as possible in the code.
For cases where there are just a few Java <-> C++ calls, try to simply inline those into the same C++ file to minimize indirection.
Example:
//components/[foo]/foo_factory.cc
<...> cross platform includes #if BUILDFLAG(IS_ANDROID) #include “base/android/scoped_java_ref.h” #include “components/[foo]/android/jni_headers/FooFactory_jni.h” #endif // BUILDFLAG(IS_ANDROID) <...> shared functions #if BUILDFLAG(IS_ANDROID) static ScopedJavaLocalRef<jobject> JNI_FooFactory_Get(JNIEnv* env) { return FooFactory::Get()->GetJavaObject(); } #endif // BUILDFLAG(IS_ANDROID)
For cases where the Java <-> C++ API surface is substantial (e.g. if you have a C++ object with a large public API and you want to expose all those functions to Java), you can split out a JNI methods to a separate class that is owned by the primary C++ object. This approach is suitable when we want to minimize the JNI boilerplate in the C++ class.
Example:
//components/[foo]/foo.h
class Foo { public: <...> #if BUILDFLAG(IS_ANDROID) void DoSomething(); #endif // BUILDFLAG(IS_ANDROID) private: #if BUILDFLAG(IS_ANDROID) std::unique_ptr<FooAndroid> foo_android_; #endif // BUILDFLAG(IS_ANDROID) }
//components/[foo]/foo.cc
<...> #if BUILDFLAG(IS_ANDROID) void Foo::DoSomething() { if (!foo_android_) { foo_android_ = std::make_unique<FooAndroid>(this); } foo_android_->DoSomething(); } #endif // BUILDFLAG(IS_ANDROID)
//components/[foo]/android/foo_android.h
class FooAndroid { public: void DoAThing(); // JNI methods called from Java. void SomethingElse(JNIEnv* env); jboolean AndABooleanToo(JNIEnv* env); <...> private: const raw_ptr<Foo> foo_; base::android::ScopedJavaGlobalRef<jobject> java_ref_; }
//components/[foo]/android/foo_android.cc
FooAndroid::FooAndroid(Foo* foo) : foo_(foo) {} FooAndroid::DoAThing() { Java_Foo_DoAThing(base::android::AttachCurrentThread(), java_ref_); } void FooAndroid::SomethingElse(JNIEnv* env) { foo_->SomethingElse(); } jboolean FooAndroid::AndABooleanToo(JNIEnv* env) { return foo->AndABooleanToo(); }
We do not allow the use of finalizers, but there are a couple of other tricks that have been used to clean up objects besides explicit lifetimes: