JNI Basics and Usage in Chromium

JNI Documents

Java Native Interface https://docs.oracle.com/javase/8/docs/technotes/guides/jni/

JNI tips https://developer.android.com/training/articles/perf-jni

Build Shared Library

C code can be built into shared library, .SO file in Linux, .DLL file in Windows.

Export Symbols

Shared library binary file needs to define what symbols to be exported. Only exported symbols can be accessed from outside.

In Linux, we can use version script file to define exported C++ symbols for so file. And use this as arguments for ld to build the library.

https://www.gnu.org/software/gnulib/manual/html_node/Exported-Symbols-of-Shared-Libraries.html

https://www.gnu.org/software/gnulib/manual/html_node/LD-Version-Scripts.html

1
2
3
4
5
6
7
8
# version script file: login_so.lst
{
global:
# exports some method
SomeStaticMethod;
local:
*;
};
1
2
3
4
5
6
7
8
// C++ code
#define EXPORTS __attribute__((visibility("default")))

// use extern C to prevent name mangling
// https://stackoverflow.com/questions/2587613/what-is-the-effect-of-declaring-extern-c-in-the-header-to-a-c-shared-libra
extern "C" {
EXPORTS void SomeStaticMethod();
}

View Exported Symbols of SO File

We can use nm command to view exported symbols of so file.

1
2
3
4
5
6
7
8
# view exported symbols for so.
# U means the symbol should be loaded from outside.
# T means the symbol is in the so and can be accessed from outside.
> nm -CD somelib.so
U abort
U __android_log_write
00318a40 T Java_com_demo_login_native_onFailure
00318821 T Java_com_demo_login_native_onSuccess

Export Sysmbols for JNI

If a so file has JNI calls, JNI related symbols must be exported. The version script file will be like the following format.

1
2
3
4
5
6
7
8
9
10
# login_so.lst
{
global:
# export jni related functions
JNI_OnLoad;
JNI_OnUnload;
Java_*;
local:
*;
};

jni.h

jni.h is a C header file. It provides JNI related API for native code. For Android JNI, it is defined in NDK.

Java Load Shared Library

In Java code, call System.loadLibrary(libname) or System.load(filename) to load shared library. After that, the Java and C code can call each other.

When the JVM load a shared library named Login, it will find if it have a exported symbol JNI_OnLoad_Login or JNI_OnLoad and then call it. If both JNI_OnLoad_Login and JNI_OnLoad are defined, the JNI_OnLoad will be ignored. JNI_OnUnload is just the same but called when unload the library.

  • JNI_OnLoad: The VM calls JNI_OnLoad when the native library is loaded (for example, through System.loadLibrary).
  • JNI_OnUnload: The VM calls JNI_OnUnload when the class loader containing the native library is garbage collected.
1
2
3
4
5
6
7
8
// Main.java

public class Main {
static {
// Will load libLogin.so in Linux system, Login.dll in Win32 system
System.loadLibrary("Login");
}
}
1
2
3
4
5
6
7
8
// jni.h

/*
* Prototypes for functions exported by loadable shared libs. These are
* called by JNI, not provided by JNI.
*/
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);

Reference:

https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#compiling_loading_and_linking_native_methods

JavaVM

JavaVM represent the JVM. It is defined in jni.h.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// jni.h

#if defined(__cplusplus)
typedef _JavaVM JavaVM;
#else
typedef const struct JNIInvokeInterface* JavaVM;
#endif

/*
* JNI invocation interface.
*/
struct JNIInvokeInterface {
void* reserved0;
void* reserved1;
void* reserved2;

jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

/*
* C++ version.
*/
struct _JavaVM {
const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

Save JavaVM for Funture Usage

We can save JavaVM as a global instance for future usage when JNI_OnLoad called.

1
2
3
4
5
6
7
8
9
10
11
12
namespace {
JavaVM *g_jvm = nullptr;
}

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_jvm = vm;
return JNI_VERSION_1_4;
}

JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) {
g_jvm = nullptr;
}

JNIEnv

When we need to call Java code from native, we need to use JNIEnv.

JNIEnv is a pointer to a structure storing all JNI function pointers. These pointers point to the Java function. Each Java thread has a JNIEnv instance.

Interface pointer

Reference:

https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#jni_interface_functions_and_pointers

JNI Data Types and Signatures

When using JNI, most of the data type conversion job is done in the native side. jni.h file has provide many definition of Java basic data types and conversion functions.

Java Types in Native Code

  • Primitive types in Java has corresponding C types defined in jni.h. E,g. boolean and jboolean, char and jchar.
  • Reference types also have corresponding C types. E.g. String and jstring, Class and jclass, Object[] and jobjectarray.
  • Other Java types in C is defined as jobject.

Field and Method IDs

  • jfieldID represents a field of Java.
  • jmethodID represents a method of Java.

Signatures of Java Type and Method

When we need to refer a Java type or method with string, we need to use its signature.

The JNI uses the Java VM’s representation of type signatures.

  • B - byte
  • C - char
  • D - double
  • F - float
  • I - int
  • J - long
  • S - short
  • V - void
  • Z - boolean
  • [ - array of the thing following the bracket
  • L [class name] ; - instance of this class, with dots becoming slashes
  • ( [args] ) [return type] - method signature

For example:

  • signature of java.lang.String is Ljava/lang/String;
  • signature of int[][] is [[I
  • signature of method int foo(String bar, long[][] baz) is (Ljava/lang/String;[[J)I

https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html

https://stackoverflow.com/questions/9909228/what-does-v-mean-in-a-class-signature

Java Call Native

Define Native Method in Java

When Java call native, we should define native method in Java first. native method can be static or non-static.

1
2
3
4
5
6
7
8
package com.mypkg;

public class MyClass {
public native void nonStaticMethod();
public static native void staticMethod();
public static native String overloadedMethod();
public static native String overloadedMethod(MyClass args);
}

Resolving Native Method Names

When we call the native method in Java, JVM will call the corresponding C native method (exported symbol) in shared library.

Dynamic linkers resolve entries based on their names. A native method name is concatenated from the following components:

  • the prefix Java_
  • a mangled fully-qualified class name
  • an underscore (“_”) separator
  • a mangled method name
  • for overloaded native methods, two underscores (“__”) followed by the mangled argument signature

We can also call JNIEnv.RegisterNatives() to dynamically register native methods and override the default resolve rules. If we have registered a native method, it does not need to be exported.

Reference:

https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#registering_native_methods
https://stackoverflow.com/questions/1010645/what-does-the-registernatives-method-do

Native Method Arguments

Arguments of native method:

  • First argument is JNIEnv.
  • Second argument is this object for non-static native method or class object for static native method.
  • Remaining arguments correspond to Java method arguments.
  • Return value correspond to Java method return value.

Use javah to Generate C Header File

We can use javah to generate the C header file.

A simple example, we have file com/mypkg/MyClass.java as following:

1
2
3
4
5
6
7
8
package com.mypkg;

public class MyClass {
public native void nonStaticMethod();
public static native void staticMethod();
public static native String overloadedMethod();
public static native String overloadedMethod(MyClass args);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# run javac to compile MyClass.java into MyClass.class
$ javac com/mypkg/MyClass.java

# run javah to generate header file
$ javah -jni -classpath . com.mypkg.MyClass

# show files in the directory
$ tree
.
├── com
│ └── mypkg
│ ├── MyClass.class
│ └── MyClass.java
└── com_mypkg_MyClass.h

2 directories, 3 files

Finally we got the following header file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_mypkg_MyClass */

#ifndef _Included_com_mypkg_MyClass
#define _Included_com_mypkg_MyClass
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_mypkg_MyClass
* Method: nonStaticMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_mypkg_MyClass_nonStaticMethod
(JNIEnv *, jobject);

/*
* Class: com_mypkg_MyClass
* Method: staticMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_mypkg_MyClass_staticMethod
(JNIEnv *, jclass);

/*
* Class: com_mypkg_MyClass
* Method: overloadedMethod
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_mypkg_MyClass_overloadedMethod__
(JNIEnv *, jclass);

/*
* Class: com_mypkg_MyClass
* Method: overloadedMethod
* Signature: (Lcom/mypkg/MyClass;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_mypkg_MyClass_overloadedMethod__Lcom_mypkg_MyClass_2
(JNIEnv *, jclass, jobject);

#ifdef __cplusplus
}
#endif
#endif

Native Call Java

We need to use JNIEnv to call Java from native. It is like using reflection. So, don’t forget to config ProGuard to avoid code obfuscation of the Java class.

Here is a simple demo of calling android log method from C++.

1
2
3
4
5
6
package android.util;
public class Log {
public static int i(String tag, String message) {
// ...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <jni.h>

// print log by calling static Java method of android.util.Log.i(String tag, String message)
int log(JNIEnv* env, const char* message) {
// Find the Java class with full class name.
jclass cls = env->FindClass("android/util/Log");
// Get the static method `i` with its name and signature.
jmethodID method = env->GetStaticMethodID(cls, "i", "(Ljava/lang/String;Ljava/lang/String;)I");
// Convert C++ string of `char*` into `jstring` as arguments.
jstring tag = env->NewStringUTF("Login");
jstring msg = env->NewStringUTF(message);
// Call static Java method with arguments and get `jint` result returned by Java.
jint result = env->CallStaticIntMethod(cls, method, tag, msg);
// Covert the result from Java type to C++ type and return the result.
return static_cast<int>(result);
}

Get JNIEnv in Native Code

When we call Java code from native, JNIEnv is always needed. This arguments is passed to the native method when Java call it. But sometimes we may not have the JNIEnv because we have not pass it from somewhere else. What can we do?

Here is a simple solution:

  • When JNI_OnLoad is called, save the JavaVM reference, and clear it when JNI_OnUnload is called.
  • When JNIEnv is needed, call JavaVM.AttachCurrentThread() to get it.

The code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <sys/prctl.h>
#include <jni.h>

// declare an anonymous namespace to avoid naming conflict
namespace {
JavaVM *g_jvm = nullptr;
}

extern "C" {
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_jvm = vm;
return JNI_VERSION_1_4;
}

JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) {
g_jvm = nullptr;
}
}

namespace MyJNI {

JNIEnv* AttachCurrentThread() {
if (!g_jvm) {
return nullptr;
}
JNIEnv* env = nullptr;
jint ret = g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_4);
if (ret == JNI_EDETACHED || !env) {
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_4;
args.group = nullptr;

// 16 is the maximum size for thread names on Android.
char thread_name[16];
int err = prctl(PR_GET_NAME, thread_name);
if (err < 0) {
args.name = nullptr;
} else {
args.name = thread_name;
}

ret = g_jvm->AttachCurrentThread(&env, &args);
}
return env;
}

} // namespace MyJNI

Call Java from Native Thread

In most case, Java code call native and then native call Java, the call is running in Java thread. And we can get a JNIEnv corresponded to this thread and made calls correctly.

But sometimes we may running native code in a native thread instead of Java thread and see a ClassNotFoundException when calling JNIEnv -> FindClass() but the class exists.

This is because the JNIEnv attached to the native thread will use the “system” class loader instead of the one associated with your application to find the class, so attempts to find app-specific classes will fail.

Here is a simple solution. We can save the class loader when JNI_OnLoad called, this must be running in a Java thread. Then we can use this class loader to find class from any thread.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
namespace {
JavaVM *g_jvm = nullptr;
jobject g_class_loader = nullptr;
jmethodID g_find_class_method = nullptr;
// this should be a class in application
constexpr char kJavaClass[] = "org/chromium/chrome/Xxx";
}

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_jvm = vm;
JNIEnv* env = oneauth_android_jni::AttachCurrentThread();
jclass java_class = env->FindClass(kJavaClass);
jclass class_class = env->GetObjectClass(java_class);
jclass class_loader_class = env->FindClass("java/lang/ClassLoader");
jmethodID get_class_loader_method = env->GetMethodID(class_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
// java object should use NewGlobalRef
g_class_loader = env->NewGlobalRef(env->CallObjectMethod(java_class, get_class_loader_method));
g_find_class_method = env->GetMethodID(class_loader_class, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;");
return JNI_VERSION_1_4;
}

JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) {
g_jvm = nullptr;
g_class_loader = nullptr;
g_find_class_method = nullptr;
}

// this method can be called from cpp native thread and find the right class
jclass FindClassInAnyThread(JNIEnv* env, const char* name) {
return static_cast<jclass>(env->CallObjectMethod(
g_class_loader, g_find_class_method, env->NewStringUTF(name)));
}

Reference:

  1. https://docs.oracle.com/javase/7/docs/technotes/guides/jni/jni-12.html#FindClass

When FindClass is called through the Invocation Interface, there is no current native method or its associated class loader. In that case, the result of ClassLoader.getBaseClassLoader is used. This is the class loader the virtual machine creates for applications, and is able to locate classes listed in the java.class.path property.

  1. https://stackoverflow.com/questions/13263340/findclass-from-any-thread-in-android-jni

  2. https://developer.android.com/training/articles/perf-jni#faq:-why-didnt-findclass-find-my-class

When we need to print log in native code on Android platform, printf is not working by default. Here is what we can do:

1、Call Android Java Log API.

1
2
3
4
5
6
7
8
void LogInfo(JNIEnv* env, const char* tag, const char* message) {
jclass cls = env->FindClass("android/util/Log");
jmethodID method = env->GetStaticMethodID(
cls, "i", "(Ljava/lang/String;Ljava/lang/String;)I");
env->CallStaticIntMethod(cls, method,
env->NewStringUTF(tag),
env->NewStringUTF(message));
}

2、Call native API of log library directly.

1
2
3
4
5
6
#include <android/log.h>

#define LOG_TAG "Native"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

LOGD("1 + 2 = %d", 3);

To call this, we need to configure log library. If we are using Gradle, just add this:

1
2
3
4
5
6
7
android {
defaultConfig {
ndk {
ldLibs "log"
}
}
}

Handle Exceptions

Conclusion:

  • Java can not catch native code error.
  • Native code can raise Java exceptions, and let Java code to handle them.
  • Native code can get Java exceptions, and have several ways to handle them.

Java Call Native and Native Crashed

Java call native, and error occurred in native: native code will stop immediately, Java can not catch the exception. And we can not get any stack trace.

1
Fatal signal 8 (SIGFPE), code 1 (FPE_INTDIV), fault addr 0xc3aa9f51 in tid 9992 (com.demo.jnidemo), pid 9992 (com.demo.jnidemo)

Produce a Pending Exception

Native call Java, and error occurred in Java: Java code will stop immediately, the JNIEnv will store a pending exception, but the native code will continue running.

1
2
3
4
5
JNIEnv *env;
jobject thisObj;
jclass cls = env->FindClass("xxx");
jmethodID method = env->GetMethodID(cls, "exceptionMethod", "()V");
env->CallVoidMethod(thisObj, method);

Native code can throw a Java exception: the JNIEnv will store a pending exception, native code will continue running.

1
2
3
4
JNIEnv *env;
jclass cls = env->FindClass("java/lang/RuntimeException");
env->ThrowNew(cls, "error thrown in native code");
// will continue running

Handle Pending Exception

If a JNIEnv already stored a pending exception:

1、Native code try to call Java or throw Java error through the JNIEnv: native code will stop immediately, and then JVM will report the pending exception.

1
2
3
JNI DETECTED ERROR IN APPLICATION: JNI FindClass called with pending exception java.lang.RuntimeException: java runtime exception
at void com.demo.jnidemo.MainActivity.exceptionMethod() (MainActivity.java:60)
...

2、Native code finished running, and the control goes back to Java: the error will be thrown in Java code. Java can catch the error with try-catch.

3、Native code can read or clear the pending exception. So, to avoid pending exception causing the process crashed, we need to check the pending exception every time after we call Java. When we found an error in native code, we have several ways to handle it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
JNIEnv env;

// 1. just return and let Java code to handle the exception
CallExceptionJavaMethod(env);
if (HasException(env)) {
return;
}

// 2. handle exception in native code
if (HasException(env)) {
ClearException();
/* code to handle exception */
}

// 3. handle exception in native code and throw a new exception
if (HasException(env)) {
env->ExceptionClear();
/* code to handle exception */
env->ThrowNew(jcls, "error message");
return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool HasException(JNIEnv* env) {
return env->ExceptionCheck() != JNI_FALSE;
}

bool ClearException(JNIEnv* env) {
if (!HasException(env))
return false;
env->ExceptionDescribe();
env->ExceptionClear();
return true;
}

void CheckAndDescribeException(JNIEnv* env) {
if (!HasException(env))
return;

jthrowable java_throwable = env->ExceptionOccurred();
if (java_throwable) {
env->ExceptionDescribe();
env->ExceptionClear();
}
}

Reference:

https://github.com/jzj1993/AndroidJniDemo/blob/master/app/src/main/cpp/native-lib.cpp

https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#java_exceptions

https://www.developer.com/java/data/exception-handling-in-jni.html

https://www.jianshu.com/p/b6129f110e86

https://blog.csdn.net/xyang81/article/details/45770551

JNI in Chromium

Build Shared Library for Android JNI with GN

In GN, we can use loadable_module rule to build shared library for Android JNI, and use ldflags parameter to specify the version script file.

1
2
3
4
5
6
7
8
9
10
11
12
13
# BUILD.gn
loadable_module(...) {
# add needed libs
libs = [
"android",
"log",
]
# flags for ld
ldflags = [
# specify version script files
"-Wl,--version-script=" + rebase_path("login_so.lst", root_build_dir),
]
}

Android JNI Utils

Chromium provides some Android JNI related utils. For example, base/android/jni_android.cc provides some useful functions for JNI.

1
2
3
4
5
6
JNIEnv* AttachCurrentThread();

bool HasException(JNIEnv* env);
void CheckException(JNIEnv* env);
bool ClearException(JNIEnv* env);
std::string GetJavaExceptionInfo(JNIEnv* env, jthrowable java_throwable);

JNI Generator

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

  1. easier to write, and
  2. typesafe.

jni_generator use AnnotationProcessor to generate Java side JNI binding class, and the source code is here:

base/android/jni_generator/java/src/org/chromium/jni_generator/JniProcessor.java.

and use python script to generate native side JNI binding, and the source code is here:

base/android/jni_generator/jni_registration_generator.py.

See more details in the code repo:

https://chromium.googlesource.com/chromium/src/base/+/master/android/jni_generator/

Write JNI Code

With the help of jni_generator, we can write JNI related code more convenient. Here is a simple example:

  • We have a JavaClass and a related native_namespace::CppClass.
  • jni_generator will generate a JavaClassJni.java and a JavaClass_jni.h from Java code.
  • When we construct Java (native) instance, a related native (Java) instance will be created at the same time.
  • Java and native code can call each other’s member methods.

File structure is as follows:

1
2
3
4
components/jni_test/android/BUILD.gn
components/jni_test/android/cpp_class.cc
components/jni_test/android/cpp_class.h
components/jni_test/android/java/src/com/demo/jni/JavaClass.java

Java File

JavaClass.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.demo.jni;

import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;

// We can use the annotation to specify the related native namespace
@JNINamespace("native_namespace")
public class JavaClass {

class SomeClass {
}

// native side instance pointer
private final long mNativeCppClass;

// create instaces from Java side
public JavaClass() {
// create native side instance
mNativeCppClass = JavaClassJni.get().init();
}

// create instances from native side
// Use CalledByNative annotation to indicate a method / constructor can be called by native
// Proguard will be keep this method name
@CalledByNative
public JavaClass(long nativeCppClass) {
mNativeCppClass = nativeCppClass;
}

int callNativeMethod() {
// call native member method
SomeClass param = new SomeClass();
int result = JavaClassJni.get().cppClassMember(mNativeCppClass, param);
return result;
}

@CalledByNative
int javaClassMember(SomeClass param) {
return 0;
}

@NativeMethods
interface Natives {
// By default the method will related to a static native function.
// Normally we can construct a related native instance in this method.
// The return value is a pointer to the native instace.
long init();

// When we need to define a member function of the native class, we can use this style.
// The first arguments should the native instance pointer with long type,
// and its name should be "native<CppClassName>"
int cppClassMember(long nativeCppClass, SomeClass param);
}
}

CPP File

cpp_class.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef COMPONENTS_JNI_TEST_ANDROID_CPP_CLASS_H_
#define COMPONENTS_JNI_TEST_ANDROID_CPP_CLASS_H_

#include <jni.h>
#include "base/android/scoped_java_ref.h"

namespace native_namespace {

class CppClass {
public:
CppClass();
~CppClass();
int CppClassMember(JNIEnv* env, const base::android::JavaParamRef<jobject>& param);

private:
// Java side instance
base::android::ScopedJavaGlobalRef<jobject> java_instance_;
};
} // namespace native_namespace

#endif // COMPONENTS_JNI_TEST_ANDROID_CPP_CLASS_H_

cpp_class.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// include header file
#include "components/jni_test/android/cpp_class.h"

// include jni header file generated from Java code
// the path is related to the target name of `generate_jni` (defined in the BUILD.gn file)
#include "components/jni_test/android/jni_headers/JavaClass_jni.h"

namespace native_namespace {

// Java call this static function to create native instance
static jlong JNI_JavaClass_Init(JNIEnv* env) {
CppClass* cpp_class = new CppClass();
return reinterpret_cast<intptr_t>(cpp_class);
}

CppClass::CppClass() {
// create Java side instance
JNIEnv* env = base::android::AttachCurrentThread();
java_instance_ = Java_JavaClass_Constructor(env, reinterpret_cast<intptr_t>(this));
}

int CppClass::CppClassMember(
JNIEnv* env, const base::android::JavaParamRef<jobject>& param) {
// call Java member method
jint result = native_namespace::Java_JavaClass_javaClassMember(env, java_instance_, param);
return static_cast<int>(result);
}

} // namespace native_namespace

GN File

BUILD.gn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import("//build/config/android/rules.gni")

# build an android java library
android_library("java") {
sources = [
"java/src/com/demo/jni/JavaClass.java",
]
deps = [
"//base:base_java",
"//base:jni_java",
]
# configure GN to call annotation processor to generate java file: JavaClassJni.java
annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
}

# configure GN to call python script to generate native header file: JavaClass_jni.h
generate_jni("jni_headers") {
sources = [
"java/src/com/demo/jni/JavaClass.java",
]
}

# build a static library with native code
static_library("cpp") {
sources = [
"cpp_class.h",
"cpp_class.cc",
]
deps = [
"//base",
# depends on the generated native header file
":jni_headers",
]
}

# this group combine multiple target into one
group("all") {
deps = [
":java",
":jni_headers",
":cpp",
]
}

BUILD.gn in root directory:

All the GN targets should be dependencies of gn_all, or GN will not resolve the BUILD.gn file and report errors like this ninja: error: unknown target 'components/jni_test/android:group'. So, to run our test, add this into deps of gn_all.

1
2
3
4
5
6
group("gn_all") {
deps = [
"//components/jni_test/android:all",
# ...
]
}

Build The Code

We can run the following command to build the code, I have already tested it.

1
autoninja -C out/debug_arm_android components/jni_test/android:all

Generated Java File (Java Side JNI Binding)

out/debug_arm_android/gen/components/jni_test/android/java/generated_java/input_srcjars/com/demo/jni/JavaClassJni.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.demo.jni;

import java.lang.Override;
import javax.annotation.Generated;
import org.chromium.base.JniStaticTestMocker;
import org.chromium.base.NativeLibraryLoadedStatus;
import org.chromium.base.annotations.CheckDiscard;
import org.chromium.base.natives.GEN_JNI;

@Generated("org.chromium.jni_generator.JniProcessor")
@CheckDiscard("crbug.com/993421")
final class JavaClassJni implements JavaClass.Natives {
private static JavaClass.Natives testInstance;

public static final JniStaticTestMocker<JavaClass.Natives> TEST_HOOKS = new org.chromium.base.JniStaticTestMocker<com.demo.jni.JavaClass.Natives>() {
@java.lang.Override
public void setInstanceForTesting(com.demo.jni.JavaClass.Natives instance) {
if (!org.chromium.base.natives.GEN_JNI.TESTING_ENABLED) {
throw new RuntimeException("Tried to set a JNI mock when mocks aren't enabled!");
}
testInstance = instance;
}
};

@Override
public long init() {
return (long)GEN_JNI.com_demo_jni_JavaClass_init();
}

@Override
public int cppClassMember(long nativeCppClass, JavaClass.SomeClass param) {
return (int)GEN_JNI.com_demo_jni_JavaClass_cppClassMember(nativeCppClass, param);
}

public static JavaClass.Natives get() {
if (GEN_JNI.TESTING_ENABLED) {
if (testInstance != null) {
return testInstance;
}
if (GEN_JNI.REQUIRE_MOCK) {
throw new UnsupportedOperationException("No mock found for the native implementation for com.demo.jni.JavaClass.Natives. The current configuration requires all native implementations to have a mock instance.");
}
}
NativeLibraryLoadedStatus.checkLoaded(false);
return new JavaClassJni();
}
}

Generated CPP Header File (Native Side JNI Binding)

out/debug_arm_android/gen/components/jni_test/android/jni_headers/JavaClass_jni.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.


// This file is autogenerated by
// base/android/jni_generator/jni_generator.py
// For
// com/demo/jni/JavaClass

#ifndef com_demo_jni_JavaClass_JNI
#define com_demo_jni_JavaClass_JNI

#include <jni.h>

#include "../../../../../../../base/android/jni_generator/jni_generator_helper.h"


// Step 1: Forward declarations.

JNI_REGISTRATION_EXPORT extern const char kClassPath_com_demo_jni_JavaClass[];
const char kClassPath_com_demo_jni_JavaClass[] = "com/demo/jni/JavaClass";
// Leaking this jclass as we cannot use LazyInstance from some threads.
JNI_REGISTRATION_EXPORT std::atomic<jclass> g_com_demo_jni_JavaClass_clazz(nullptr);
#ifndef com_demo_jni_JavaClass_clazz_defined
#define com_demo_jni_JavaClass_clazz_defined
inline jclass com_demo_jni_JavaClass_clazz(JNIEnv* env) {
return base::android::LazyGetClass(env, kClassPath_com_demo_jni_JavaClass,
&g_com_demo_jni_JavaClass_clazz);
}
#endif


// Step 2: Constants (optional).


// Step 3: Method stubs.
namespace native_namespace {

static jlong JNI_JavaClass_Init(JNIEnv* env);

JNI_GENERATOR_EXPORT jlong Java_org_chromium_base_natives_GEN_1JNI_com_1demo_1jni_1JavaClass_1init(
JNIEnv* env,
jclass jcaller) {
return JNI_JavaClass_Init(env);
}

JNI_GENERATOR_EXPORT jint
Java_org_chromium_base_natives_GEN_1JNI_com_1demo_1jni_1JavaClass_1cppClassMember(
JNIEnv* env,
jclass jcaller,
jlong nativeCppClass,
jobject param) {
CppClass* native = reinterpret_cast<CppClass*>(nativeCppClass);
CHECK_NATIVE_PTR(env, jcaller, native, "CppClassMember", 0);
return native->CppClassMember(env, base::android::JavaParamRef<jobject>(env, param));
}


static std::atomic<jmethodID> g_com_demo_jni_JavaClass_Constructor(nullptr);
static base::android::ScopedJavaLocalRef<jobject> Java_JavaClass_Constructor(JNIEnv* env, jlong
nativeCppClass) {
jclass clazz = com_demo_jni_JavaClass_clazz(env);
CHECK_CLAZZ(env, clazz,
com_demo_jni_JavaClass_clazz(env), NULL);

jni_generator::JniJavaCallContextChecked call_context;
call_context.Init<
base::android::MethodID::TYPE_INSTANCE>(
env,
clazz,
"<init>",
"(J)V",
&g_com_demo_jni_JavaClass_Constructor);

jobject ret =
env->NewObject(clazz,
call_context.base.method_id, nativeCppClass);
return base::android::ScopedJavaLocalRef<jobject>(env, ret);
}

static std::atomic<jmethodID> g_com_demo_jni_JavaClass_javaClassMember(nullptr);
static jint Java_JavaClass_javaClassMember(JNIEnv* env, const base::android::JavaRef<jobject>& obj,
const base::android::JavaRef<jobject>& param) {
jclass clazz = com_demo_jni_JavaClass_clazz(env);
CHECK_CLAZZ(env, obj.obj(),
com_demo_jni_JavaClass_clazz(env), 0);

jni_generator::JniJavaCallContextChecked call_context;
call_context.Init<
base::android::MethodID::TYPE_INSTANCE>(
env,
clazz,
"javaClassMember",
"(Lcom/demo/jni/JavaClass$SomeClass;)I",
&g_com_demo_jni_JavaClass_javaClassMember);

jint ret =
env->CallIntMethod(obj.obj(),
call_context.base.method_id, param.obj());
return ret;
}

} // namespace native_namespace

#endif // com_demo_jni_JavaClass_JNI

Traditional JNI Code Style Is Deprecated

It is deprecated to use traditional code style ( native method ) in Java. We should use the new style Chromium suggested. The new code style is more user-friendly, and it has more additional functions.

If we use the native method, Chromium still can generate JNI binding for the native code, but it can not support all the JNI code grammar and sometime it may report some errors:

1
2
Inner class (%s) can not be imported and used by JNI (%s). Please import the outer class and use Outer.Inner instead.
Inner class (%s) can not be used directly by JNI. Please import the outer class, probably: import %s.%s;

Some style of Java code is not supported, for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// use full class name as native method params or return type without import it,
// will recogonize it as an inner class com.xxx.java.util.UUID.
// SyntaxError: Inner class (java.util.UUID) can not be used directly by JNI.
// Please import the outer class, probably:
// import com.xxx.java.util.UUID
package com.xxx;
class XXX {
native void someMethod(java.util.UUID uuid);
}

// use same name as the java.lang package,
// this type will be recognized as java.lang.InternalError.
// Ambiguous class (%s) can not be used directly by JNI.
// Please import it, probably:
// import java.lang.InternalError;
class XXX {
class InternalError {
}
native void someMethod(InternalError error);
}

If the code needs JNI binding generation, you can rewrite the code to make it works. If the code does not need JNI binding generation, you can exclude them from JNI sources.

1
2
3
4
5
6
7
8
9
10
11
12
# chrome/android/BUILD.gn

chrome_jni_sources_exclusions = []
chrome_jni_sources_exclusions += [
"//third_party/android_sdks/login/com/main/Main.java",
]

template("chrome_public_apk_or_module_tmpl") {
chrome_public_common_apk_or_module_tmpl(target_name) {
jni_sources_exclusions = chrome_jni_sources_exclusions
}
}

Smart Pointer For JNI

We should use smart pointer provided by Chromium to hold Java variables in native code.

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

More Samples

We can see more JNI samples here: https://chromium.googlesource.com/chromium/src/base/+/master/android/jni_generator/java/src/org/chromium/example/jni_generator/SampleForTests.java

Crazy Linker

Crazy Linker is a custom dynamic linker for Android programs that adds a few interesting features compared to /system/bin/linker. Read the docs for more details.

https://chromium.googlesource.com/chromium/src.git/+/master/third_party/android_crazy_linker/src/README.TXT

Native Initialization Tips

Normally, Chromium will initialize native side in ChromeTabbedActivity.java. But in some cases (e.g. test), we may need to call native code before this Activity started.

We can call LibraryLoader.getInstance().ensureInitialized() to load native libraries.

Another tips is that we can call ChromeBrowserInitializer to initialize the native environment. This will not only load native libraries but also initialize the necessary basic components in native code.

1
2
3
4
5
6
7
8
9
10
final BrowserParts parts = new EmptyBrowserParts() {
@Override
public void finishNativeInitialization() {
// this method will be called when native initialized.
// if native is already initialized, this method will be called immediately.
}
};

ChromeBrowserInitializer.getInstance().handlePreNativeStartup(parts);
ChromeBrowserInitializer.getInstance().handlePostNativeStartup(true, parts);

JniMocker for Test

Sometimes when we write Java tests in Chromium, we can use JniMocker and Mockito library to mock native method behavior.

For example, we have a SigninManagerImpl, here is part of its code:

1
2
3
4
5
6
class SigninManagerImpl {
@NativeMethods
interface Natives {
boolean isSigninAllowedByPolicy(long nativeSigninManagerAndroid);
}
}

In the generated Java side JNI binding class we can see, if we call SigninManagerImplJni.get() it will check if there is a testInstance, and we can call TEST_HOOKS to set the testInstance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class SigninManagerImplJni implements SigninManagerImpl.Natives {
private static SigninManagerImpl.Natives testInstance;

public static final JniStaticTestMocker<SigninManagerImpl.Natives> TEST_HOOKS = new org.chromium.base.JniStaticTestMocker<org.chromium.chrome.browser.signin.SigninManagerImpl.Natives>() {
@java.lang.Override
public void setInstanceForTesting(
org.chromium.chrome.browser.signin.SigninManagerImpl.Natives instance) {
if (!org.chromium.base.natives.GEN_JNI.TESTING_ENABLED) {
throw new RuntimeException("Tried to set a JNI mock when mocks aren't enabled!");
}
testInstance = instance;
}
};

@Override
public boolean isSigninAllowedByPolicy(long nativeSigninManagerAndroid) {
return (boolean)GEN_JNI.org_chromium_chrome_browser_signin_SigninManagerImpl_isSigninAllowedByPolicy(nativeSigninManagerAndroid);
}

public static SigninManagerImpl.Natives get() {
if (GEN_JNI.TESTING_ENABLED) {
if (testInstance != null) {
return testInstance;
}
if (GEN_JNI.REQUIRE_MOCK) {
throw new UnsupportedOperationException("No mock found for the native implementation for org.chromium.chrome.browser.signin.SigninManagerImpl.Natives. The current configuration requires all native implementations to have a mock instance.");
}
}
NativeLibraryLoadedStatus.checkLoaded(false);
return new SigninManagerImplJni();
}
}

Code of JniMocker:

1
2
3
4
5
6
7
8
public class JniMocker extends ExternalResource {
private final ArrayList<JniStaticTestMocker> mHooks = new ArrayList<>();

public <T> void mock(JniStaticTestMocker<T> hook, T testInst) {
hook.setInstanceForTesting(testInst);
mHooks.add(hook);
}
}

Use JniMocker in the test case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(BaseRobolectricTestRunner.class)
public class SigninManagerTest {

@Rule
public final JniMocker mocker = new JniMocker();

private final SigninManagerImpl.Natives mNativeMock = mock(SigninManagerImpl.Natives.class);

@Before
public void setUp() {
// set SigninManagerImplJni to use mNativeMock as testInstance
mocker.mock(SigninManagerImplJni.TEST_HOOKS, mNativeMock);
// set mNativeMock to return true when the mocked method is called
doReturn(true).when(mNativeMock).isSigninAllowedByPolicy(anyLong());
// now we can write the related test code...
}
}

More Resources

https://docs.oracle.com/javase/8/docs/technotes/guides/jni/

https://chromium.googlesource.com/chromium/src/base/+/master/android/jni_generator

https://developer.android.com/training/articles/perf-jni

Java™ Native Interface. Programmer’s Guide and Specification https://book.douban.com/subject/3162962/