A zero-overhead (or better!) middleware for JNI. Works on JVMs, but the focus is Android.
Recommended pre-reading: https://developer.android.com/ndk/guides/jni-tips
Googlers, see: go/jnizero.
JNI (Java Native Interface) is the mechanism that enables Java code to call native functions, and native code to call Java functions.
<jni.h>, which basically mirror Java's reflection APIs.native keyword, and then calling them as normal Java functions.JNI Zero generates boiler-plate code with the goal of making our code:
JNI Zero uses regular expressions to parse .java files, so don't do anything too fancy :).
Pointers to Java objects must be registered with JNI in order to prevent garbage collection from invalidating them.
To help with this, JNI Zero provides the following smart pointers:
ScopedJavaLocalRef<> - When lifetime is the current function's scope.ScopedJavaGlobalRef<> - When lifetime is longer than the current function's scope.LeakedJavaGlobalRef<> - For singletons (avoids having a destructor).JavaObjectWeakGlobalRef<> - Weak reference (does not prevent garbage collection).JavaRef<>& - Use to accept any of the above as a parameter to a function without creating a redundant registration.jni.h provides a limited number of types to represent Java objects. E.g.:
jobjectjstringjthrowablejclassTo provide type-safety, JNI Zero generates subclasses for all referenced Java classes. E.g.:
JListJMapJMyClassEach of these types is defined in a C++ namespace that mirrors its Java package, and is aliased to the top-level scope on a first-come basis.
Example usage:
jni_zero::ScopedJavaLocalRef<JList> GetValues(const jni_zero::JavaRef<JMap>& map) {
...
}
These custom subclasses are defined in a generated ClassName_shared_jni.h header so that they can be used from header files without pulling in all of the method-calling-related codegen (which lives in ClassName_jni.h).
long native${OriginalClassName}), then the bindings will not call a static function but instead cast the variable into a cpp ${OriginalClassName} pointer type and then call a member method with that name on said object.To add JNI to a class:
@NativeMethods that contains the declaration of the corresponding static methods you wish to have implemented.${OriginalClassName}Jni.get().${method}()#include "${OriginalClassName}_jni.h"generate_jni build rule that lists your Java source code.@JniType annotations.DEFINE_JNI(JavaClassName) to the bottom of your .cc file_jni.h file.JNI_${ClassName}_${UpperCamelCaseMethod}${OriginalClassName}::${UpperCamelCaseMethod}Java
class MyClass { // Cannot be private. Must be package or public. @NativeMethods /* package */ interface Natives { void foo(List<String> list); double bar(int a, int b); // Either the |MyClass| part of the |nativeMyClass| parameter name must // match the native class name exactly, or the method annotation // @NativeClassQualifiedName("MyClass") must be used. // // If the native class is nested, use // @NativeClassQualifiedName("FooClassName::BarClassName") and call the // parameter |nativePointer|. void nonStatic(long nativeMyClass); } void callNatives() { // MyClassJni is generated by the generate_jni rule. // Storing MyClassJni.get() in a field defeats some of the desired R8 // optimizations, but local variables are fine. Natives jni = MyClassJni.get(); jni.foo(List.of("hi")); jni.bar(1,2); jni.nonStatic(mNativePointer); } }
C++
#include "third_party/jni_zero/jni_zero.h" // Must come after all headers that specialize FromJniType() / ToJniType(). #include "<path to BUILD.gn>/<generate_jni target name>/MyClass_jni.h" class MyClass { public: // The JNIEnv* parameter is optional. void NonStatic(JNIEnv* env); } namespace { // Can also declare each with `static` // The JNIEnv* parameter is optional. void JNI_MyClass_Foo(JNIEnv* env, const jni_zero::JavaRef<JList>& list) { ... } void JNI_MyClass_Bar(int32_t a, int32_t b) { ... } } // namespace void MyClass::NonStatic(JNIEnv* env) { ... } DEFINE_JNI(MyClass)
Directly expose Java methods using the native keyword and JNI Zero will generate the bindings. This still works, but we are keen to drop support once all usage has been migrated.
Annotate some methods with @CalledByNative, the generator will now generate stubs in ${OriginalClassName}_jni.h header to call into those java methods from cpp.
In C++ code, #include the header ${OriginalClassName}_jni.h. (The path will depend on the location of the generate_jni build rule that lists your Java source code).
Call the generated methods using the ClassNameJni class or the JClassName type.
ScopedJavaLocalRef<JMyClass> obj = MyClassJni::New(env, ...);MyClassJni::staticMethod(env, ...);obj->instanceMethod(env, ...);Note: For test-only methods, use @CalledByNativeForTesting which will ensure that it is stripped in our release binaries.
Java
class MyClass { @CalledByNative MyClass() {} @CalledByNative int method() { return 0; } }
C++
#include "third_party/jni_zero/jni_zero.h" // Must come after all headers that specialize FromJniType() / ToJniType(). #include "<path to BUILD.gn>/<generate_jni target name>/MyClass_jni.h" void Example() { JNIEnv* env = jni_zero::AttachCurrentThread(); jni_zero::ScopedJavaLocalRef<JMyClass> ref = MyClassJni::New(env); ref->method(env); }
Calling methods like: Java_ClassName_methodName(env, ...).
This syntax still works, but support will be dropped when all usages are migrated. It does not use jobject subclasses.
Normally, JNI Zero maps Java types to C++ types as follows:
| Java Type | C++ Type |
|---|---|
String | jstring |
Throwable | jthrowable |
Class | jclass |
Any other object | jobject |
boolean | bool |
byte | int8_t |
char | uint16_t |
short | int16_t |
int | int32_t |
long | int64_t |
float | float |
double | double |
T[] | jobjectArray |
boolean[] | jbooleanArray |
short[] | jshortArray |
... | ... |
By annotating a parameter or a return type with @JniType("cpp_type_here") the generated code will convert from the JNI type to the type listed inside the annotation.
@JniType can be used to convert primitives to enums, or Java types to C++ types, but there can be only one conversion for each C++ type. E.g. you cannot have a different conversion from String <-> std::string and URI <-> std::string.
Annotating your class with @JNINamespace("foo") will result in a using namespace ::foo; being added to the codegen, allowing for @JniType strings to be reference types that are defined in a namespace.
Java
class MyClass { @NativeMethods interface Natives { void foo( @JniType("std::string") String convertedString, @JniType("std::vector<std::string>") String[] convertedStrings, @JniType("myModule::CPPClass") MyClass convertedObj, @JniType("std::vector<myModule::CPPClass>") MyClass[] convertedObjects); } }
C++
#include "third_party/jni_zero/jni_zero.h" #include "<path to BUILD.gn>/<generate_jni target name>/MyClass_jni.h" void JNI_MyClass_Foo(JNIEnv* env, const std::string&, const std::vector<std::string>>&, myModule::CPPClass&&, const std::vector<myModule::CPPClass>&) { ... }
JNI Zero provides built-in conversions for several common C++ and Java types within third_party/jni_zero/default_conversions.h.
| C++ Type | Java Type |
|---|---|
std::optional<T> | @Nullable T |
std::vector<T> | T[] or List<T> |
std::map<K, V> | Map<K, V> |
bool | Boolean (boxed) |
int32_t | Integer (boxed) |
int64_t | Long (boxed) |
float | Float (boxed) |
double | Double (boxed) |
Note: std::vector<T> and std::map<K, V> conversions work by recursively calling ToJniType / FromJniType on their elements.
Note: When going from C++ -> Java, any collection-like container should work (e.g. std::set).
For Chromium-specific types (like std::string or base::OnceClosure), see README.chromium.md.
Conversion functions must exist for types that appear in @JniType. Forgetting to #include the header that defines it will will result in a compile error.
// The conversion function primary templates. template <typename O> O FromJniType(JNIEnv*, const JavaRef<jobject>&); template <typename O> ScopedJavaLocalRef<jobject> ToJniType(JNIEnv*, const O&);
Example conversion function:
#include "third_party/jni_zero/jni_zero.h" namespace jni_zero { template <> EXPORT std::string FromJniType<std::string>( JNIEnv* env, const JavaRef<jstring>& input) { // Do the actual conversion to std::string. } template <> EXPORT ScopedJavaLocalRef<jstring> ToJniType<std::string>( JNIEnv* env, const std::string& input) { // Do the actual conversion from std::string. } } // namespace jni_zero
Array conversion functions look different due to the partial specializations. The ToJniType direction also takes a jclass parameter which is the class of the array elements, because java requires it when creating a non-primitive array.
template <typename O> struct ConvertArray { static O FromJniType(JNIEnv*, const JavaRef<jobjectArray>&); static ScopedJavaLocalRef<jobjectArray> ToJniType(JNIEnv*, const O&, jclass); };
JniZero provides implementations for partial specializations to wrap and unwrap std::vector for object arrays and some primitive arrays.
All non-primitive default JNI C++ types (e.g. jstring, jobject) are pointer types (i.e. nullable). Some C++ types (e.g. std::string) are not pointer types and thus cannot be nullptr. This means some conversion functions that return non-nullable types have to handle the situation where the passed in java type is null.
There are two ways to have native methods be found by Java:
RegisterNatives() function.dlsym()) the first time a native method is called.(2) Is generally preferred due to a smaller code size and less up-front work, but (1) is sometimes required (e.g. when OS bugs prevent dlsym() from working). Both ways are supported.
JNI Zero ships with R8 configs that disable renaming of symbols that use @CalledByNative.
/** * Tests for {@link AnimationFrameTimeHistogram} */ @RunWith(RobolectricTestRunner.class) public class AnimationFrameTimeHistogramTest { // Optional: Resets test overrides during tearDown(). // Not needed when using Chrome's test runners. @Rule public JniResetterRule jniResetterRule = new JniResetterRule(); @Mock AnimationFrameTimeHistogram.Natives mNativeMock; @Before public void setUp() { MockitoAnnotations.initMocks(this); AnimationFrameTimeHistogramJni.setInstanceForTesting(mNativeMock); } @Test public void testNatives() { AnimationFrameTimeHistogram hist = new AnimationFrameTimeHistogram("histName"); hist.startRecording(); hist.endRecording(); verify(mNativeMock).saveHistogram(eq("histName"), any(long[].class), anyInt()); } }
Each APK split with its own native library has its own generated GEN_JNI, which is <module_name>_GEN_JNI. In order to get your split's JNI to use the <module_name> prefix, you must add your module name into the argument of the @NativeMethods annotation.
So, for example, say your module was named test_module. You would annotate your Natives interface with @NativeMethods("test_module"), and this would result in test_module_GEN_JNI.
You must call System.loadLibrary("libname") before making JNI calls, and it is up to each application to do so. Using an app's Application subclass is a good place to do this.
UnsatisfiedLinkError, it’s likely that you are missing a deps entry onto the code that implements that C++ side of a JNI method.When you hit a scenario leading to an exception, relocate (or defer) the appropriate call to be made to a place where (or time when) you know the native libraries have been initialized (eg. onStartWithNative, onNativeInitialized etc).
Avoid calling LibraryLoader.isInitialized() / LibraryLoader.isLoaded(), because the tell you only whether System.loadLibray() has been called, and often return “true” in Robolectric tests, which contain only a small number of JNI methods.
One robust solution is to use your own “sIsNativeReady” flag that is set via a @CalledByNative method.
Minimize the surface API between the two sides. Rather than calling multiple functions across boundaries, call only one (and then on the other side, call as many little functions as required).
If a Java object “owns” a native one, store the pointer via "long mNativeClassName". Ensure to eventually call a native method to delete the object. For example, have a close() that deletes the native object.
Refer to the performance README.
For @CalledByNative, we directly call the <jni.h> methods, which are basically just reflection APIs, and then add a proguard rule to ensure the annotated method/field is kept in Java. The registration step does nothing for this direction of JNI, since we do not do any sort of proxying. However, using the registration step for @CalledByNatives has been discussed before: go/proxy-called-by-natives-proposal.
JNI Zero has 2 primary modes for @NativeMethods. In each, we insert a “proxy” class per annotated class which allows us to fake for tests and optimize better. We insert a class with the name of <EnclosingClass>Jni, and this class is just a testable shim into the “real” GEN_JNI class. This GEN_JNI class is generated at the registration step, and how the registration works is different in different modes.
For examples, we will imagine we have the following two classes:
class org.foo.Foo { @NativeMethods interface Natives { int f(); } } class org.bar.Bar { @NativeMethods interface Natives { int b(); } }
Which will have the 2 generate_jni steps output something like:
// Java .srcjar outputs class FooJni { public int f() { return GEN_JNI.org_foo_Foo_f(); } } class BarJni { public int b() { return GEN_JNI.org_bar_Bar_b(); } }
// C++ header outputs class FooJni { int Java_GEN_JNI_org_foo_Foo_f() { return JNI_Foo_f(); // User implements this native function. } int Java_GEN_JNI_org_bar_Bar_b() { return JNI_Bar_b(); // User implements this native function. }
In debug mode, the GEN_JNI is a file containing native methods that match every single @NativeMethods from every generate_jni in our program.
class GEN_JNI { public static native int org_foo_Foo_f(); public static native int org_bar_Bar_b(); }
In release mode, the GEN_JNI.java is just a callthrough shim to N.java (a short name to reduce size), and N uses multiplexing by signature type to reduce the number of JNI functions. Then, we generate a C++ file with matching names to the smaller list of functions in N, which de-multiplexes back into the original functions.
class GEN_JNI { public static int org_foo_Foo_f() { return N._I(0); } public static int org_bar_Bar_b() { return N._I(1); } } class N { public static native int _I(int switchNum); }
// Generated C++ to be compiled into the final binary. int Java_N__1V(int32_t switch_num) { switch (switch_num) { case 0: return org_foo_Foo_f(); case 1: return org_bar_Bar_b(); } }
We also have the concept of “priority” classes, which are classes which need to be in the front of the multiplexing numbers. This is not a performance thing, it‘s so that Chrome can support multiple ABIs with a single Java file - we put the smaller (subset) ABI switch numbers first, and the superset ABI’s unique classes get the final switch numbers.
This was added to make transitioning to JNI Zero easier. It allows using @NativeMethods without needing a registration step at the cost of extra binary size by putting the native methods directly in the FooJni classes.
Example:
class FooJni { public static int f() { nativeF(); } public static native nativeF(); } class BarJni { public static int b() { nativeB(); } public static native nativeB(); }
These are modes which JNI provides currently, but we hope to remove. Please do not add any new uses of these.
E.g.:
class Foo {
native someMethod();
}
This is still supported by default, but is less efficient than @NativeMethods interfaces. We plan to delete support for this.
This was our old release mode. GEN_JNI would call into N, just as it does for our current release mode, but instead of multipelxing, we'd just take a short hash of the name so we have shorter exported string literals. This would also change the output of the headers made by generate_jni, as they needed to likewise have a hashed name generated.
class GEN_JNI { public static int org_foo_Foo_f() { return N.MaQxW612(); } public static int org_bar_Bar_b() { return N.M2R2WaZb(); } } class N { public static native int MaQxW612(); public static native int M2R2WaZb(); }
In the original design of JNI Zero, generated .java files would be included directly into the android_library target that contains the annotated classes. This was necessary because the generated files can reference any type that the host source files can. If it were a separate library, then we'd have a circular dependency (the codegen depends on the original, and the original depends on the codegen).
The main downside of using a single target is that the generated FooJni classes refer to a placeholder “GEN_JNI” class, which the host library then needs to mark as compile-only (e.g. with jar_excluded_patterns). Another downside is that if one android_library depends on two generate_jni srcjars, the compiler complains of duplicate GEN_JNI.java classes.
To address both of these downsides, and for easier integration with Bazel (which supports neverlink, but not jar_excluded_patterns), we now generate separate android_library targets for the generated code. To avoid a circuclar dependency, we generate placeholder (compile-only) files for each type referenced by the generated code. See testPlaceholdersOverlapping-placeholder.srcjar.golden for an example.
test/integration_tests.pysample:jni_zero_sample_apk and this app is tested in sample:jni_zero_sample_apk_test.test:jni_zero_compile_check_apkjni_zero.py contains our flags and is the entry point, jni_generator.py is the main file for the per-library generation step, and jni_registration_generator.py is the main file for the whole-program registration step.