Java
是一种跨平台编程语言,JVM
虚拟机封装了Java
程序与底层硬件的交互逻辑,只要操作系统安装了Java
运行环境,Java
代码就可以一次编译到处运行。Java
的跨平台是优点但是有时候也会打破这种优点,比如当Java
想要调用操作系统特有的功能。
Java
的跨语言调用通过JNI(Java Native Interface)
来完成。
JNI 例子
我们以一个Java
调用c
的例子来了解JNI
的调用流程。实现Java
调用c
需要如下步骤:
- 编写
Java Native
接口并对代码进行编译。 - 根据步骤
2
的Java
代码生成c
头文件,头文件中包含接口的声明。 - 编写
c
代码,代码中引入步骤2
生成的头文件。 - 将步骤
3
的代码编译成为动态链接库。 - 在
Java
代码中调用动态链接库。
1. 编写Java Native代码
Java代码的绝对路径:
/Users/user/Documents/workSpace/microservice/doc/demo/demo-code/src/main/java/jni
。
在Java
代码中声明native
接口。
//包名
package jni;
/**
* Datetime: 2023/5/23
* Author: YanYong
*/
public class JniTest {
//本地方法接口
public native void helloWorld(String s);
}
将JniTest.java
编译成class
文件,生成的class
文件与JniTest.java
在同一个目录下。如下是编译指令:
javac /Users/user/Documents/workSpace/microservice/doc/demo/demo-code/src/main/java/jni/JniTest.java
2. 生成头文件
进入的/Users/user/Documents/workSpace/microservice/doc/demo/demo-code/src/main/java
目录下,然后执行java -classath . -jni 包名.类名称
指令,如下例子:
javah -classpath . -jni jni.JniTest
生产的头文件的名称规则是:包名_类名称.h
。如下是JniTest
生成的jni_JniTest.h
文件内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class jni_JniTest */
#ifndef _Included_jni_JniTest
#define _Included_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jni_JniTest
* Method: helloWorld
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_jni_JniTest_helloWorld
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
3. 编写c代码
前面的流程只是生成了c
代码的头文件,接下来就是引入jni_JniTest.h
头文件,并实现相关的方法:
#include <stdio.h>
#include "jni_JniTest.h"
JNIEXPORT void JNICALL Java_jni_JniTest_helloWorld
(JNIEnv *env, jobject thisObject, jstring str){
const char *strs = (*env)->GetStringUTFChars(env,str,0);
printf("%s\n",strs);
return;
}
如上Java_jni_JniTest_helloWorld
是接口helloWorld
的本地方法实现,它的名称命名规则是Java_包名_Java类名_native方法名
。在Java_jni_JniTest_helloWorld
方法上有三个参数:
env
:JNI
函数指针的集合,如果在c
代码中想要调用Java
的特性,可以通过env
中的函数指针来实现。thisObject
:this
对象。如果是静态方法,那么该方法的第二个参数是jclass
。str
:方法参数。
4. 生成动态库
c
代码完成后,通过gcc
命令将其编译成为动态链接库。不同的环境,生成的文件的后缀名不同。
mac
环境:生成dylib
结尾的文件
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -o demoJni.dylib -shared demo1.c
linux
环境:生成的是.so
文件。
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -o demoJni.so -shared demo1.c
注意这里几个gcc
的选项,-shared
是说明要生成动态库,而两个 -I
的选项,是因为我们用到<jni.h>
相关的头文件,放在<jdk>/include
和 <jdk>/include/linux
两个目录下。
5.Jni调用
System.load
加载动态链接库,参数是绝对地址。
public static void main(String[] args) {
try {
System.load("/Users/user/Documents/cspace/MyFirstProject/MyFirstProject/demoJni.dylib");
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
new JniTest().helloWorld("hello world ");
}
执行结果:hello world
.
JNI参数说明
Java 基本类型与C映射
JNI
会将Java
中的基本类型和引用类型与c
映射出一套可供c
代码调用的数据结构。
-
基本类型
Java基本类型 c数据结构 boolean jboolean byte jbyte char jchar short jshort int jint long jlong float jfloat double jdouble void void -
引用类型
jobject |- jclass (java.lang.Class) |- jstring (java.lang.String) |- jthrowable (java.lang.Throwable) |- jarray (arrays) |- jobjectArray (object arrays) |- jbooleanArray (boolean arrays) |- jbyteArray (byte arrays) |- jcharArray (char arrays) |- jshortArray (short arrays) |- jintArray (int arrays) |- jlongArray (long arrays) |- jfloatArray (float arrays) |- jdoubleArray (double arrays)
JNIEnv参数
在C
代码中可能需要获取Java
代码传递过来的参数或者Java
对象中的全局变量,这些功能通过JNI函数来实现。JVM
将JNI
函数的函数指针聚合到了JNIEnv
里。
JNIEnv
是一个线程私有的数据结构,JVM
会为每个线程创建一个JNIEnv
对象,并且c
代码不能将当前的JNIEnv
分享给其他线程。如下是JNIEnv
使用例子:
//java 代码
public native void helloWorld(String s);
int i = 0xDEADBEEF;
改造一下前面的c
例子代码,通过JNIEnv
里面的函数指针去获取 Java
对象中的变量i
:
#include <stdio.h>
#include "jni_JniTest.h"
JNIEXPORT void JNICALL Java_jni_JniTest_helloWorld
(JNIEnv *env, jobject thisObject, jstring str){
const char *strs = (*env)->GetStringUTFChars(env,str,0);
printf("%s\n",strs);
//获取class
jclass cls = (*env)->GetObjectClass(env, thisObject);
//获取class中变量i 的 fieldID
jfieldID fieldID = (*env)->GetFieldID(env, cls, "i", "I");
//通过fieldID获取变量i值
jint value = (*env)->GetIntField(env, thisObject, fieldID);
printf("Hello, World 0x%x\n", value);
return;
}
JNI
调用
public static void main(String[] args) {
try {
System.load("/Users/user/Documents/cspace/MyFirstProject/MyFirstProject/demoJni.dylib");
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
new JniTest().helloWorld("begin");
}
输出:
begin
Hello, World 0xdeadbeef
从结果可以看出,c
代码成功获取到了java
对象中的变量i
。更多关于函数指针请参考:https://docs.oracle.com/en/java/javase/11/docs/specs/jni/functions.html
附录
提示'jni_md.h' file not found
错误,拷贝一下缺少的文件。
sudo cp /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/include/darwin/jni_md.h /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/include