静态注册 jni 函数的参数问题

106 阅读4分钟

引子

某个 APP 中使用静态注册的方式使用 jni,但是在修改问题的时候发现,只要函数名相同,即便函数参数不同,也可以调用到相应的函数。示例代码如下:

public static native String getStrFromJNI(String test);
/*
 * Class:     com_test_MainActivity
 * Method:    getStrFromJNI
 */
jstring
Java_com_test_MainActivity_getStrFromJNI(JNIEnv *env, jclass obj) {
    return (*env)->NewStringUTF(env, "hello world.");
}

为什么会出现这种情况

这里引用 NDK 系列(6):说一下注册 JNI 函数的方式和时机 中分析的静态注册的 jni 函数加载过程,jni 函数在加载的时候有长名字和断名字的区分,优先短名字,未找到则查找长名字。

  void* FindNativeMethodInternal(Thread* self,
                                 void* declaring_class_loader_allocator,
                                 const char* shorty,
                                 const std::string& jni_short_name,
                                 const std::string& jni_long_name)
      REQUIRES(!Locks::jni_libraries_lock_) {
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    for (const auto& lib : libraries_) {
      SharedLibrary* const library = lib.second;
      // Use the allocator address for class loader equality to avoid unnecessary weak root decode.
      if (library->GetClassLoaderAllocator() != declaring_class_loader_allocator) {
        // We only search libraries loaded by the appropriate ClassLoader.
        continue;
      }
      // 添加的 log
      LOG(ERROR) << "[Found native code for " << jni_short_name << ", and " << jni_long_name
        << " in "" << library->GetPath() << ""]";

      // Try the short name then the long name...
      const char* arg_shorty = library->NeedsNativeBridge() ? shorty : nullptr;
      void* fn = library->FindSymbol(jni_short_name, arg_shorty);
      if (fn == nullptr) {
        fn = library->FindSymbol(jni_long_name, arg_shorty);
      }
      if (fn != nullptr) {
        VLOG(jni) << "[Found native code for " << jni_long_name
                  << " in "" << library->GetPath() << ""]";
        return fn;
      }
    }
    return nullptr;
  }

其实在 oracle 的 jni design spec 中已经详细的讲解了 jni 函数的规范,函数名,函数参数的定义和使用等,如下部分引用自 oracle:jni design spec

Native Method Arguments

The JNI interface pointer is the first argument to native methods. The JNI interface pointer is of type JNIEnv. The second argument differs depending on whether the native method is static or nonstatic. The second argument to a nonstatic native method is a reference to the object. The second argument to a static native method is a reference to its Java class.

The remaining arguments correspond to regular Java method arguments. The native method call passes its result back to the calling routine via the return value. Chapter 3 describes the mapping between Java and C types.

Code Example 2-1 illustrates using a C function to implement the native method f. The native method f is declared as follows:

package pkg;  

class Cls { 

     native double f(int i, String s); 

     ... 

}

The C function with the long mangled name Java_pkg_Cls_f_ILjava_lang_String_2 implements native method f:

Code Example 2-1 Implementing a Native Method Using C

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

}

Note that we always manipulate Java objects using the interface pointer env . Using C++, you can write a slightly cleaner version of the code, as shown in Code Example 2-2:

Code Example 2-2 Implementing a Native Method Using C++

extern "C" /* specify the C calling convention */  

jdouble Java_pkg_Cls_f__ILjava_lang_String_2 ( 

     JNIEnv *env,        /* interface pointer */ 

     jobject obj,        /* "this" pointer */ 

     jint i,             /* argument #1 */ 

     jstring s)          /* argument #2 */ 

{ 

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

     ... 

     env->ReleaseStringUTFChars(s, str); 

     return ... 

}

With C++, the extra level of indirection and the interface pointer argument disappear from the source code. However, the underlying mechanism is exactly the same as with C. In C++, JNI functions are defined as inline member functions that expand to their C counterparts.

可以看到,oracle 建议直接使用带有函数参数的长名字 jni 函数,Java 类型和描述符参考文章最后的附件。

综上:

native 方法 public static native String getStrFromJNI(String test);通过 jni 规则组装后得到相应的 jni 方法短名字即是:Java_com_test_MainActivity_getStrFromJNI(),直接找到了不带参数的 jni 函数。注意:AS 中会对此 jni 函数提示“Missing parameter: xxx”。

[Found native code for Java_com_test_MainActivity_getStrFromJNI, and Java_com_test_MainActivity_getStrFromJNI__Ljava_lang_String_2 in "/data/app/~~JLOc-F-NHVirp6Wf2yLqgA==/com.test-Bu4wruS8CkyKlM5Z4mqLyQ==/base.apk!/lib/arm64-v8a/libtest.so"]

解决

解决方案有三个:

  1. 不使用静态注册,使用动态注册(因为有注册表,所以查找效率更高),没有长名字短名字的问题,需要在注册的时候设定好函数参数、返回值。动态注册的 jni 函数 AS 中会提示报错,而静态的不会;
  2. 静态注册时都使用长名字,稍微麻烦,每个函数名字都要包含参数;
  3. 在写代码的时候注意规避,Java、jni 两侧匹配。

问题

  1. 为什么 jni 函数必须添加 extern "C"

根据上面分析的,Java 调用 jni 函数时是根据函数名来查找的,有短名字和长名字的区分。有这个区分的原因是 jni 是 C 语言实现的,但是 C 语言没有函数重载,他无法区分名字相同而参数不同的函数。而 C++ 有函数重载,编译后的函数名不是我们定义的函数名,额外附带了参数签名,所以如果不带extern "C",那么 C++ 编译器将 jni 函数编译成带参数签名的名字后,该函数就无法被找到了。

参考:掘金:Android-JNI开发

附件

Java 类型和描述符参照表

Java 类型描述符
booleanZ
byteB
charC
shortS
intI
longJ
floagF
doubleD
voidV
引用类型以 L 开头 ; 结尾,中间是 / 分隔的包名和类名。例如 String 的字段描述符为 Ljava/lang/String;

参考资料

  1. 掘金:NDK 系列(6):说一下注册 JNI 函数的方式和时机
  2. 谷歌开发者网站:JNI 提示
  3. JNI 从入门到实践
  4. stackoverflow:How can i implement two JNI methods with same name but different Params?
  5. oracle:jni design spec
  6. 掘金:Android-JNI开发