This analysis is based on Android 11(R)

The source code path

The essence of registration is to establish a one-to-one relationship between (Java layer)native methods and (native /C++ layer)JNI functions. Static registration refers to the pre-setting of mapping rules so that a native method name can be converted to a unique JNI function name. Dynamically registered mapping rules are set by programmers themselves, binding native methods to JNI function Pointers through constructs.

Library to load

If you need to use the code in the so library in Java, the first thing you do is load the library. Library loads are usually placed in static code blocks to ensure that they are executed as soon as the class is loaded.

package com.hangl.jni;
public class TestJNI {
    static {
        System.loadLibrary("native"); }... }Copy the code

As we all know, the JNI_Onload function in the library is called when the library is loaded. In the ART source System. Will eventually call loadLibrary JavaVMExt: : LoadNativeLibrary. Start by looking for the JNI_Onload symbol in the library and turning it into a function pointer to call. The JNI_Onload function then returns the necessary JNI version number for the library, and the virtual machine determines whether it supports it.

void* sym = library->FindSymbol("JNI_OnLoad".nullptr);

using JNI_OnLoadFn = int(*)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
int version = (*jni_on_load)(this.nullptr); .if (version == JNI_ERR) {
  StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
} else if (JavaVMExt::IsBadJniVersion(version)) {
  StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
                path.c_str(), version); . }else {
  was_successful = true;
}
Copy the code

The VM supports only three JNI versions, as shown in the following table. JNI_VERSION_1_4 is typically returned more often from libraries. Compared to 1_4, the 1_6 version of JNI only has one more GetObjectRefType function [link]. If our library doesn’t use this function, we just need 1_4.

bool JavaVMExt::IsBadJniVersion(int version) {
  // We don't support JNI_VERSION_1_1. These are the only other valid versions.
  returnversion ! = JNI_VERSION_1_2 && version ! = JNI_VERSION_1_4 && version ! = JNI_VERSION_1_6; }Copy the code

The JNI_Onload function is a convenient place for dynamic registration, and you can call RegisterNatives to register. The methods and details are left to the “Dynamic Registration” section.

Native methods in virtual machines

Typically, a Java method can be executed in a virtual machine in two ways, one interpreted and the other machine-code. When the interpretation is executed, the interpreter looks for the entry address of the bytecode. As machine code executes, the virtual machine looks for the address of the machine’s instructions. Considering that each Java method is represented in the virtual machine by an ArtMethod object, the bytecode entry information is (indirectly) stored in its data_ field and the machine code entry information is stored in the entry_POINt_FROM_quick_COMPILed_code_ field. As shown below.

// Must be the last fields in the method.
struct PtrSizedFields {
  // Depending on the method type, the data is
  // - native method: pointer to the JNI function registered to this method
  // or a function to resolve the JNI function,
  // - resolution method: pointer to a function to resolve the method and
  // the JNI function for @CriticalNative.
  // - conflict method: ImtConflictTable,
  // - abstract/interface method: the single-implementation if any,
  // - proxy method: the original interface method or constructor,
  // - other methods: during AOT the code item offset, at runtime a pointer
  // to the code item.
  void* data_;

  // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
  // the interpreter.
  void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
Copy the code

For native methods, however, only the definition is not implemented in the Java world, so there is no bytecode information. There is no need to store the entry address of the bytecode, but the native method takes an extra step in the call process.

  • The first step is to enter a springboard function, which will handle the conversion of Java parameters to Native parameters and the process of thread state switch.
  • Call JNI functions implemented in the Native world from within the springboard function.

In this way, instead of storing bytecode entry information, the data_ field can be used to store the entry address of the JNI function. Entry_point_from_quick_compiled_code_ is the entry address of the springboard function. Specific reference ART view | why can call Native methods into c + + world.

Static registration

If we do not call RegisterNatives in JNI_Onload or write JNI_Onload functions in the SO library at all, the mapping of native methods can only be done by static registration.

Although the name for this process is “static registration,” the actual registration is done dynamically at run time on demand, but it is called “static” because the mapping is determined in advance.

So what are the specific mapping rules?

JNI implements two sets of mapping rules, a simplified version and a complex version to deal with method overloading. The final converted function names are concatenated in sequence according to the following rules.

  • The prefixJava_
  • The name of the class, will be/into_
  • Underscore hyphen_
  • The method name
  • If you need to distinguish between two overloaded methods, use a double underscore__Connection parameter signature. If the method is not overloaded, omit this step.

To distinguish overloaded methods, a concatenation parameter signature is required at the end of the string, and it is possible to have [,;, and _ characters in the signature. To avoid these special characters in the function name (or to avoid confusion with the previous hyphen _), these characters are treated specially during the conversion.

  • _ converts to _1

  • ; Convert _2

  • Convert to _3

Since neither Java class nor method names can begin with a number, such conversions do not conflict with Java class or method names. Refer to the JNI documentation for specific rules. Here is an example of a transformation.

package pkg;  

class Cls { 

     native double f(int i, String s); . }Copy the code

To:

JNIEXPORT jdouble JNICALL Java_pkg_Cls_f__ILjava_lang_String_2 ( 

     JNIEnv *env,        /* interface pointer */ 

     jobject obj,        /* "this" pointer */ 

     jint i,             /* argument #1 */ 

     jstring s)          /* argument #2 */ 

{ 

     /* Obtain a C-copy of the Java string */ 

     const char *str = (*env)->GetStringUTFChars(env, s, 0); 

     /* process the string */./* Now we are done with str */ 

     (*env)->ReleaseStringUTFChars(env, s, str); 

     return. }Copy the code

Note, however, that statically registered JNI functions must be decorated by JNIEXPORT and JNICALL.

JNIEXPORT indicates that the function name is printed to the dynamic symbol table so that it can be found by subsequent registration calls to DLSYM. By default, all function names are printed to the dynamic symbol table. But for security, we can turn off this output at compile time by passing -fvisibility=hidden (the JNIEXPORT modifier will still output) to prevent others from knowing which functions are defined in so. This is especially important for business software.

JNICALL is mainly used to bridge the differences between the invocation rules of different hardware platforms. For AArch64, JNICALL does nothing.

After the introduction of rules, the specific process of in-depth registration will be followed.

As mentioned earlier, the ArtMethod object’s data_ field stores the entry address of the JNI function, while entry_point_from_quick_compiled_code_ stores the entry address of the springboard function. For static registration, however, the mapping is not established until the first method call.

Before a method can be called, the class to which it belongs must be loaded. So what are data_ and entry_point_from_quick_compiled_code_ equal between the time the class is loaded and the first call to the method?

The LinkCode function is called when the class is loaded to set the entry_POINt_FROM_quick_compiled_code_ and data_ fields for the ArtMethod object.

static void LinkCode(ClassLinker* class_linker,
                     ArtMethod* method,
                     const OatFile::OatClass* oat_class,
                     uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {...const void* quick_code = nullptr;
  if(oat_class ! =nullptr) {
    // Every kind of method should at least get an invoke stub from the oat_method.
    // non-abstract methods also get their code pointers.
    const OatFile::OatMethod oat_method = oat_class->GetOatMethod(class_def_method_index);
    quick_code = oat_method.GetQuickCode();
  }
  ...
  if (quick_code == nullptr) {
    method->SetEntryPointFromQuickCompiledCode(  / / set entry_point_from_quick_compiled_code_ fields
        method->IsNative()?GetQuickGenericJniStub() : GetQuickToInterpreterBridge()); }...if (method->IsNative()) {
    // Set up the dlsym lookup stub. Do not go through `UnregisterNative()`
    // as the extra processing for @CriticalNative is not needed yet.
    method->SetEntryPointFromJni(  // set data_ field
        method->IsCriticalNative()?GetJniDlsymLookupCriticalStub() : GetJniDlsymLookupStub()); . }}Copy the code

There are two possible values for entry_point_FROM_quick_compiled_code_ :

  1. As the springboard function is mainly responsible for parameter conversion, the same springboard function can be used for different native methods as long as the number and type of their parameters are consistent. These springboard functions are only generated under AOT-compiled conditions, so pure interpretation is executedquick_code= = nullptr.
  2. whenquick_codeIf == NullPTR, isentry_point_from_quick_compiled_code_The value set isart_quick_generic_jni_trampolineFunction pointer. It is equivalent to a general-purpose springboard function that performs dynamic parameter conversion during execution.

The value of data_ (without regard to CriticalNative) has only one possibility:

  1. Set toart_jni_dlsym_lookup_stubFunction pointer. At execution time, the function finds the JNI function according to the static transformation rules, and then jumps to it. So the real registration takes place in it.

Let’s look at the process of finding JNI functions. Art_jni_dlsym_lookup_stub is assembly code that internally calls artFindNaitveMethod to find a pointer to the JNI function, and then jumps to the JNI function using the BR X17 instruction.

ENTRY art_jni_dlsym_lookup_stub ... // Call artFindNativeMethod() for normal native and artFindNativeMethodRunnable() // for @FastNative or @CriticalNative. . b.ne .Llookup_stub_fast_or_critical_native bl artFindNativeMethod b .Llookup_stub_continue .Llookup_stub_fast_or_critical_native: bl artFindNativeMethodRunnable .Llookup_stub_continue: mov x17, x0 // store result in scratch reg. ... cbz x17, 1f // is method code null ? br x17 // if non-null, tail call to method's code. 1: ret // restore regs and return to caller to handle exception. END art_jni_dlsym_lookup_stubCopy the code
extern "C" const void* artFindNativeMethodRunnable(Thread* self)
    REQUIRES_SHARED(Locks::mutator_lock_) {...const void* native_code = class_linker->GetRegisteredNative(self, method);
   if(native_code ! =nullptr) {
     returnnative_code; }... JavaVMExt* vm = down_cast<JNIEnvExt*>(self->GetJniEnv() - >GetVm(a); native_code = vm->FindCodeForNativeMethod(method); .return class_linker->RegisterNative(self, method, native_code);
 }
Copy the code

ArtFindNativeMethod internal call is artFindNativeMethodRunnable, it first determines ArtMethod data_ field is already registered, if is returned directly data_ storage function pointer. Otherwise call FindCodeForNativeMethod to find it. Finally, the function pointer found is written to the data_ field.

// See section 11.3 "Linking Native Methods" of the JNI spec.
void* FindNativeMethod(Thread* self, ArtMethod* m, std::string& detail)
    REQUIRES(! Locks::jni_libraries_lock_)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  std::string jni_short_name(m->JniShortName());
  std::string jni_long_name(m->JniLongName()); . {// Go to suspended since dlsym may block for a long time if other threads are using dlopen.
    ScopedThreadSuspension sts(self, kNative);
    void* native_code = FindNativeMethodInternal(self,
                                                 declaring_class_loader_allocator,
                                                 shorty,
                                                 jni_short_name,
                                                 jni_long_name);
    if(native_code ! =nullptr) {
      returnnative_code; }}... }Copy the code

FindCodeForNativeMethod internally calls FindNativeMethod to create two strings, one with jni_short_name and the other with jni_long_name. In fact, they reflect the two kinds of mapping rules mentioned earlier.

std::string GetJniShortName(const std::string& class_descriptor, const std::string& method) {
  // Remove the leading 'L' and trailing '; '...
  std::string class_name(class_descriptor);
  CHECK_EQ(class_name[0].'L') << class_name;
  CHECK_EQ(class_name[class_name.size() - 1].'; ') << class_name;
  class_name.erase(0.1);
  class_name.erase(class_name.size() - 1.1);

  std::string short_name;
  short_name += "Java_";
  short_name += MangleForJni(class_name);
  short_name += "_";
  short_name += MangleForJni(method);
  return short_name;
}
Copy the code
std::string ArtMethod::JniLongName(a) {
  std::string long_name;
  long_name += JniShortName(a); long_name +="__";

  std::string signature(GetSignature().ToString());
  signature.erase(0.1);
  signature.erase(signature.begin() + signature.find(') '), signature.end());

  long_name += MangleForJni(signature);

  return long_name;
}
Copy the code

Short name is a mapping rule without overloading, while Long name adds parameter information to distinguish between methods. When looking for symbols, look first for short name, then for long name.

Dynamic registration

Dynamic registration requires actively calling the RegisterNatives function in JNI_Onload and passing in the class and JNINativeMethod structure parameters. Here is a practical example.

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  JNIEnv* env = NULL;
  jint ret = vm->AttachCurrentThread(&env, NULL);
  LOG_ALWAYS_FATAL_IF(ret ! = JNI_OK,"AttachCurrentThread failed");
  android::RegisterDrawFunctor(env);
  android::RegisterDrawGLFunctor(env);
  android::RegisterGraphicsUtils(env);

  return JNI_VERSION_1_4;
}
Copy the code
const char kClassName[] = "com/android/webview/chromium/GraphicsUtils";
const JNINativeMethod kJniMethods[] = {
    { "nativeGetDrawSWFunctionTable"."()J".reinterpret_cast<void*>(GetDrawSWFunctionTable) },
    { "nativeGetDrawGLFunctionTable"."()J".reinterpret_cast<void*>(GetDrawGLFunctionTable) },
};

void RegisterGraphicsUtils(JNIEnv* env) {
  jclass clazz = env->FindClass(kClassName);
  LOG_ALWAYS_FATAL_IF(! clazz,"Unable to find class '%s'", kClassName);

  int res = env->RegisterNatives(clazz, kJniMethods, NELEM(kJniMethods));
  LOG_ALWAYS_FATAL_IF(res < 0."register native methods failed: res=%d", res);
}
Copy the code

When RegisterNatives is passed in clazz, the class is already loaded in FindClass. Then look at the JNINativeMethod structure. It stores three fields internally, one for the method name, one for the method signature, and one for the JNI function pointer. With Clazz, method names and method signatures uniquely identify a method in the Java world. Corresponding to the JNI function pointer, the one-to-one mapping rule from the Java world to the Native world is determined.

RegisterNatives is also a simple registration procedure, with the method name and method signature finding the corresponding ArtMethod object, and then writing the JNI function pointer to its data_ field. Therefore, it is faster than static registration.

Registration type Registration time Registration speed
Static registration The method is registered with the art_jni_dlsym_lookup_stub function when first called Common, you need to look up symbols in the library through DLSYM
Dynamic registration Register with the RegisterNatives function when the library loads fast

Refer to the article

  1. JNI calls and dynamic registration exploration
  2. JNI rules
  3. ART view | why can call Native methods into the c + + world