引言
查看关于 JNI 相关资料的时候,不巧碰到了这篇文章,通篇读了一下感觉写的很不错,所以拿过来翻译了一下,由于翻译的比较快,有些细节方面的工作可能做的不是很到位,不过后期会进行相关修正,这里先放上来尝尝鲜。
如果你也对 JNI 比较感兴趣,并且打算深入学习,那么本文将会是一个不错的选择。
1.0 JNI 概述
- 什么是 JNI ?
JNI 是 Java 与其他语言交互的一个桥梁。 - 为什么要有 JNI ?
- 代码的可重用性
- 用 Java 重用现有的以及一些较老的代码(更多是用 C/C++ 编写的)。
- 性能
- 解释模式下,本地代码的速度最快于 Java 20 倍左右。
- 现代的即使编译器(HotSpot)使其成为一个争议点。
- 可以使 Java 调用一些底层的进程,如:O/S, H/W。
- 代码的可重用性
- JNI 不可移植
📚 Tips
JNI 还可以用于本地编写的程序(如:C/C++)调用 Java 代码;
比如 Java 的命令行工具(Java 虚拟机启动 Java 代码)。
2.0 JNI 组件
- javah 将包含 native 方法的 Java 类编译成 C 风格头文件的 JDK 工具
将 Java 方法签名转化为本地函数原型。 - jni.h JDK 包含的 C/C++ 头文件,用于将 Java 类型映射为本地对应的类型
javah 会自动引入该文件到应用程序的头文件中。
3.0 JNI 开发 (Java)
- 创建一个包含 native 方法的 Java 类
1
public native void sayHi(String who, int times);
- 载入实现该方法的库
1
System.loadLibrary("HelloImpl");
- Java 调用本地方法
Demo
1 | package com.marakana.jniexamples; |
其中:
1、3 C/C++ 将实现 sayHi 方法,并且编译成库文件
2 库的名称:
- Linux
libHelloImpl.so - Windows
HelloImpl.dll - macOS
libHelloImpl.jnilib
注意:
Java 载入的库叫 HelloImpl。
4.0 JNI 开发(C)
- 我们用 JDK 提供的工具 javah 生成包含 sayHi 方法原型的头文件 package_name_classname.h。
- 编译生成 class 文件
javac -d ./classes/ ./src/com/marakana/jniexamples/Hello.java - 生成 com_marakana_jniexamples_Hello.h 头文件
javah -jni com.marakana.jniexamples.Hello
- 编译生成 class 文件
- 我们接着创建 com_marakana_jniexamples_Hello.c 来实现 Java_com_marakana_jniexamples_Hello_sayHi 函数。
com_marakana_jniexamples_Hello.h 文件如下:
1 |
|
Hello.c 文件如下图:
1 |
|
5.0 JNI 开发 (编译)
- 我们接下来编译并且运行它(不同的系统会有不同的编译结果)。
- 生成相应的库 libHelloImpl.so, HelloImpl.dll, libHelloImpl.jnilib。
- 设置 LD_LIBRARY_PATH 为你库保存的路径。
- 运行应用程序。
例如:为了编译类路径中的 com_marakana_jniexamples_Hello.c 文件(前提是你得确保 .h 以及 .c 文件在那)。
Linux
1 | gcc -o libHelloImpl.so -lc -shared \ |
macOS
1 | gcc -o libHelloImpl.jnilib -lc -shared \ |
设置 LD_LIBRARY_PATH 环境变量。
1 | export LD_LIBRARY_PATH=. |
最后,运行你的应用程序。
1 | java com.marakana.jniexamples.Hello Student 5 |
📚 Tips
比较常见的问题是 java.lang.UnsatisfiedLinkError 错误,而导致该错误的一般问题是共享库名称的错误、库并没有在指定的搜索路径上或者 Java 代码载入了错误的库。
6.0 类型转换
- 一般情况,程序需要向本地方法传递参数以及接收本地方法的返回值。
- Java 中存在两种类型:
原始类型,如:int、float、char 等。
引用类型,如:数组、字符串、实例、Classes 对象等。 - 然而,原始类型与引用类型在 JNI 中有不同的处理方式。
- 在 JNI 中映射原始类型比较简单。
Table 3. JNI 数据类型映射
Java 类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | 8 bits, unsigned |
byte | jbyte | 8 bits, signed |
char | jchar | 16 bits, unsigned |
double | jdouble | 64 bits |
float | jfloat | 32 bits |
int | jint | 32 bits, signed |
long | jlong | 64 bits, signed |
short | jshort | 16 bits, signed |
void | void | N/A |
- 映射对象类型会更加复杂一点。这里我们主要关注字符串以及数组类型。不过不要着急,在我们深入探讨之前,先让我们看看本地方法的参数们。
- JNI 把这些对象作为不透明引用传递给本地方法。
- 不透明引用是 C 指针的一种类型,它指向 JVM 内部的数据结构
让我们考虑以下 Java class:
1 | package com.marakana.jniexamples; |
- .h 文件看起来如下所示:
1 |
|
- 以下的 .c 文件并不会产生预期的结果:
1
2
3
4
5
6
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name){
printf("Hello %s", name);
}7.0 本地方法参数
- 所有的本地方法实现都接收两个标准参数:
- 我们刚刚讨论的 JNIEnv *env 将会作为我们接下来找到的类型转换方法的参数来使用。
- 有很多字符串相关的方法:
- 一些方法将 java.lang.String 转化为 C 字符串,如:GetStringChars (Unicode format), GetStringUTFChars (UTF-8 format)
- 一些方法转换 java.lang.String 为 C 字符串,如:NewString (Unicode format), NewStringUTF (UTF-8 format)
- 一些方法用来释放 C 字符串内存,如:ReleaseStringChars, ReleaseStringUTFChars
📚 Tips
详细的内容可以参考:http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/functions.html
- 不知你是否还记得前一个例子,那是一个用来显示 “Hello name” 的本地方法:
1 |
|
因为 jstring 类型代表的是 Java 虚拟机中的字符串类型,而跟 C 中的字符串类型 (char *) 是不同的,所以这个例子不会按照预期运行。。
- 以下是你需要做的,使用 UTF-8 string:
1 |
|
1 它返回一个指向代表 UTF-8 编码的字符串字节数组的指针(并没有产生内存复制)。
2 当我们并没有发生字符串复制的时候,调用 ReleaseStringUTFChars 函数可以防止字符串使用的内存区域保持固定状态。如果数据被复制,我们需要调用 ReleaseStringUTFChars 去释放那些不再使用的内存。
- 这是另外一个例子,用于构造以及返回一个 java.lang.String 字符串实例:
1
2
3
4
5
6
7
8
JNIEXPORT jstring JNICALL Java_com_marakana_jniexamples_ReturnName_GetName(JNIEnv *env, jclass class) {
char buffer[20];
scanf("%s", buffer);
return (*env)->NewStringUTF(env, buffer);
}- 我们接下来将焦点放到原始数组上,因为它们与 JNI 中的对象数组不同。
- 数组在 JNI 中由 jarray 引用类型及其“子类型”(例如 jintArray)表示。注意 jarray 并不是 C 数组!
- 我们将要再一次使用 JNIEnv *env 参数访问类型转换方法
- Get
ArrayRegion:复制原始数组的内容到预分配的 C 缓冲区中。当数组大小已知的情况下,该方法很好用。 - Get
ArrayElements:获取指向原始数组的指针。 - New
Array: 创建一个指定大小的数组。
- Get
- 我们接下来看一个如何在本地环境中读取 Java 原始数组的例子。
- 首先,看看 Java 程序:
1 | package com.marakana.jniexamples; |
1 2 这个方法将返回数组中元素的总和。
- 运行 javah 之后,创建你的 .c 文件,如下图:
1 |
|
1 由于我们恰恰知道数组的大小,所以我们也可以使用 GetIntArrayRegion 函数
10 在本地世界中抛出异常
- 我们将看到如何在本地世界抛出一个异常
- 从本地世界抛出异常需要以下几个步骤:
- 找到你想抛出异常的类
- 抛出一个异常
- 删除异常类的本地引用
- 我们可以想象出这样一个实用函数:
1 | void ThrowExceptionByClassName(JNIEnv *env, const char *name, const char *message) { |
1 通过名字找到该异常类
2 使用我们之前获得的类引用和异常信息抛出异常
3 删除异常类的本地引用
- 以下是如何使用此程序的方法
1 | ThrowExceptionByClassName(env,"java/lang/IllegalArgumentException","This exception is thrown from C code"); |
11 从本地代码访问属性和方法
- 你可能想要通过调用本地代码来修改一些属性或者实例的调用方法
- 总会是围绕这几个操作开始:通过调用 GetObjectClass 方法获取指向对象的引用。
- 接着通过使用 GetFieldID 或者 GetMethodID 方法从 class 引用获取实例的字段 id 或者实例方法 id
- 最后,不同的地方依赖于我们访问的是一个字段还是一个方法
- 从这个 Java 类中,我们将会看到如何在本地代码中调用它的方法或者访问它的属性
1 | package com.marakana.jniexamples; |
1 name 属性会在代码执行的时候被修改
2 该方法在本地代码修改 name 属性的时候被调用
3 5 本地方法通过直接访问 name 属性的方式对其进行修改
4 6 本地方法通过调用 Java setName() 方法对 name 属性进行修改
- 以下就是我们用来本地执行的 C 代码
1 |
|
1 7 获取 class 对象的引用
2 从 class 对象中获取字段 Id,以及指定要获取的属性以及内部类型。可以从以下链接中获取关于 jni 类型的信息:http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/types.html
3 这里将会返回本地类型中的属性值 jstring
4 我们需要将 jstring 类型转换为 C 中的字符串
5 这里会创建出一个新的 java.lang.String 类型用以修改属性的值
6 将新的值设置给该属性
8 从先前获取到的 class 对象中通过方法的名称以及签名获取方法 id 。这里有一个用来获取方法签名的实用工具:javap -s -p ClassName for instance javap -s -p InstanceAccess
9 创建出一个新的 java.lang.String 对象作为从本地代码调用 java 方法的参数。
10 由于 Java 方法返回值类型为 void,所以调用 CallVoidMethod 方法,并且将先前创建出的 jstring 作为参数传递给它