src/third_party/jni_zero/README.md
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.
[TOC]
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) |
jni_zero::ByteArrayView | byte[] |
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.
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();
}
This was added to make transitioning to JNI Zero easier. The idea is that this
allows you to partially onboard without needing to use a registration step, so
no GEN_JNI is generated at all, and the generate_jni step's outputs look
different than "normal" mode.
class FooJni {
public static int f() {
nativeF();
}
public static native nativeF();
}
class BarJni {
public static int b() {
nativeB();
}
public static native nativeB();
}
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.