向JVM註冊本地方法是怎麼實現的

超人汪小建發表於2018-09-03

前言

Java 中我們經常會遇到要呼叫本地方法的情況,而且 Java 核心庫中的很多類也大量使用了本地方法,使用 JNI 時本地函式需要按照約定好的格式進行命名,如果不想寫長長的函式名則需要將方法註冊到 JVM 中,這裡看看怎麼向 JVM 註冊本地方法。

命名約定

JVM 中對本地方法名有約定,在使用 JNI 時需要遵守,即為Java_<fully qualified class name>_method

比如這裡編寫一個 Java 類提供本地加密的方法,其中加密方法為本地方法,實現是在ByteCodeEncryptor動態庫,那麼它本地對應的函式名為Java_com_seaboat_bytecode_ByteCodeEncryptor_encrypt

package com.seaboat.bytecode;

public class ByteCodeEncryptor {
  static{
    System.loadLibrary("ByteCodeEncryptor"); 
  }
  
  public native static byte[] encrypt(byte[] text);
  
}
複製程式碼

registerNatives

如果覺得本地函式的命名約定比較繁瑣,那麼可以使用 registerNatives 方式來註冊本地函式,這樣就可以隨意命名函式。而且認為經過 registerNatives 往 JVM 中註冊的函式在執行時會更加高效,因為函式的查詢更快了。

如何註冊

有兩種方式可以實現本地方法註冊:

1、Java 中靜態塊

  • 在 Java 類中宣告一個registerNatives靜態方法。
  • 在原生程式碼中定義一個Java_<fully qualified class name>_registerNatives函式。
  • 在呼叫其他本地函式前要先呼叫registerNatives方法。

比如對於 Object 類,在類中進行如下操作:

private static native void registerNatives();
static {
    registerNatives();
  }

複製程式碼

本地中通過registerNatives將指定的本地方法繫結到指定函式,比如這裡將hashCodeclone本地方法繫結到JVM_IHashCodeJVM_IHashCode函式。

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}
複製程式碼

2、使用JNI_OnLoad

JNI_OnLoad函式在 JVM 執行System.loadLibrary方法時被呼叫,所以可以在該方法中呼叫RegisterNatives函式註冊本地函式。通過該種方式註冊本地方法則無需在 Java 類中宣告RegisterNatives本地方法來註冊了。

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};

int JNI_OnLoad(JavaVM* vm, void* reserved)
{
...
if ((*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0])) < 0)
{
    return JNI_ERR;
}
...
}
複製程式碼

registerNatives幹了什麼

定義了 JNINativeMethod 結構體用於宣告方法和函式,如下,name 表示 Java 的本地方法名,signature 表示方法的簽名,fnPtr 表示函式指標。

typedef struct {
    char *name;
    char *signature;
    void *fnPtr;
} JNINativeMethod;
複製程式碼

主要要看(*env)->RegisterNatives這個函式,

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}
複製程式碼

該方法的宣告在一個JNINativeInterface_結構體中,該結構體包含了 JNI 的所有介面函式宣告,JVM 中定義了結構體變數jni_NativeInterface來使用,這裡只列出RegisterNatives函式的宣告,其他函式省略。

struct JNINativeInterface_ {
    ...
    jint (JNICALL *RegisterNatives) (JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
    ...
}

struct JNINativeInterface_ jni_NativeInterface = {
    ...
    jni_RegisterNatives,
    ...
}
複製程式碼

在看jni_RegisterNatives函式的實現前先了解JNI_ENTRYJNI_END巨集,這兩個巨集將共同的部分都抽離出來了。其中JNI_END比較簡單,就兩個結束大括號。

#define JNI_ENTRY(result_type, header)  JNI_ENTRY_NO_PRESERVE(result_type, header)    WeakPreserveExceptionMark __wem(thread);

#define JNI_END } }
複製程式碼

JNI_ENTRY主要邏輯:

  • 獲取當前執行執行緒 JavaThread 指標物件。
  • 建立 ThreadInVMfromNative 物件。
  • TRACE_CALL ,這裡什麼都不幹。
  • 建立 HandleMarkCleaner 物件。
  • 將 thread 賦值給 Exceptions 中的 THREAD。
  • 校驗棧對齊。
  • 建立 WeakPreserveExceptionMark 物件。
#define JNI_ENTRY_NO_PRESERVE(result_type, header)                   \
extern "C" {                                                         \
  result_type JNICALL header {                                       \
    JavaThread* thread=JavaThread::thread_from_jni_environment(env); \
    assert( !VerifyJNIEnvThread || (thread == Thread::current()), "JNIEnv is only valid in same thread"); \
    ThreadInVMfromNative __tiv(thread);                              \
    debug_only(VMNativeEntryWrapper __vew;)                          \
    VM_ENTRY_BASE(result_type, header, thread)
    
#define VM_ENTRY_BASE(result_type, header, thread)                   \
  TRACE_CALL(result_type, header)                                    \
  HandleMarkCleaner __hm(thread);                                    \
  Thread* THREAD = thread;                                           \
  os::verify_stack_alignment();      
複製程式碼

現在看jni_RegisterNatives函式具體的實現,邏輯為:

  • JNIWrapper 用於 debug。
  • HOTSPOT_JNI_REGISTERNATIVES_ENTRY 和 DT_RETURN_MARK 都用於 dtrace。
  • 建立 KlassHandle 物件。
  • 開始遍歷方法陣列,獲取對應的方法名、方法簽名和方法長度等資訊。
  • 嘗試在符號常量池中查詢是否已經存在對應的方法名和方法簽名,如果找不到則要拋異常,因為正常情況載入 Java 類時已經新增到常量池中了。
  • 呼叫register_native函式註冊。
JNI_ENTRY(jint, jni_RegisterNatives(JNIEnv *env, jclass clazz,
                                    const JNINativeMethod *methods,
                                    jint nMethods))
  JNIWrapper("RegisterNatives");
  HOTSPOT_JNI_REGISTERNATIVES_ENTRY(env, clazz, (void *) methods, nMethods);
  jint ret = 0;
  DT_RETURN_MARK(RegisterNatives, jint, (const jint&)ret);

  KlassHandle h_k(thread, java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz)));

  for (int index = 0; index < nMethods; index++) {
    const char* meth_name = methods[index].name;
    const char* meth_sig = methods[index].signature;
    int meth_name_len = (int)strlen(meth_name);

    TempNewSymbol  name = SymbolTable::probe(meth_name, meth_name_len);
    TempNewSymbol  signature = SymbolTable::probe(meth_sig, (int)strlen(meth_sig));

    if (name == NULL || signature == NULL) {
      ResourceMark rm;
      stringStream st;
      st.print("Method %s.%s%s not found", h_k()->external_name(), meth_name, meth_sig);
      THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), -1);
    }

    bool res = register_native(h_k, name, signature,
                               (address) methods[index].fnPtr, THREAD);
    if (!res) {
      ret = -1;
      break;
    }
  }
  return ret;
JNI_END
複製程式碼

register_native函式邏輯如下:

  • 到對應 Klass 物件中查詢指定方法,如果不存在則拋異常。
  • 方法如果不是宣告為 native,則先嚐試查詢被新增了字首的本地方法,這個是因為可能在JVM TI agent 中設定某些 native 方法的字首,如果還是為空則最終丟擲異常。
  • 呼叫最重要的set_native_function函式,將 C++ 的函式繫結到該 Method 物件中。
  • 如果函式指標為空,則呼叫clear_native_function清理本地方法物件。
static bool register_native(KlassHandle k, Symbol* name, Symbol* signature, address entry, TRAPS) {
  Method* method = k()->lookup_method(name, signature);
  if (method == NULL) {
    ResourceMark rm;
    stringStream st;
    st.print("Method %s name or signature does not match",
             Method::name_and_sig_as_C_string(k(), name, signature));
    THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), false);
  }
  if (!method->is_native()) {
    method = find_prefixed_native(k, name, signature, THREAD);
    if (method == NULL) {
      ResourceMark rm;
      stringStream st;
      st.print("Method %s is not declared as native",
               Method::name_and_sig_as_C_string(k(), name, signature));
      THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), false);
    }
  }

  if (entry != NULL) {
    method->set_native_function(entry,
      Method::native_bind_event_is_interesting);
  } else {
    method->clear_native_function();
  }
  if (PrintJNIResolving) {
    ResourceMark rm(THREAD);
    tty->print_cr("[Registering JNI native method %s.%s]",
      method->method_holder()->external_name(),
      method->name()->as_C_string());
  }
  return true;
}
複製程式碼

set_native_function函式邏輯為:

  • 通過native_function_addr函式獲取本地函式地址,這個函式直接return (address*) (this+1);,可以看到它是直接將 Method 物件的地址+1 作為本地函式地址的。能夠這樣操作是因為在建立 Method 物件時會判斷是否為 native 方法,如果是則會額外留兩個地址位置,分別用於本地函式地址和方法簽名。
  • 判斷本地函式地址是否已經等於函式指標,是的話說明已經繫結,直接返回,否則繼續往下。
  • 如果 Jvmti 設定了傳播繫結本地方法事件則傳送事件。
  • 將函式指標賦給本地函式地址。
  • GCC 獲取編譯的函式程式碼。
void Method::set_native_function(address function, bool post_event_flag) {
  assert(function != NULL, "use clear_native_function to unregister natives");
  assert(!is_method_handle_intrinsic() || function == SharedRuntime::native_method_throw_unsatisfied_link_error_entry(), "");
  address* native_function = native_function_addr();

  address current = *native_function;
  if (current == function) return;
  if (post_event_flag && JvmtiExport::should_post_native_method_bind() &&
      function != NULL) {
    assert(function !=
      SharedRuntime::native_method_throw_unsatisfied_link_error_entry(),
      "post_event_flag mis-match");
    JvmtiExport::post_native_method_bind(this, &function);
  }
  *native_function = function;
  CompiledMethod* nm = code(); 
  if (nm != NULL) {
    nm->make_not_entrant();
  }
}
複製程式碼

-------------推薦閱讀------------

我的開源專案彙總(機器&深度學習、NLP、網路IO、AIML、mysql協議、chatbot)

為什麼寫《Tomcat核心設計剖析》

我的2017文章彙總——機器學習篇

我的2017文章彙總——Java及中介軟體

我的2017文章彙總——深度學習篇

我的2017文章彙總——JDK原始碼篇

我的2017文章彙總——自然語言處理篇

我的2017文章彙總——Java併發篇


跟我交流,向我提問:

向JVM註冊本地方法是怎麼實現的

歡迎關注:

向JVM註冊本地方法是怎麼實現的

相關文章