Java JNI 学习笔记
JNI(Java Native Interface)是 Java 提供的一种接口,使得 java 代码可以与其他语言(如 C 和 C++)编写的代码进行交互。具体来说,JNI 允许你在 Java 中调用本地(Native)代码,或者从本地代码调用 Java 方法。
基本概念
jni.h
:这是 JNI 的头文件,使用javac
生成,定义了 JNI 的接口函数和数据结构jni.cpp
:这是实现了本地方法的 C++ 文件,包含了具体的本地代码jni.so
:这是生成的共享库文件(在 windows 上为.dll
文件),java 程序通过它调用本地方法
Demo:java 调用 C++ 代码
编写 java 类
创建一个 Java 类并声明一个本地方法。
// HelloJNI.java
public class HelloJNI {
// 加载本地库
static {
System.loadLibrary("hello"); // Load native library at runtime
// hello.dll (Windows) or libhello.so (Unixes)
}
// 声明一个本地方法
private native void sayHello();
// 主方法
public static void main(String[] args) {
new HelloJNI().sayHello(); // 调用本地方法
}
}
上面代码的静态代码块在这个类被类加载器加载的时候调用了 System.loadLibrary
库来加载一个 native 库 “hello”,这个库实现了 sayHello
函数。接下来,我们使用 native 关键字将 sayHello()
方法声明为本地实例方法。注意,一个 native 方法不包含方法体,只有声明。上面代码中的 main 方法实例化了一个 HelloJJNI 类的实例,然后调用了本地方法 sayHello()
。
生成 C++ 头文件
编译 Java 类并使用 javac
生成 JNI 所需要的 C++ 头文件。
# 编译 Java 类并生成 C++ 头文件
javac -h . HelloJNI.java
此时会生成一个名为 HelloJNI.h
的头文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
# include <jni.h>
/* Header for class HelloJNI */
# ifndef _Included_HelloJNI
# define _Included_HelloJNI
# ifdef __cplusplus
extern "C" {
# endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
# ifdef __cplusplus
}
# endif
# endif
上面的头文件生成了一个 Java_HelloJNI_sayHello
的 C 函数:
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
将 java 的 native 方法转换成 C 函数声明的规则是这样的:Java_{package_and_classname}_{function_name}(JNI arguments)
。包名中的点换成单下划线。需要说明的是生成函数中的两个参数:
JNIEnv *
:这是一个指向 JNI 运行环境的指针,我们可以通过这个指针访问 JNI 函数jobject
:指代 java 中的 this 对象
头文件中有一个 extern “C”,同时上面还有 C++ 的条件编译语句,这里的函数声明是要告诉 C++ 编译器:这个函数是 C 函数,请使用 C 函数的签名协议规则去编译!因为我们知道 C++ 的函数签名协议规则和 C 的是不一样的,因为 C++ 支持重写和重载等面向对象的函数语法。
编写 C++ 实现
创建一个 C++ 文件,实现头文件中声明的本地方法。
// HelloJNI.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"
// 实现本地方法:注意函数名称需要和头文件中的函数名称保持一致
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj) {
std::cout << "Hello from C++!" << std::endl;
}
编译 C++ 代码生成共享库
将 C++ 文件编译为共享库文件:
# Linux/macOS
g++ -shared -fpic -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloJNI.cpp
g++ -shared -fpic -o libllm_jni.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux llm_jni.cpp
# Windows
g++ -shared -o hello.dll -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" HelloJNI.cpp
运行 Java 程序
确保共享库文件位于 Java 的库路径中,然后运行 java 程序:
# 运行 Java 程序
java -Djava.library.path=. -cp . HelloJNI
# -Djava.library.path=. : 指定 java 查找本地库的路径
# -cp . :设置 java 的类路径(class path),即查找 java 类文件的路径
执行上述命令后,你应该会看到输出:
Hello from C++!
在 java 和 Native 代码之间传递参数和返回值
传递基本类型
传递 java 的基本类型是非常简单而直接的,一个 jxxx 之类的类型已经定义在本地系统中了,比如:jint,jbyte,jshort,jlong,jfloat,jdouble,jchar 和 jboolean 分别对应 java 的 int,byte,short,long,float,double,char 和 boolean 基本类型。
Java JNI 程序:
public class TestJNIPrimitive {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Declare a native method average() that receives two ints and return a double containing the average
private native double average(int n1, int n2);
// Test Driver
public static void main(String args[]) {
System.out.println("In Java, the average is " + new TestJNIPrimitive().average(3, 2));
}
}
生成头文件:javac -h . TestJNIPrimitive.java
头文件 TestJNIPrimitive.h 中包含了一个函数声明:
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);
可以看到,这里的 jint 和 jdouble 分别表示 java 中的 int 和 double。
TestJNIPrimitive.cpp
的实现如下:
// TestJNIPrimitive.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *env, jobject obj, jint n1, jint n2) {
std::cout << "In C++, the numbers are " << n1 << " and " << n2 << std::endl;
jdouble result;
result = ((jdouble)n1 + n2) / 2.0;
return result;
}
编译为共享库并运行:
g++ -shared -fpic -o libmyjni.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux TestJNIPrimitive.cpp
java -Djava.library.path=. -cp . TestJNIPrimitive
传递字符串
Java JNI 程序:
public class HelloJNI {
static {
System.loadLibrary("hello");
}
public native String sayHello(String msg);
public static void main(String[] args) {
String res = new HelloJNI().sayHello("Hello, JNI");
System.out.println("JNI Results: " + res);
}
}
生成头文件:javac -h . HelloJNI.java
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject, jstring);
编写 C++ 实现:
// HelloJNI.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj, jstring inJNIStr) {
if (inJNIStr == NULL) {
return NULL;
}
// Convert the JNI String (jstring) into C-String (char*)
const char* inCStr = env->GetStringUTFChars(inJNIStr, NULL);
if (inCStr == NULL) {
return NULL;
}
// Log the received string
std::cout << "In C++, the received string is: " << inCStr << std::endl;
// Perform operations on the string
std::string resultStr = std::string(inCStr) + " from C++";
// Release the JNI String resources
env->ReleaseStringUTFChars(inJNIStr, inCStr);
// Convert the modified C-string back into JNI String (jstring) and return
return env->NewStringUTF(resultStr.c_str());
}
注意,传递一个字符串比传递基本类型要复杂得多,因为 java 的 String 是一个对象,而 C 的 string 是一个 NULL 结尾的 char 数组。因此,我们需要将 java 的 String 对象转换成 C 的字符串表示形式:char *。
前面我们提到,JNI 环境指针 JNIEnv * 已经为我们定义了非常丰富的接口函数来处理数据的转换:
- 调用
const char* GetStringUTFChars(jstring, jboolean*)
来将 JNI 的 jstring 转换成 C 的 char * - 调用
jstring NewStringUTF(char*)
来将 C 的 char * 转换成 JNI 的 jstring
编译生成共享库并运行:
g++ -shared -fpic -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloJNI.cpp
java -Djava.library.path=. -cp . HelloJNI