ART视角 | JNI静态注册和动态注册

本文分析基于Android 11(R)

源码路径

注册的本质是建立(Java层)native方法和(Native/C++层)JNI函数之间的一对一关系。静态注册指的是映射规则预先设定,一个native方法名可以转换成一个唯一的JNI函数名。动态注册的映射规则由程序员自己设定,通过结构体将native方法和JNI函数指针绑定起来。

库加载

如果需要在Java中使用so库中的代码,那么首先要做的就是库加载。库加载一般放在static代码块中,保证类加载后第一时间执行。

package com.hangl.jni;
public class TestJNI {
    static {
        System.loadLibrary("native");
    }     
...
}
复制代码

众所周知,库加载时会去调用库中的JNI_Onload函数。在ART源码中,System.loadLibrary最终会调用JavaVMExt::LoadNativeLibrary。首先去库中寻找JNI_Onload符号,将其转成函数指针进行调用。接着,JNI_Onload函数返回该库所必需的JNI版本号,由虚拟机判断是否支持。

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;
}
复制代码

当前虚拟机只支持三个JNI版本,如下所示。一般库返回较多的是JNI_VERSION_1_4。与1_4相比,1_6版本的JNI只多了一个GetObjectRefType函数[链接],如果我们的库用不到这个函数,只需要1_4即可。

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

JNI_Onload函数是我们进行动态注册的宝地,在其中调用RegisterNatives即可进行注册。具体的方法和细节留到”动态注册”章节阐述。

虚拟机中的native方法

通常而言,一个Java方法在虚拟机中可以找到两种执行方式,一种是解释执行,另一种是机器码执行。解释执行时,解释器会去寻找字节码的入口地址。而机器码执行时,虚拟机会去寻找机器指令的入口地址。考虑到每个Java方法在虚拟机中都由ArtMethod对象表示,字节码的入口信息(间接)存在其data_字段中,而机器码入口信息则存在entry_point_from_quick_compiled_code_字段中。如下所示。

// 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_;
复制代码

不过对于native方法而言,它在Java世界中只有定义没有实现,因此不会有字节码信息。虽然不需要存储字节码的入口地址,但native方法在调用过程中却会多出一步。
JNI跳板函数.png

  • 首先进入一个跳板函数,其中会处理Java参数到Native参数的转换以及线程状态切换等过程。
  • 在跳板函数内部调用Native世界中实现的JNI函数。

这样一来,不用存储字节码入口信息的data_字段就可以用来存储JNI函数的入口地址了。而entry_point_from_quick_compiled_code_中存储的就是跳板函数的入口地址。具体可参考ART视角 | 为什么调用Native方法可以进入C++世界

静态注册

当我们不在JNI_Onload中调用RegisterNatives,或者压根不在so库中编写JNI_Onload函数时,native方法的映射只能由静态注册完成。

虽然这个过程的名字叫”静态注册”,但实际注册是在运行时按需动态完成的,只不过由于映射关系是事先确定的,所以才被叫做”静态”。

那么具体的映射规则是什么呢?

JNI实现了两套映射规则,一套是简化版,一套是为了解决方法重载的复杂版。最终转换出来的函数名按照如下规则顺次拼接。

  • 前缀Java_
  • 类名,将/转成_
  • 下划线连接符_
  • 方法名
  • 如果需要区分两个重载的方法,则用双下划线__连接参数签名。如果该方法没有被重载,则省略这一步。

为了区分重载方法,字符串的末尾需要拼接参数签名,而签名中是有可能有[;_字符的。为了不在函数名中出现这些特殊字符(或者为了不和之前的连接符_混淆),转换时对这些字符做了特殊处理。

  • _转换为_1

  • ;转换为_2

  • [转换为_3

由于Java类名和方法名中都不可能以数字开头,所以这样的转换不会跟Java类名或方法名冲突。具体规则参考JNI文档。以下是一个转换示例。

package pkg;  

class Cls { 

     native double f(int i, String s); 

     ... 

} 
复制代码

转换为:

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

} 
复制代码

不过要注意一点,静态注册的JNI函数必须由JNIEXPORTJNICALL进行修饰。

JNIEXPORT表明将函数名输出到动态符号表中,这样后续注册时调用dlsym才能找的到。默认情况下,所有的函数名都会输出到动态符号表中。但为了安全性,我们可以在编译时传入-fvisibility=hidden来关闭这种输出(JNIEXPORT修饰的依然会输出),防止别人知道so中定义了哪些函数。这一点对于商业软件尤为重要。

JNICALL主要用于消除不同硬件平台调用规则的差异,对于AArch64而言,JNICALL不执行任何动作。

规则介绍完毕,接下来就要深入注册的具体过程。

上文中提到,ArtMethod对象的data_字段存储JNI函数的入口地址,而entry_point_from_quick_compiled_code_存储跳板函数的入口地址。可是对静态注册而言,直到第一次方法调用时映射关系才建立。

一个方法被调用之前,首先要加载它所属的类。那么在类加载到方法第一次调用的这段时间里,data_entry_point_from_quick_compiled_code_等于什么呢?

类加载时会调用LinkCode函数,为ArtMethod对象设置entry_point_from_quick_compiled_code_data_字段。

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_ 字段
        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_ 字段
        method->IsCriticalNative() ? GetJniDlsymLookupCriticalStub() : GetJniDlsymLookupStub());
  ...
  }
}
复制代码

entry_point_from_quick_compiled_code_的值有两种可能:

  1. 由于跳板函数主要负责参数转换,因此对于不同的native方法,只要它们的参数个数和类型一致,就可以使用同一个跳板函数。这些跳板函数只在AOT编译条件下才会生成,因此纯解释执行时quick_code == nullptr。
  2. quick_code == nullptr时,为entry_point_from_quick_compiled_code_设置的值是art_quick_generic_jni_trampoline函数指针。它相当于一个通用的跳板函数,在执行过程中动态进行参数转换。

data_的值(不考虑CriticalNative)只有一种可能:

  1. 设置为art_jni_dlsym_lookup_stub函数指针。该函数在执行时根据静态转换规则找到JNI函数,接着跳转过去。因此真正的注册发生在它里面。

接下来看看JNI函数的寻找过程。art_jni_dlsym_lookup_stub是汇编代码,其内部会调用artFindNaitveMethod找到JNI函数的指针,然后通过br x17指令跳转到该JNI函数中。

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_stub
复制代码
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) {
     return native_code;
   }
   ...
   JavaVMExt* vm = down_cast<JNIEnvExt*>(self->GetJniEnv())->GetVm();
   native_code = vm->FindCodeForNativeMethod(method);
   ...
   return class_linker->RegisterNative(self, method, native_code);
 }
复制代码

artFindNativeMethod内部调用的是artFindNativeMethodRunnable,它首先判断ArtMethod的data_字段是不是已经注册过了,如果是则直接返回data_存储的函数指针。否则调用FindCodeForNativeMethod去寻找。最后将找到的函数指针写入data_字段中。

// 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) {
      return native_code;
    }
  }
  ...
}
复制代码

FindCodeForNativeMethod内部调用FindNativeMethod,创建两个字符串,一个是jni_short_name,另一个是jni_long_name。其实二者反映的就是之前所说的两种映射规则。

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;
}
复制代码
std::string ArtMethod::JniLongName() {
  std::string long_name;
  long_name += JniShortName();
  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;
}
复制代码

Short name是不考虑重载的映射规则,long name则增加了参数信息用于区分不同方法。寻找符号时,先找short name,再找long name。

动态注册

动态注册需要在JNI_Onload中主动调用RegisterNatives函数,并传入class和JNINativeMethod结构体两个参数。以下是一个实际的例子。

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;
}
复制代码
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);
}
复制代码

在调用RegisterNatives传入clazz时,该类已经在FindClass时被加载。接着看看JNINativeMethod结构体。其内部存储了三个字段,一个是方法名,一个是方法签名,还有一个是JNI函数指针。通过clazz,方法名和方法签名可以唯一确定Java世界中的一个方法。将其和JNI函数指针对应,便确定了Java世界到Native世界一对一的映射规则。

RegisterNatives中的注册过程也很简单,通过方法名和方法签名找到对应的ArtMethod对象,然后将JNI函数指针写入其data_字段。因此它的注册速度要优于静态注册。

注册种类 注册时机 注册速度
静态注册 方法第一次被调用时通过art_jni_dlsym_lookup_stub函数注册 普通,需要通过dlsym在库中查找符号
动态注册 库加载时通过RegisterNatives函数注册 快速

参考文章

  1. JNI调用和动态注册探索
  2. JNI规则
  3. ART视角 | 为什么调用Native方法可以进入C++世界
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享