Clone this repo:

Branches

  1. 8685c64 Revert "JNI Zero: Do not #include default_conversions.h by default" by Andrew Grieve · 2 days ago main
  2. b0a152a JNI Zero: Do not #include default_conversions.h by default by Andrew Grieve · 3 days ago
  3. 91c51e7 JNI Zero: Do not allow a parameter for @CalledByNative by Andrew Grieve · 4 days ago
  4. c92c094 Simplify JNI usage in modal_dialog_wrapper.cc by Andrew Grieve · 5 days ago
  5. 67bd55f JNI Zero: Document placeholder .jar files by Andrew Grieve · 6 days ago

JNI Zero

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.

Overview

JNI (Java Native Interface) is the mechanism that enables Java code to call native functions, and native code to call Java functions.

  • Native code calls into Java using apis from <jni.h>, which basically mirror Java's reflection APIs.
  • Java code calls native functions by declaring body-less functions with the native keyword, and then calling them as normal Java functions.

JNI Zero generates boiler-plate code with the goal of making our code:

  1. easier to write,
  2. typesafe,
  3. more optimizable.

JNI Zero uses regular expressions to parse .java files, so don't do anything too fancy :).

Usage

Java Smart Pointers

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.:

  • jobject
  • jstring
  • jthrowable
  • jclass

To provide type-safety, JNI Zero generates subclasses for all referenced Java classes. E.g.:

  • JList
  • JMap
  • JMyClass

Each 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).

Calling Java -> Native

  • For each JNI method:
    • C++ stubs are generated that forward to C++ functions that you must write. By default the c++ functions you are expected to implement are not associated with a class.
    • If the first parameter is a C++ object (e.g. 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:

  1. Create a nested-interface annotated with @NativeMethods that contains the declaration of the corresponding static methods you wish to have implemented.
  2. Call native functions using ${OriginalClassName}Jni.get().${method}()
  3. In C++ code, add: #include "${OriginalClassName}_jni.h"
    • The path will depend on the location of the generate_jni build rule that lists your Java source code.
    • The header should generally be included last, as it must appear after headers that define types used in @JniType annotations.
  4. Add DEFINE_JNI(JavaClassName) to the bottom of your .cc file
  5. Implement the native methods.
    • If unsure of what the signatures should look like, inspect the generated _jni.h file.
    • The naming scheme is
      • Non-class methods: JNI_${ClassName}_${UpperCamelCaseMethod}
      • Class methods: ${OriginalClassName}::${UpperCamelCaseMethod}

Example:

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)

Legacy Syntax

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.

Calling Native -> Java

  1. Annotate some methods with @CalledByNative, the generator will now generate stubs in ${OriginalClassName}_jni.h header to call into those java methods from cpp.

  2. 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).

  3. Call the generated methods using the ClassNameJni class or the JClassName type.

    • Constructors: ScopedJavaLocalRef<JMyClass> obj = MyClassJni::New(env, ...);
    • Static Methods: MyClassJni::staticMethod(env, ...);
    • Instance Methods: obj->instanceMethod(env, ...);

Note: For test-only methods, use @CalledByNativeForTesting which will ensure that it is stripped in our release binaries.

Example:

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);
}

Legacy Syntax

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.

Automatic Type Conversions using @JniType

Normally, JNI Zero maps Java types to C++ types as follows:

Java TypeC++ Type
Stringjstring
Throwablejthrowable
Classjclass
Any other objectjobject
booleanbool
byteint8_t
charuint16_t
shortint16_t
intint32_t
longint64_t
floatfloat
doubledouble
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.

Example Usage

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>&) {
  ...
}

Built-in Conversions

JNI Zero provides built-in conversions for several common C++ and Java types within third_party/jni_zero/default_conversions.h.

C++ TypeJava Type
std::optional<T>@Nullable T
std::vector<T>T[] or List<T>
std::map<K, V>Map<K, V>
boolBoolean (boxed)
int32_tInteger (boxed)
int64_tLong (boxed)
floatFloat (boxed)
doubleDouble (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.

Implementing Conversion Functions

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

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.

Nullability

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.

Exposing Native Methods

There are two ways to have native methods be found by Java:

  1. Explicitly register the name -> function pointer mapping using JNI's RegisterNatives() function.
  2. Export the symbols from the shared library, and let the runtime resolve them on-demand (using 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.

Exposing Java Methods

JNI Zero ships with R8 configs that disable renaming of symbols that use @CalledByNative.

Testing Mockable Natives

/**
 * 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());
    }
}

Special case: APK Splits

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.

How to Know if Native is Loaded?

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.

Chrome-specific Guidance

  • Be careful of logic in early initialization where native has not yet been loaded.
  • For tests, JNI Zero‘s check to ensure that all C++ implementations of JNI methods are included is disabled (due to too many violations). If you hit a 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.

Additional Guidelines / Advice

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.

JNI Benchmarking

Refer to the performance README.

Under the Hood

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.
}

Debug Mode

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();
}

Release Mode

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.

Per-File Natives

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();
}

Legacy Modes

These are modes which JNI provides currently, but we hope to remove. Please do not add any new uses of these.

Using the “native” Keyword

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.

Hashed Names

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();
}

Placeholder .jar Files

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.

Changing JNI Zero

  • Python golden tests live in test/integration_tests.py
  • A working demo app exists as sample:jni_zero_sample_apk and this app is tested in sample:jni_zero_sample_apk_test.
  • Compile-only tests exist in test:jni_zero_compile_check_apk
  • We are a Chromium project developed in the Chromium repo, but we intend to have no dependencies on Chromium, to allow this project to be easily portable.
  • jni_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.