标签 JNI 下的文章

在Java中经常会遇到通过外部语言扩展Java本身的需要,此时就需要使用到JNI这门技术(规范)。
在c/c++中,java层的数据类型需要转化成类型签名,如在google中被收录在最前边的[官方文档](https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html)中[Table 3-2](https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html#wp276)所示。

问题来了,那么void类型在c/c++用什么来映射呢。这份表格里边是没有的(对应java 7的[JNI规范里](http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html#wp16432)边也没有)。今天在[其他文档](http://www.rgagnon.com/javadetails/java-0286.html)里边间接查到void类型的类型签名是V:

```bash
Type Chararacter
boolean Z
byte B
char C
double D
float F
int I
long J
object L
short S
void V
array [
```
比如Java方法:
```java
public static void jsDebugDetachCallback(long udata)
```
的类型签名为(其中返回值类型V不可省略):
```java
"(J)V"
```
在c中获取该方法id的方式为:
```c
jsDebugDetachCallbackMethod = (*env)->GetStaticMethodID(env, someClz, "jsDebugDetachCallback", "(J)V");
```

在一些复杂的JNI调用中,比如JNI调用Java层的对象、Java层又调native方法,嵌套过多了,某一次调用产生的异常会在下一次调用JNI时被check出来,
这时候会产生如下日志:

```bash
01-13 21:22:43.247 24613-24613/com.somepkg A/art: art/runtime/check_jni.cc:65] JNI DETECTED ERROR IN APPLICATION: JNI NewByteArray called with pending exception 'java.lang.NullPointerException' thrown in unknown throw location
01-13 21:22:43.247 24613-24613/com.somepkg A/art: art/runtime/check_jni.cc:65] in call to NewByteArray
```

这种问题都是在Java代码中产生了异常,但是并不是所有case下都能一眼通过逻辑判断是哪行,这时候有个JNIEnv的方法能帮上我们大忙:
```c
jthrowable thr = (*env)->ExceptionOccurred(env);

if (thr) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
}
```

c++版本:
```c++
void CheckException(JNIEnv* env) {
if (!HasException(env)) return;

// Exception has been found, might as well tell breakpad about it.
jthrowable java_throwable = env->ExceptionOccurred();
if (!java_throwable) {
// Do nothing but return false.
CHECK(false);
}

// Clear the pending exception, since a local reference is now held.
env->ExceptionDescribe();
env->ExceptionClear();

// Set the exception_string in BuildInfo so that breakpad can read it.
// RVO should avoid any extra copies of the exception string.
base::android::BuildInfo::GetInstance()->set_java_exception_info(
GetJavaExceptionInfo(env, java_throwable));

// Now, feel good about it and die.
CHECK(false);
}
```

在被checkjni侦测到异常的代码(比如上例中是NewByteArray)之前加上如上代码就可以将Java层的异常信息给优雅地打印出来,从而精准定位问题。

另一种定位此类问题的方式是打开Android设备的CheckJni功能。但是一般production设备上都不太容易实现。所以强力推荐以上方法。

android在Java层对 utf 编码是支持得很好了,非常全面;但当你从事一些c/c++工程的开发时可就没有这么幸运了。
笔者最近在使用v8 javascript 引擎时便碰到了一个问题:

有些用户在昵称中使用了 emoji 表情,v8 引擎内部默认会使用utf-16编码,通过 v8 API 取到这个值之后转为utf-8的字符串,进而通过 JNI 的 API JNIEnv->NewStringUTF 往 Java 传递时会被系统的 checkJNI 给拦截住而报错。原因在为了让字符串中不包含任何 null 字节,JNI 以及 Java VM 内部都是使用的Modified UTF-8格式来编码字符串。

后来找到一个办法可以通过将字符串转为 utf-16 编码后传递给 JNI API JNIEnv->NewString 解决之:

  1. 使用 v8 API 将 utf-8 的字符串转为 utf-16 编码

    size_t utf8_to_utf16(const char *src, const uint16_t **dest) {
     if (src == NULL || dest == NULL) {
         return 0;
     }
     Isolate::Scope scope(Isolate::GetCurrent());
     HandleScope handle_scope;
     Local<String> str = String::New(src);
     String::Value val(str); // String::Value的内部编码是 utf-16
     const size_t len = (val.length()) * sizeof(uint16_t);
     uint16_t* target = (uint16_t*)calloc(val.length() + 1, sizeof(uint16_t));
     if (target == NULL) {
         return 0;
     }
     memcpy(target, *val, len);
     *dest = target;
     return val.length();
    }
  2. 将生成的 utf-16 字符串通过 JNI 传递到 Java 层

    uint16_t *utf16_action = NULL;
    size_t len = utf8_to_utf16(action, &utf16_action);
    jstring jText = (*env)->NewString(env, utf16_action, len);
    if (utf16_action != NULL) {
     free(utf16_action);
    }
    if (len == 0) {
     jText = (*env)->NewStringUTF(env, ""); // 降级到使用NewStringUTF来创建一个""字符串
    }

另外一种解决方法是通过 byteArray 来将 utf-8 编码的字符串传送到java层,详情请参考这篇文章