【译】Java Native Interface (JNI)
引言
查看关于 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 类
public native void sayHi(String who, int times);
- 载入实现该方法的类库
System.loadLibrary("HelloImpl");
- Java 调用本地方法
Demo
package com.marakana.jniexamples;
public class Hello {
public native void sayHi(String who, int times); // 1
// 2
static {
System.loadLibrary("HelloImpl");
}
public static void main (String[] args) {
Hello hello = new Hello();
hello.sayHi(args[0], Integer.parseInt(args[1])); // 3
}
}
其中:
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 文件如下:
...
#include <jni.h>
...
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi
(JNIEnv *, jobject, jstring, jint);
...
Hello.c 文件如下图:
#include <stdio.h>
#include "com_marakana_jniexamples_Hello.h"
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi(JNIEnv *env, jobject obj, jstring who, jint times) {
jint i;
jboolean iscopy;
const char *name;
name = (*env) -> GetStringUTFChars(env, who, &iscopy);
for (i = 0; i < times; i++) {
printf("Hello %s\n", name);
}
}
5.0 JNI 开发 (编译)
- 我们接下来编译并且运行它(不同的系统会有不同的编译结果)。
- 生成相应的类库 libHelloImpl.so, HelloImpl.dll, libHelloImpl.jnilib。
- 设置 LD_LIBRARY_PATH 为你类库保存的路径。
- 运行应用程序。
例如:为了编译类路径中的 com_marakana_jniexamples_Hello.c 文件(前提是你得确保 .h 以及 .c 文件在那)。
Linux
gcc -o libHelloImpl.so -lc -shared \
-I/usr/local/jdk1.6.0_03/include \
-I/usr/local/jdk1.6.0_03/include/linux com_marakana_jniexamples_Hello.c
macOS
gcc -o libHelloImpl.jnilib -lc -shared \
-I/System/Library/Frameworks/JavaVM.framework/Headers com_marakana_jniexamples_Hello.c
设置 LD_LIBRARY_PATH 环境变量。
export LD_LIBRARY_PATH=.
最后,运行你的应用程序。
java com.marakana.jniexamples.Hello Student 5
Hello Student
Hello Student
Hello Student
Hello Student
Hello Student
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:
package com.marakana.jniexamples;
public class HelloName {
public static native void sayHelloName(String name);
static {
System.loadLibrary("helloname");
}
public static void main (String[] args) {
HelloName hello = new HelloName();
String name = "John";
hello.sayHelloName(name);
}
}
- .h 文件看起来如下所示:
...
#include <jni.h>
...
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName
(JNIEnv *, jclass, jstring);
...
- 以下的 .c 文件并不会产生预期的结果:
#include <stdio.h>
#include "com_marakana_jniexamples_HelloName.h"
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name){
printf("Hello %s", name);
}
7.0 本地方法参数
- 所有的本地方法实现都接收两个标准参数:
- JNIEnv *env: 指向函数表(指针数组)的指针的指针。函数表中每个条目都指向一个 JNI 函数,我们可以使用这些函数进行类型转换。
- 第二个参数依赖于本地方法是静态方法还是实例方法而不同。
实例方法:它是一个 jobject 参数,该参数指向方法调用者对象。
静态方法:它是一个 jclass 参数,该参数指向方法定义所在的 class 对象。
8.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" 的本地方法:
#include <stdio.h>
#include "com_marakana_jniexamples_HelloName.h"
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name) {
printf("Hello %s", name); // 1
}
因为 jstring 类型代表的是 Java 虚拟机中的字符串类型,而跟 C 中的字符串类型 (char *) 是不同的,所以这个例子不会按照预期运行。。
- 以下是你需要做的,使用 UTF-8 string:
#include <stdio.h>
#include "com_marakana_jniexamples_HelloName.h"
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name){
const jbyte *str;
str = (*env)->GetStringUTFChars(env, name, NULL); // 1
printf("Hello %s\n", str);
(*env)->ReleaseStringUTFChars(env, name, str); // 2
}
1 它返回一个指向代表 UTF-8 编码的字符串字节数组的指针(并没有产生内存复制)。
2 当我们并没有发生字符串复制的时候,调用 ReleaseStringUTFChars 函数可以防止字符串使用的内存区域保持固定状态。如果数据被复制,我们需要调用 ReleaseStringUTFChars 去释放那些不再使用的内存。
- 这是另外一个例子,用于构造以及返回一个 java.lang.String 字符串实例:
#include <stdio.h>
#include "com_marakana_jniexamples_GetName.h"
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<Type>ArrayRegion:复制原始数组的内容到预分配的 C 缓冲区中。当数组大小已知的情况下,该方法很好用。
- Get<Type>ArrayElements:获取指向原始数组的指针。
- New<Type>Array: 创建一个指定大小的数组。
- 我们接下来看一个如何在本地环境中读取 Java 原始数组的例子。
- 首先,看看 Java 程序:
package com.marakana.jniexamples;
public class ArrayReader {
private static native int sumArray(int[] arr); // 1
public static void main(String[] args) {
// Array declaration
int arr[] = new int[10];
// Fill the array
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
ArrayReader reader = new ArrayReader();
// Call native method
int result = reader.sumArray(arr); // 2
System.out.println("The sum of every element in the array is " + Integer.toString(result));
}
static {
System.loadLibrary("arrayreader");
}
}
1 2 这个方法将返回数组中元素的总和。
- 运行 javah 之后,创建你的 .c 文件,如下图:
#include <stdio.h>
#include "com_marakana_jniexamples_ArrayReader.h"
JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_ArrayReader_sumArray(JNIEnv *env, jclass class, jintArray array) {
jint *native_array;
jint i, result = 0;
native_array = (*env) -> GetIntArrayElements(env, array, NULL); // 1
if (native_array == NULL) {
return 0;
}
for (i = 0; i < 10; i++) {
result += native_array[i];
}
(*env) -> ReleaseIntArrayElements(env, array, native_array, 0);
return result;
}
1 由于我们恰恰知道数组的大小,所以我们也可以使用 GetIntArrayRegion 函数
10 在本地世界中抛出异常
- 我们将看到如何在本地世界抛出一个异常
- 从本地世界抛出异常需要以下几个步骤:
- 找到你想抛出异常的类
- 抛出一个异常
- 删除异常类的本地引用
- 我们可以想象出这样一个实用函数:
void ThrowExceptionByClassName(JNIEnv *env, const char *name, const char *message) {
jclass class = (*env) -> FindClass(env, name); // 1
if (class != NULL) {
(*env) -> ThrowNew(env, class, message); // 2
}
(*env) -> DeleteLocalRef(env, class); // 3
}
1 通过名字找到该异常类
2 使用我们之前获得的类引用和异常信息抛出异常
3 删除异常类的本地引用
- 以下是如何使用此程序的方法
ThrowExceptionByClassName(env,"java/lang/IllegalArgumentException","This exception is thrown from C code");
11 从本地代码访问属性和方法
- 你可能想要通过调用本地代码来修改一些属性或者实例的调用方法
- 总会是围绕这几个操作开始:通过调用 GetObjectClass 方法获取指向对象的引用。
- 接着通过使用 GetFieldID 或者 GetMethodID 方法从 class 引用获取实例的字段 id 或者实例方法 id
- 最后,不同的地方依赖于我们访问的是一个字段还是一个方法
- 从这个 Java 类中,我们将会看到如何在本地代码中调用它的方法或者访问它的属性
package com.marakana.jniexamples;
public class InstanceAccess {
public String name; // 1
public void setName(String name) { // 2
this.name = name;
}
// Native method
public native void propertyAccess(); // 3
public native void methodAccess(); // 4
public static void main(String args[]) {
InstanceAccess instanceAccessor = new InstanceAccess();
// Set the initial value of the name property
instanceAccessor.setName("Jack");
System.out.println("Java: value of name = \""+ instanceAccessor.name +"\"");
// Call the propetyAccess() method
System.out.println("Java: calling propertyAccess() method...");
instanceAccessor.propertyAccess(); // 5
// Value of name after calling the propertyAccess() method
System.out.println("Java: value of name after calling propertyAccess() = \""+ instanceAccessor.name +"\"");
// Call the methodAccess() method
System.out.println("Java: calling methodAccess() method...");
instanceAccessor.methodAccess(); // 6
System.out.println("Java: value of name after calling methodAccess() = \""+ instanceAccessor.name +"\"");
}
// Load library
static {
System.loadLibrary("instanceaccess");
}
}
1 name 属性会在代码执行的时候被修改
2 该方法在本地代码修改 name 属性的时候被调用
3 5 本地方法通过直接访问 name 属性的方式对其进行修改
4 6 本地方法通过调用 Java setName() 方法对 name 属性进行修改
- 以下就是我们用来本地执行的 C 代码
#include <stdio.h>
#include "com_marakana_jniexamples_InstanceAccess.h"
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_propertyAccess(JNIEnv *env, jobject object){
jfieldID fieldId;
jstring jstr;
const char *cString;
/* Getting a reference to object class */
jclass class = (*env) -> GetObjectClass(env, object); /* 1 */
/* Getting the field id in the class */
fieldId = (*env) -> GetFieldID(env, class, "name", "Ljava/lang/String;"); /* 2 */
if (fieldId == NULL) {
return; /* Error while getting field id */
}
/* Getting a jstring */
jstr = (*env) -> GetObjectField(env, object, fieldId); /* 3 */
/* From that jstring we are getting a C string: char* */
cString = (*env) -> GetStringUTFChars(env, jstr, NULL); /* 4 */
if (cString == NULL) {
return; /* Out of memory */
}
printf("C: value of name before property modification = \"%s\"\n", cString);
(*env) -> ReleaseStringUTFChars(env, jstr, cString);
/* Creating a new string containing the new name */
jstr = (*env) -> NewStringUTF(env, "Brian"); /* 5 */
if (jstr == NULL) {
return; /* Out of memory */
}
/* Overwrite the value of the name property */
(*env) -> SetObjectField(env, object, fieldId, jstr); /* 6 */
}
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_methodAccess(JNIEnv *env, jobject object){
jclass class = (*env) -> GetObjectClass(env, object); /* 7 */
jmethodID methodId = (*env) -> GetMethodID(env, class, "setName", "(Ljava/lang/String;)V"); /* 8 */
jstring jstr;
if (methodId == NULL) {
return; /* method not found */
}
/* Creating a new string containing the new name */
jstr = (*env) -> NewStringUTF(env, "Nick"); /* 9 */
(*env) -> CallVoidMethod(env, object, methodId, jstr); /* 10 */
}
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 作为参数传递给它