1. JNI 简介
众所周知,Java 的主要优势之一是它的可移植性,这意味着一旦我们编写并且编译了代码,这个过程的结果就是不依赖于平台的字节码。它可以像我们预期的那样运行在任何能够运行 Java 虚拟机的机器或设备上。
但是,有时我们确实需要使用一些为某些特定架构而进行本地编译的原生代码。例如:
- 需要对硬件执行某些操作
- 对性能要求非常苛刻
- 想要重用的现有库,而不是用 Java 重写它。
为了实现这一点,JDK 在我们的 JVM 中运行的字节码和原生代码(通常用 C 或 C++ 编写)之间搭建了一座桥梁。该桥梁就称为Java Native Interface。
2. JNI 如何工作
Java 提供了 native 关键字,用于指明该方法的实现将由原生代码提供。native 关键字将我们的方法转换为一种抽象方法:
private native void aNativeMethod();
在这里,这个方法的实现不是由另一个 Java 类实现,而是在一个分离的原生动态共享库中实现。它将在内存中构造一个表,其中包含指向我们所有原生方法实现的指针,以便可以从 Java 代码中调用它们。
让 JNI 工作起来所需要的一些关键组件如下:
- Java 代码 - 我们的类,它将至少包含一种本地方法。
- 原生代码 - 我们原生代码的实际逻辑,通常使用 C 或者 C++ 代码。
- JNI 头文件 - 这个 C/C++ 的头文件 (jni.h),包括了我们可以在原生程序中使用的所有JNI 元素。
- C/C++ 编译器 - 用于为我们的平台生成原生共享库。
代码中的 JNI 组件包括了 Java 和 C/C++ 代码。
Java 代码:
- "native" 关键字 - 标记为 native 的方法都必须在原生共享库中实现。
- System.loadLibrary(String libname) - 一种静态方法,用于将共享库从文件系统加载到内存中,并使其包含的函数可用于我们的 Java 代码。
C/C++ 代码:
- JNIEXPORT - 将共享库中的函数标记为可导出,它将包含在函数表中,因此 JNI 可以找到它。
- JNICALL - 与 JNIEXPORT 结合使用,确保我们的方法可用于 JNI 框架。
- JNIEnv - 一个包含方法的结构,可以使用我们的原生代码访问 Java 元素。
- JavaVM - 一种让我们可以操纵正在运行的 JVM(甚至启动一个新的 JVM)的结构,向它添加线程、销毁它等等。
3. 编写 hello world JNI
3.1 创建 Java 类
受限编写我们的 Java 代码 HelloWorldJNI.java,具体如下,此类中用 native 关键字定义了需要 C/C++ 实现的原生方法 sayHello()
public class HelloWorldJNI {
static {
System.loadLibrary("native");
}
public static void main(String[] args) {
new HelloWorldJNI().sayHello();
}
// 定义原生sayHello()方法
private native void sayHello();
}
3.2 通过 Java 类生成 C/C++ 所需的头文件
通过 Java 类自动生成 sayHello() 方法的定义,并保存在 HelloWorldJNI.h 头文件中
javac -h . HelloWorldJNI.java
自动生成的 HelloWorldJNI.h 的头文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorldJNI */
#ifndef _Included_HelloWorldJNI
#define _Included_HelloWorldJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorldJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
函数名是使用完全限定的包名、类名和方法名自动生成的。这个函数有两个参数,1 个是指向当前 JNIEnv 的指针,另一个是该方法附加到的 Java 对象,HelloWorldJNI 类的实例。
3.3 编写 C/C++ 文件,完成 sayHello() 的函数实现。
需要为 sayHello 函数的实现创建一个新的 c/cpp 文件,文件里面包含了函数的具体实现。将此文件和 .h 使用相同的命名。
以 C 代码举例,HelloWorldJNI.c 的具体实现如下:
#include <stdio.h>
#include "HelloWorldJNI.h"
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
(JNIEnv* env, jobject thisObject) {
printf("hello world!\n");
}
3.4 编译 C 代码,生成共享库
我们已经实现了所有代码的编写,接下来需要从 C 代码编译成共享库,将共享库命名为 libnative.so 。
[root@wuhan hello]# export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.b07-2.el8_5.x86_64/
[root@wuhan hello]# gcc -shared -fPIC -o libnative.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorldJNI.c -lc
目前为止,我们当前的文件夹底下应该包含了如下文件:
[root@wuhan hello]# ls
HelloWorldJNI.c HelloWorldJNI.class HelloWorldJNI.h HelloWorldJNI.java libnative.so
3.5 运行 Java 程序
最后,运行我们的 Java 程序,即可获得原生代码的输出 "hello world!"
[root@wuhan hello]# java -cp . -Djava.library.path=. HelloWorldJNI
hello world!
4. 定义带参数和返回值的函数方法
只调用原生的 C 代码并打印 "hello world!" 肯定是不能满足编码要求的。函数需要有非空的入参和返回值才能处理更多的事情。
在 HelloWorldJNI 类里面新增一个 sumIntegers 的方法,有两个 int 入参和一个 long 返回值。
public class HelloWorldJNI {
static {
System.loadLibrary("native");
}
public static void main(String[] args) {
new HelloWorldJNI().sayHello();
long sum = new HelloWorldJNI().sumIntegers(10, 20);
System.out.println("sum:" + sum);
}
// 定义原生sayHello()方法
private native void sayHello();
// 定义原生sumIntegers方法,返回值是long,有两个int入参
private native long sumIntegers(int first, int second);
}
查看生成的头文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorldJNI */
#ifndef _Included_HelloWorldJNI
#define _Included_HelloWorldJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorldJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
(JNIEnv *, jobject);
/*
* Class: HelloWorldJNI
* Method: sumIntegers
* Signature: (II)J
*/
JNIEXPORT jlong JNICALL Java_HelloWorldJNI_sumIntegers
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
C 代码新增 Java_HelloWorldJNI_sumIntegers 的实现:
#include <stdio.h>
#include "HelloWorldJNI.h"
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
(JNIEnv* env, jobject thisObject) {
printf("hello world!\n");
}
JNIEXPORT jlong JNICALL Java_HelloWorldJNI_sumIntegers
(JNIEnv *env, jobject thisObject, jint first, jint second) {
printf("received first:%d second:%d\n", first, second);
long sum = (long)first + (long)second;
return sum;
}
同样的编译运行步骤,可以得到 sumIntegers 的运行结果。
[root@wuhan hello]# java -cp . -Djava.library.path=. HelloWorldJNI
hello world!
received first:10 second:20
sum:30
可以在 Oracle 官方文档中查看 Java 类型和等价的 C JNI 类型。Java Native Interface Specification: 3 - JNI Types and Data Structures
5. 原生代码调用 Java 方法
Java 不仅能够调用原生代码,同样的,原生代码也可以调用 Java 方法。
首先先新建一个 UserData Java 类,这个类里定义的方法就是原生代码需要调用的。
public class UserData {
public String name;
public String getUserName() {
return name;
}
}
再将 HelloWorldJNI 类改造一下,增加 createUser 和 printUserName 两个原生方法。在 main 里面调用 printUserName。
public class HelloWorldJNI {
static {
System.loadLibrary("native");
}
public static void main(String[] args) {
new HelloWorldJNI().sayHello();
long sum = new HelloWorldJNI().sumIntegers(10, 20);
System.out.println("sum:" + sum);
HelloWorldJNI instance = new HelloWorldJNI();
UserData newUser = instance.createUser("LeBron James");
instance.printUserName(newUser);
}
// 定义原生sayHello()方法
private native void sayHello();
// 定义原生sumIntegers方法,返回值是long,有两个int入参
private native long sumIntegers(int first, int second);
// 定义原生createUser方法
public native UserData createUser(String name);
//定义原生printUserName方法
public native String printUserName(UserData user);
}
在 HelloWorldJNI.c 中添加原生方法 createUser 和 printUserName 的实现。
#include <stdio.h>
#include "HelloWorldJNI.h"
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
(JNIEnv* env, jobject thisObject) {
printf("hello world!\n");
}
JNIEXPORT jlong JNICALL Java_HelloWorldJNI_sumIntegers
(JNIEnv *env, jobject thisObject, jint first, jint second) {
printf("received first:%d second:%d\n", first, second);
long sum = (long)first + (long)second;
return sum;
}
JNIEXPORT jobject JNICALL Java_HelloWorldJNI_createUser
(JNIEnv *env, jobject thisObject, jstring myName){
// 创建 UserData 类对象
jclass userDataClass = (*env)->FindClass(env, "UserData");
jobject newUserData = (*env)->AllocObject(env, userDataClass);
// 获取需要UserData类的name成员字段
jfieldID nameField = (*env)->GetFieldID(env, userDataClass , "name", "Ljava/lang/String;");
// 给UserData类的name成员赋值
(*env)->SetObjectField(env, newUserData, nameField, myName);
// 返回创建的对象
return newUserData;
}
JNIEXPORT jstring JNICALL Java_HelloWorldJNI_printUserName
(JNIEnv *env, jobject thisObject, jobject userData){
// 获取createUser创建的userData类
jclass userDataClass = (*env)->GetObjectClass(env, userData);
// 获取getUserName方法id
jmethodID methodId = (*env)->GetMethodID(env, userDataClass, "getUserName", "()Ljava/lang/String;");
// 调用getUserName方法,获取返回值,保存到result
jstring result = (jstring)(*env)->CallObjectMethod(env, userData, methodId);
// 打印getUserName获取的返回值
printf("My name is: %s\n", (*env)->GetStringUTFChars(env, result, NULL));
return result;
}
生成头文件,编译共享库,运行 Java 程序,可以得到如下结果:
[root@wuhan hello]# javac -h . HelloWorldJNI.java
[root@wuhan hello]# gcc -shared -fPIC -o libnative.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorldJNI.c -lc
[root@wuhan hello]# java -cp . -Djava.library.path=. HelloWorldJNI
hello world!
received first:10 second:20
sum:30
My name is: LeBron James
在获取类成员或调用类方法的时候,最后一个参数 "Ljava/lang/String;" 或 "()Ljava/lang/String;" 是参数或者方法的签名。签名的格式如图所示:
JNI 的函数使用说明可参考 Java Native Interface Specification: 4 - JNI Functions