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_generator generates boiler-plate code with the goal of making our code:

  1. easier to write, and
  2. typesafe.

jni_generator uses regular expressions to parse .Java files, so don't do anything too fancy. E.g.:

  • Classes must be either explicitly imported, or are assumed to be in the same package. To use java.lang classes, add an explicit import.
  • Inner classes need to be referenced through the outer class. E.g.: void call(Outer.Inner inner)

The presense of any JNI within a class will result in ProGuard obfuscation for the class to be disabled.

Exposing Native Methods

Without Crazy Linker:

  • Java->Native calls are exported from the shared library and lazily resolved by the runtime (via dlsym()).

With Crazy Linker:

  • Java->Native calls are explicitly registered with JNI on the native side. Explicit registration is necessary because crazy linker provides its own dlsym(), but JNI is hardcoded to use the system's dlsym().
    • The logic to explicitly register stubs is generated by jni_registration_generator.py.
      • This script finds all native methods by scanning all source .java files of an APK. Inefficient, but very convenient.
    • Since dlsym() is not used in this case, we use a linker script to avoid the cost of exporting symbols from the shared library (refer to //build/config/android:hide_all_but_jni_onload).
  • jni_registration_generator.py exposes two registrations methods:
    • RegisterNonMainDexNatives - Registers native functions needed by multiple process types (e.g. Rendereres, GPU process).
    • RegisterMainDexNatives - Registers native functions needed only by the browser process.

Exposing Java Methods

Java methods just need to be annotated with @CalledByNative. The generated functions can be put into a namespace using @JNINamespace("your_namespace").

Usage

Because the generator does not generate any source files, generated headers must not be #included by multiple sources. If there are Java functions that need to be called by multiple sources, one source should be chosen to expose the functions to the others via additional wrapper functions.

Calling Java -> Native

  • Declare methods using a nested interface annotated with @NativeMethods.
  • The JNI annotation processor generates a class named ${OriginalClassName}Jni with a get() method that returns an implementation of the annotated interface. The C++ function that it routes to is the same as if it would be in the legacy method.
  • For each JNI method:
    • C++ stubs are generated that forward to C++ functions that you must write.
    • If the first parameter is a C++ object (e.g. long mNativePointer), then the bindings will generate the appropriate cast and call into C++ code.

To add JNI to a class:

  1. Enable the JNI processor by adding to your android_library target:
    annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
    deps = [ "//base:jni_java" ]
    
  2. Create a nested-interface annotated with @NativeMethods that contains the declaration of the corresponding static methods you wish to have implemented.
  3. Call native functions using ${OriginalClassName}Jni.get().${method}
  4. 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.) Only include this header from a single .cc file as the header defines functions. That .cc must implement your native code by defining functions named JNI_${OriginalClassName}_${UpperCamelCaseMethod}

Example:

class MyClass {
  // Cannot be private. Must be package or public.
  @NativeMethods
  /* package */ interface Natives {
    void foo();
    double bar(int a, int b);
    // Either the |ClassName| part of the |nativeClassName| parameter name must
    // match the native class name exactly, or the method annotation
    // @NativeClassQualifiedName("ClassName") must be used.
    //
    // If the native class is nested, use
    // @NativeClassQualifiedName("FooClassName::BarClassName") and call the
    // parameter |nativePointer|.
    void nonStatic(long nativeClassName, NewStyle self);
  }

  void callNatives() {
    // NewStyleJni is generated by the JNI annotation processor.
    // Storing NewStyleJni.get() in a field defeats some of the desired R8
    // optimizations, but local variables are fine.
    Natives jni = NewStyleJni.get();
    jni.foo();
    jni.bar(1,2);
    jni.nonStatic(this, mNativePointer);
  }
}

Using the ‘native’ keyword

  • The binding generator also looks for native JNI method declarations and generates stubs for them. This used to be the norm, but is now obsolete.
  • If you have native methods that you don't want stubs generated for, you should add @JniIgnoreNatives to the class.

Testing Mockable Natives

  1. Add the JniMocker rule to your test.
  2. Call JniMocker#mock in a setUp() method for each interface you want to stub out.

Note: Mocking native methods doesn't work in tests that are part of APKs that use an apk_under_test. This crbug tracks removing the apk_under_test variable.

JniMocker will reset the stubs during tearDown().

/**
 * Tests for {@link AnimationFrameTimeHistogram}
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class AnimationFrameTimeHistogramTest {
    @Rule
    public JniMocker mocker = new JniMocker();

    @Mock
    AnimationFrameTimeHistogram.Natives mNativeMock;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mocker.mock(AnimationFrameTimeHistogramJni.TEST_HOOKS, mNativeMock);
    }

    @Test
    public void testNatives() {
        AnimationFrameTimeHistogram hist = new AnimationFrameTimeHistogram("histName");
        hist.startRecording();
        hist.endRecording();
        verify(mNativeMock).saveHistogram(eq("histName"), any(long[].class), anyInt());
    }
}

If a native method is called without setting a mock in a unit test, an UnsupportedOperationException will be thrown.

Calling Native -> Java

  • Methods annotated with @CalledByNative will have stubs generated for them.
    • Inner class methods must provide the inner class name explicitly (ex. @CalledByNative("InnerClassName"))
  • Just call the generated stubs defined in generated .h files.

Java Objects and Garbage Collection

All pointers to Java objects must be registered with JNI in order to prevent garbage collection from invalidating them.

For Strings & Arrays - it's common practice to use the //base/android/jni_* helpers to convert them to std::vectors and std::strings as soon as possible.

For other objects - use smart pointers to store them:

  • ScopedJavaLocalRef<> - When lifetime is the current function's scope.
  • ScopedJavaGlobalRef<> - When lifetime is longer than the current function's scope.
  • JavaObjectWeakGlobalRef<> - Weak reference (do not prevent garbage collection).
  • JavaParamRef<> - Use to accept any of the above as a parameter to a function without creating a redundant registration.

Native Java Unittests and @CalledByNativeJavaTest

Native Java Unittests are Java unit tests that run on Android (not on the host machine). Unlike junit and robolectric, these tests can use the native library, and real Android APIs. Unlike unit tests in ChromePublicTestApk and similar, the Activity is not restarted between tests, so the tests run much faster (and you must be careful not to leave state behind). Example tests may be found in chrome/android/native_java_unittests/.

The @CalledByNativeJavaTest annotation causes the JNI generator to generate C++ test methods that simply call out to the Java test methods.

For Example:

class FooTest {
  @CalledByNative public FooTest() {}
  @CalledByNativeJavaTest public void testFoo() { ... }
  @CalledByNativeJavaTest public void testOtherFoo() { ... }
}
class FooTest : public ::testing::Test {
  FooTest() : j_test_(Java_FooTest_Constructor(AttachCurrentThread())) {}
  const ScopedJavaGlobalRef<jobject>& j_test() { return j_test_; }
 private:
  ScopedJavaGlobalRef<jobject> j_test_;
}
JAVA_TESTS(AndroidPaymentAppFinderUnitTest, j_test())

In some cases you may want to run custom C++ code before running the Java test, in which case, use CalledByNative instead of CalledByNativeJavaTest and call the test method yourself. eg. Java_FooTest_testFancyFoo(env, j_test());

Disabling @CalledByNativeJavaTest tests

In order to disabled a Native Java Unittest, replace the @CalledByNativeJavaTest annotation with @DisabledCalledByNativeJavaTest. This will generate a native test prefixed with DISABLED_.

Please note that unlike @DisabledTest, you should not provide a message in the annotation as to why the test is disabled, as this will be interpreted as the inner class name by the CalledByNative jni generator.

Feature flag support in @CalledByNativeJavaTest tests

Features may be enabled/disabled for the Java test methods by using the @NativeJavaTestFeatures.Enabled or @NativeJavaTestFeatures.Disabled annotations.

Note that these annotations will re-initialize the feature list given they initialize through the command line approach, and so will clear any features set by, say, test setup.

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.

The best way to pass “compound” types across in either direction is to create an inner class with PODs and a factory function. If possible, make mark all the fields as “final”.

Build Rules

  • generate_jni - Generates a header file with stubs for given .java files
  • generate_jar_jni - Generates a header file with stubs for a given .jar file
  • generate_jni_registration - Generates a header file with functions to register native-side JNI methods (required only when using crazy linker).

Refer to //build/config/android/rules.gni for more about the GN templates.

Changing jni_generator

  • Python unit tests live in jni_generator_tests.py
  • A working demo app exists as //base/android/jni_generator:sample_jni_apk