xref: /aosp_15_r20/external/fbjni/docs/rationale.md (revision 65c59e023c5336bbd4a23be7af78407e3d80e7e7)
1*65c59e02SInna Palant# Why a JNI wrapper?
2*65c59e02SInna Palant
3*65c59e02SInna Palant## Simplified code
4*65c59e02SInna Palant
5*65c59e02SInna PalantStandard JNI is very verbose and error prone. The goal of the fbjni is to make its usage simple, robust and extensible. Below is an example of plain JNI code to call the `toString()` method of an object.
6*65c59e02SInna Palant
7*65c59e02SInna Palant```cpp
8*65c59e02SInna Palant// JNI Example: toString()
9*65c59e02SInna Palantstd::string jniToString(JNIEnv* env, jobject self) {
10*65c59e02SInna Palant  static auto cls = env->FindClass("java/lang/Object");
11*65c59e02SInna Palant  static auto toStringId =
12*65c59e02SInna Palant    env->GetMethodID(cls, "toString", "()Ljava/lang/String;");
13*65c59e02SInna Palant
14*65c59e02SInna Palant  auto jstr = static_cast<jstring>(env->CallObjectMethod(self, toStringId));
15*65c59e02SInna Palant  auto chbuf = env->GetStringUTFChars(jstr, nullptr);
16*65c59e02SInna Palant  auto stdstr = std::string(chbuf);
17*65c59e02SInna Palant  env->ReleaseStringUTFChars(jstr, chbuf);
18*65c59e02SInna Palant
19*65c59e02SInna Palant  return stdstr;
20*65c59e02SInna Palant}
21*65c59e02SInna Palant```
22*65c59e02SInna Palant
23*65c59e02SInna PalantThe code is already verbose and there are some quirks, but most importantly it has no error handling. By adding error handling we get the following code.
24*65c59e02SInna Palant
25*65c59e02SInna Palant```cpp
26*65c59e02SInna Palant// JNI Example with error handling: toString()
27*65c59e02SInna Palantstd::string jniToString(JNIEnv* env, jobject self) {
28*65c59e02SInna Palant  static auto cls = env->FindClass("java/lang/Object");
29*65c59e02SInna Palant  assert(cls != nullptr);
30*65c59e02SInna Palant  static auto id = env->GetMethodID(cls, "toString", "()Ljava/lang/String;");
31*65c59e02SInna Palant  assert(id != nullptr);
32*65c59e02SInna Palant
33*65c59e02SInna Palant  auto jstr = static_cast<jstring>(env->CallObjectMethod(self, toStringId));
34*65c59e02SInna Palant  throwIfJavaExceptionRaised(env);
35*65c59e02SInna Palant
36*65c59e02SInna Palant  auto chbuf = env->GetStringUTFChars(jstr, nullptr);
37*65c59e02SInna Palant  throwIfJavaExceptionRaised(env);
38*65c59e02SInna Palant
39*65c59e02SInna Palant  auto stdstr = std::string(chbuf);
40*65c59e02SInna Palant  env->ReleaseStringUTFChars(jstr, chbuf);
41*65c59e02SInna Palant
42*65c59e02SInna Palant  return stdstr;
43*65c59e02SInna Palant}
44*65c59e02SInna Palant```
45*65c59e02SInna PalantIn this example we skipped the implementation of `throwIfJavaExceptionRaised()` for the sake of readability. Using fbjni we can greatly reduce the code with robust error handling.
46*65c59e02SInna Palant
47*65c59e02SInna Palant```cpp
48*65c59e02SInna Palant// fbjni Example: toString()
49*65c59e02SInna Palantstd::string helperToString(alias_ref<jobject> obj) {
50*65c59e02SInna Palant  static auto toStringMethod =
51*65c59e02SInna Palant    findClass("java/lang/Object")->getMethod<jstring()>("toString");
52*65c59e02SInna Palant
53*65c59e02SInna Palant  return toStringMethod(obj)->toStdString();
54*65c59e02SInna Palant}
55*65c59e02SInna Palant```
56*65c59e02SInna PalantActually, `toString()` is often useful and thus it is made easily available.
57*65c59e02SInna Palant
58*65c59e02SInna Palant```cpp
59*65c59e02SInna Palant// fbjni Example (alternative): toString()
60*65c59e02SInna Palantstd::string helperToString(alias_ref<jobject> obj) {
61*65c59e02SInna Palant  return obj->toString();
62*65c59e02SInna Palant}
63*65c59e02SInna Palant```
64*65c59e02SInna PalantThe implementations do basically the same things as the plain JNI example. The major difference is that all errors are handled by throwing C++ exceptions. In the plain JNI code some assertions were used to simplify the code.
65*65c59e02SInna Palant
66*65c59e02SInna Palant
67*65c59e02SInna Palant## JNI is a C API
68*65c59e02SInna Palant
69*65c59e02SInna PalantThere is very little syntactic sugar in the API to help you manage resources. And since it is an API specification only, the implementation varies by your JVM — the majority of articles on the web assume HotSpot rather than Dalvik. The documentation is heavily skewed towards the use case where Java calls into a single, self-contained native function (and this is usually a fairly easy case to handle). However, our code is usually asynchronous, requiring callbacks or other stateful interactions between Java and native code.
70*65c59e02SInna Palant
71*65c59e02SInna Palant## Memory Lifecycle
72*65c59e02SInna Palant
73*65c59e02SInna PalantJava and native code have separate memory heaps, so managing the lifecycle of an object that is used on both sides requires extra care. JNI provides the concept of references, which allows native code to tell the JVM that it is still using an object and prevent that object from being garbage collected. When calling from Java, all parameters to the native function are automatically turned into local references, which are dropped when the call completes. To maintain state on the native side or initiate calls into Java, you need to manage these references yourself. References come from fixed size pools, so in addition to leaking the Java objects they point to, leaking or double-freeing references is a problem in and of itself. fbjni provides RAII smart references which tremendously simplify the management of these references.
74*65c59e02SInna Palant
75*65c59e02SInna PalantConversely, C++ smart pointers only survive as long as C++ code is holding them. Via reinterpret_cast, pointers to heap memory can be stored in Java objects, but you need to be diligent in making sure the C++ allocations are deleted when the Java object becomes unused.  fbjni's Hybrid class provides a convenient way to encapsulate this behavior.
76*65c59e02SInna Palant
77*65c59e02SInna Palant## Exceptions
78*65c59e02SInna Palant
79*65c59e02SInna PalantMost usages of both C++ and Java use exceptions. These two exception frameworks are incompatible, and an exception hitting the JNI boundary therefore becomes a problem: C++ exceptions will cause the program to abort, while Java exceptions require C-style checking for the existence of an error condition. Unhandled Java exceptions will cause the *next* JNI call to abort, which may be completely unrelated to the code that caused the exception in the first place. fbjni provides a translation facility for exceptions that lets them be passed across the boundary in a sane fashion, along with helpers to wrap C++ functions in try/catch blocks.
80*65c59e02SInna Palant
81*65c59e02SInna Palant## Java Type Signatures
82*65c59e02SInna Palant
83*65c59e02SInna PalantNative functions need to specify a stringified version of the Java function signature they correspond to (called a descriptor). This is mechanical; javac can do it for you. However this flow is inconvenient; you can’t use the javah-generated function prototypes as-is, for example, because they assume implicit function registration, which is not reliable on older versions of Android. Getting the descriptor wrong (or forgetting to update it during a refactor) usually results in a very quick but difficult to debug crash. fbjni includes wrappers which deduce the function signature for you based the C++ function prototype.
84*65c59e02SInna Palant
85*65c59e02SInna Palant## Type Safety
86*65c59e02SInna Palant
87*65c59e02SInna PalantJNI provides a single base object type of jobject, with token specializations for jstring, jclass and jexception. fbjni makes it relatively easy to extend this so that any arbitrary Java class can be handled in C++ in a type-safe manner.
88