# 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 voidSomeStaticMethod(); }
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.
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
publicclassMain{ 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 voidJNI_OnUnload(JavaVM* vm, void* reserved);
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.
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
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.
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++.
// print log by calling static Java method of android.util.Log.i(String tag, String message) intlog(JNIEnv* env, constchar* 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. returnstatic_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.
// 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.
namespace { JavaVM *g_jvm = nullptr; jobject g_class_loader = nullptr; jmethodID g_find_class_method = nullptr; // this should be a class in application constexprchar kJavaClass[] = "org/chromium/chrome/Xxx"; }
// this method can be called from cpp native thread and find the right class jclass FindClassInAnyThread(JNIEnv* env, constchar* name){ returnstatic_cast<jclass>(env->CallObjectMethod( g_class_loader, g_find_class_method, env->NewStringUTF(name))); }
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.
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.
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. 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; }
// We can use the annotation to specify the related native namespace @JNINamespace("native_namespace") publicclassJavaClass{
classSomeClass{ }
// native side instance pointer privatefinallong mNativeCppClass;
// create instaces from Java side publicJavaClass(){ // 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 publicJavaClass(long nativeCppClass){ mNativeCppClass = nativeCppClass; }
intcallNativeMethod(){ // call native member method SomeClass param = new SomeClass(); int result = JavaClassJni.get().cppClassMember(mNativeCppClass, param); return result; }
@NativeMethods interfaceNatives{ // 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. longinit();
// 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>" intcppClassMember(long nativeCppClass, SomeClass param); } }
// 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 = newCppClass(); returnreinterpret_cast<intptr_t>(cpp_class); }
# 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.
publicstaticfinal JniStaticTestMocker<JavaClass.Natives> TEST_HOOKS = new org.chromium.base.JniStaticTestMocker<com.demo.jni.JavaClass.Natives>() { @java.lang.Override publicvoidsetInstanceForTesting(com.demo.jni.JavaClass.Natives instance){ if (!org.chromium.base.natives.GEN_JNI.TESTING_ENABLED) { thrownew RuntimeException("Tried to set a JNI mock when mocks aren't enabled!"); } testInstance = instance; } };
publicstatic JavaClass.Natives get(){ if (GEN_JNI.TESTING_ENABLED) { if (testInstance != null) { return testInstance; } if (GEN_JNI.REQUIRE_MOCK) { thrownew 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); returnnew JavaClassJni(); } }
Generated CPP Header File (Native Side JNI Binding)
// 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
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:
// 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; classXXX{ nativevoidsomeMethod(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; classXXX{ classInternalError{ } nativevoidsomeMethod(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.
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.
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 publicvoidfinishNativeInitialization(){ // this method will be called when native initialized. // if native is already initialized, this method will be called immediately. } };
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.
publicstatic SigninManagerImpl.Natives get(){ if (GEN_JNI.TESTING_ENABLED) { if (testInstance != null) { return testInstance; } if (GEN_JNI.REQUIRE_MOCK) { thrownew 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); returnnew SigninManagerImplJni(); } }
Code of JniMocker:
1 2 3 4 5 6 7 8
publicclassJniMockerextendsExternalResource{ privatefinal ArrayList<JniStaticTestMocker> mHooks = new ArrayList<>();
public <T> voidmock(JniStaticTestMocker<T> hook, T testInst){ hook.setInstanceForTesting(testInst); mHooks.add(hook); } }
@Before publicvoidsetUp(){ // 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... } }