JNI实战教程
概述
jni全称java native interface。出于某些原因我们需要在java程序中调用C/C++程序,一般情况下我们通过jni调用。
前提
我们需要以下工具:
- JDK11: 笔者采用了JDK11,其实其他版本也可以。主要是在命令上有些许不同。
- MinGW: C/C++工具链采用的MinGW,主要用来把C/C++文件编译成DLL。
下面为非必须:
- IntelliJ IDEA: 笔者版本为2021.
- CLion: 笔者版本为2021.2.
工作流程
SayHello
秉承计算机届的光荣传统,第一个程序就输出"Hello world"。
-
Java端
可以很明显的发现,我们这里使用了native关键词,它就是用来标识这个借口是用来调用C++代码的。下面代码的静态代码块在这个类被类加载器加载的时候调用了System.loadLibrary()方法来加载一个native库“hello”(这个库中实现了sayHello函数)。这个库在windows品台上对应了“hello.dll”,而在类UNIX平台上对应了“libhello.so”。这个库应该包含在Java的库路径(使用java.library.path系统变量表示)上,否则这个上面的程序会抛出UnsatisfiedLinkError错误。你应该使用VM的参数-Djava.library.path=path_to_lib来指定包含native库的路径。
public class HelloJni {
static {
System.loadLibrary("hello");
}
private native void sayHello();
public static void main(String[] args) {
new HelloJni().sayHello();
}
}
接下来我们使用javac -h命令来生成 .h 文件。使用如下 -h 后面的点表示生成的文件存放在当前目录。
javac -h. .\HelloJni.java
如果不出意外我们就会生成一个.h文件,内容如下。可以看到就是一个普普通通的头文件。注意开头引入的jni.h文件在你java安装目录的 inlcude文件夹下面。
/* 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
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对象下面我们给出的例子中没有使用上面的两个参数,不过后面我们的例子会使用的。到目前为止,你可以先忽略JNIEXPORT和JNICALL这两个玩意。上面头文件中有一个extern “C”,同时上面还有C++的条件编译语句,这么一来大家就明白了,这里的函数声明是要告诉C++编译器:这个函数是C函数,请使用C函数的签名协议规则去编译!因为我们知道C++的函数签名协议规则和C的是不一样的,因为C++支持重写和重载等面向对象的函数语法。
- C++端
新建一个C++文件然后实现如上的头文件即可。代码如下
#include "HelloJni.h"
#include <iostream>
using namespace std;
JNIEXPORT void JNICALL Java_HelloJni_sayHello(JNIEnv *, jobject) {
cout << "hello world" << endl;
}
然后我们使用如下命令编译C++文件:
g++ -I "D:\JDK11\include" -I "D:\JDK11\include\win32" -shared -o hello.dll
.\HelloJNI.cpp
执行完上述命令之后,我们便会dll文件。注意上述命令以及命令中的文件皆在%JAVA_HOME%\JDK11\include下执行的。得到dll文件我们可以选择就放在此目录下,然后在java程序启动的时候添加虚拟机参数如下即可:
-Djava.library.path=D:\JDK11\include
笔者直接在idea的启动配置中添加了此参数。
然后我们直接运行HelloJNI.java,输出如下:
至此我们算是成功完成了第一步,走通了这个流程。接下来笔者会用各种工具自动化这个流程。
IDEA,CLion配置JNI开发环境
- IDEA配置
添加External Tool
File-->Setting-->tools-->External Tool
点击添加,然后填入如下参数:
Program:D:\JDK11\bin\javac.exe
Arguments:-h .\header
Working directory:
接着我们右击想要生成.h文件的java文件,这样我们就可以一键生成且生成文件保存在当前目录的header文件下:
- CLion配置
首先C/C++工具链的设置
接着工程目录结构如下:
然后设置Cmake文件,最外层配置如下
cmake_minimum_required(VERSION 3.20)
project(jni_test)
set(CMAKE_CXX_STANDARD 20)
//这里加载jni.h需要配置好JAVA_HOME
find_package(JNI REQUIRED)
include_directories(${JNI_INCLUDE_DIRS})
//头文件存储的地方
include_directories(header)
//子目录
add_subdirectory(src)
src下Cmake配置如下:
//jni.cpp为我们接下来编写例子的文件
add_library(jni SHARED jni.cpp)
上述流程可能存在不详细的地方,详细过程参考这篇文章,笔者也是按照此文章配置的工程。
JNI基础知识
上面我们简单演示了怎么使用JNI,现在我们来系统梳理一下JNI中涉及的基本知识。JNI定义了以下数据类型,这些类型和Java中的数据类型是一致的:
Java原始类型:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean这些分别对应这java的int, byte,short, long, float, double, char and boolean。
Java引用类型:jobject用来指代java.lang.Object,除此之外,还定义了以下子类型:
- jclass for java.lang.Class.
- jstring for java.lang.String.
- jthrowable for java.lang.Throwable.
-
jarray对java的array。java的array是一个指向 8 个基本类型array的引用类型。于是,JNI中就有 8 个基本类型的array:jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray,jcharArray 和 jbooleanArray,还有一个就是指向Object的jobjectarray。Native函数会接受上面类型的参数,并且也会返回上面类型的返回值。然而,本地函数(C/C++)是需要按照它们自己的S方式处理类型的(比如C中的string,就是char *)。因此,需要在JNI类型和本地类型之间进行转换。通常来讲,本地函数需要:
- 加收JNI类型的参数(从java代码中传来)
- 对于JNI类型参数,需要讲这些数据转换或者拷贝成本地数据类型,比如讲jstring转成char *,jintArray转成C的int[]。需要注意的是,原始的JNI类型,诸如jint,jdouble之类的不用进行转换,可以直接使用,参与计算。
- 进行数据操作,以本地的方式
- 创建一个JNI的返回类型,然后讲结果数据拷贝到这个JNI数据中
- returnJNI类型数据
这其中最麻烦的事莫过于在JNI类型(如jstring, jobject, jintArray, jobjectArray)和本地类型(如C-string, int[])之间进行转换这件事情了。不过所幸的是,JNI环境已经为我们定义了很多的接口函数来做这种烦人的转换。(译者注:这里就需要使用上面我们提到的JNIEnv*那个参数了!)
类型转换
Java数据分为基本数据类型和引用数据类型,JNI层也是区别对待这两种类型的。下面是基本数据类型的
类型转换
Java | Native类型 | 符号类型 | 字长 |
---|---|---|---|
boolean | jboolean | 无符号 | 8位 |
byte | jbyte | 无符号 | 8位 |
char | jchar | 无符号 | 16位 |
short | jshort | 有符号 | 16位 |
int | jint | 有符号 | 32位 |
long | jlong | 有符号 | 64位 |
float | jfloat | 有符号 | 32位 |
double | jdouble | 有符号 | 64位 |
Java引用类型转换
Java引用类型 | Native类型 | Java引用类型 | Native类型 |
---|---|---|---|
All object | jobject | char[ ] | jcharArray |
java.lang.Class 实例 | jclass | short[ ] | jshortArray |
java.lang.String 实例 | jstring | int[ ] | jintArray |
Object[ ] | jobjectArray | long[ ] | jlongArray |
boolean[ ] | jbooleanArray | float[ ] | jfloatArray |
byte[ ] | jbyteArray | double[ ] | jboubleArray |
java.lang.Throwable 实例 | jthrowable |
JNI 签名
因为Java支持方法重载,所以native层调用Java层方法时需要方法的签名,这样来唯一找到目标函数。
类型标识示意表
几个函数签名例子
Java和Native代码之间传递参数和返回值
传递int 返回double
java代码
//java
private native double average(int n1, int n2);
//main中代码
System.out.println("transfer int :In Java, the average is " + new
TestJNIPrimitive().average( 3 , 2 ));
头文件
/*
* Class: testprimitive_TestJNIPrimitive
* Method: average
* Signature: (II)D
*/
JNIEXPORT jdouble JNICALL Java_testprimitive_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);
这里解释一下头文件注释。
- testprimitive_TestJNIPrimitive : TestJNIPrimitive对应java中的类名 testprimitive表示包名
- average: java中的方法名
- (II)D: II表示入参为两个int类型,D表示返回为double类型。
C++实现代码
JNIEXPORT jdouble JNICALL Java_testprimitive_TestJNIPrimitive_average(JNIEnv *env, jobject obj, jint a, jint b) {
cout << "In C++ " << a << "--" << b << endl;
//注意这里我们将类型转为jdoule然后返回,java只能接受再类型转换中对应的类型。
return ((jdouble)a + b) / 2.0;
}
由于上面配置了Clion因此我们直接使用Clion即可完成编译工作,将dll放到java项目中:
运行效果如下:
传递字符串
java代码
private native String transString(String str);
//main中代码
System.out.println("transfer string :In Java, the transString is " + new
TestJNIPrimitive().transString("hello native"));
头文件
/*
* Class: testprimitive_TestJNIPrimitive
* Method: transString
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_testprimitive_TestJNIPrimitive_transString(JNIEnv *, jobject, jstring);
C++实现:
/*
* Class: testprimitive_TestJNIPrimitive
* Method: transString
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_testprimitive_TestJNIPrimitive_transString(JNIEnv *env, jobject obj, jstring str) {
const char *inCStr = env->GetStringUTFChars(str, nullptr);【 1 】
if (nullptr == inCStr) {
return nullptr;
}
cout << "In c,the received string is: " << inCStr << endl;【 2 】
// 释放内存
env->ReleaseStringUTFChars(str, inCStr);【 3 】
string outCppStr;
cout << "Enter a String";
cin >> outCppStr;
return env->NewStringUTF(outCppStr.c_str());
}
执行效果如下:
In c,the received string is: 0xe8130ff
Enter a String
transfer string :In Java, the transString is 1231
【 1 】:将jstring转为char*
【 2 】:打印字符串
【 3 】:释放内存
JNI定义了jstring类型应对java的String类型。上面声明中的最后一个参数jstring就是来自Java代码中的String参数,同时,返回值也是一个jstring类型。传递一个字符串比传递基本类型要复杂的多,因为java的String是一个对象,而C++的string是一个NULL结尾的char数组。因此,我们需要将Java的String对象转换成C的字符串表示形式:char*。前面我们提到,JNI环境指针JNIEnv * 调用const char*GetStringUTFChars(JNIEnv , jstring, jboolean ) 来将JNI的jstring转换成C的char 调用 jstringNewStringUTF(JNIEnv , char)* 来将C的char *转换成JNI的jstring因此我们的C程序基本过程如下:使用GetStringUTFChars()函数来将jstring转换成char *,然后进行需要的数据处理使用NewStringUTF()函数来将char *转换成jstring,并且返回
JNI中的string转换函数
上面我们展示了两个函数,现在我们全面梳理下JNI为我们提供的函数。JNI支持Unicode(16bit字符)和UTF-8(使用1~3字节的编码)转化。一般而言,我们应该在C/C++中使用UTF-8的编码方式。JNI系统提供了如下关于字符串处理的函数(一共两组,UTF8和Unicode):
// UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
// Can be mapped to null-terminated char-array C-string
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
// Returns a pointer to an array of bytes representing the string in modifiedUTF-8 encoding.
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
// Informs the VM that the native code no longer needs access to utf.
jstring NewStringUTF(JNIEnv *env, const char *bytes);
// Constructs a new java.lang.String object from an array of characters inmodified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
// Returns the length in bytes of the modified UTF-8 representation of astring.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length,char *buf);
// Translates len number of Unicode characters beginning at offset start intomodified UTF-8 encoding
// and place the result in the given buffer buf.
// Unicode Strings (16-bit character)
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
// Returns a pointer to the array of Unicode characters
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
// Informs the VM that the native code no longer needs access to chars.
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);
// Constructs a new java.lang.String object from an array of Unicodecharacters.
jsize GetStringLength(JNIEnv *env, jstring string);
// Returns the length (the count of Unicode characters) of a Java string.
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar*buf);
// Copies len number of Unicode characters beginning at offset start to thegiven buffer buf
GetStringUTFChars()函数可以将jstring转成char *,这个函数会返回NULL,如果系统的内容分配失败的话。因此,好的做法是检查这个函数的返回是不是NULL。第三个参数是isCopy,这个参数是一个in-out参数,传进去的是一个指针,函数结束的时候指针的内容会被修改。如果内容是JNI_TRUE的话,那么代表返回的数据是jstring数据的一个拷贝,反之,如果是JNI_FALSE的话,就说明返回的字符串就是直接指向那个String对象实例的。在这种情况下,本地代码不应该随意修改string中的内容,因为修改会代码Java中的修改。JNI系统会尽量保证返回的是直接引用,如果不能的话,那就返回一个拷贝。通常,我们很少关心修改这些string ,因此我们这里一般传递NULL给isCopy参数。必须要注意的是,当你不在需要GetStringUTFChars返回的字符串的时候,一定记得调用ReleaseStringUTFChars()函数来将内存资源释放!否则会内存泄露!并且上层java中的GC也不能进行!另外,在GetStringUTFChars和ReleaseStringUTFChars不能block!NewStringUTF()函数可以从char *字符串得到jstring。关于更详细的描述,请参考Java Native Interface Specification:docs.oracle.com/javase/7/do…
传递基本类型的数组
java代码
private native double[] sumAndAverage(int[] numbers);
//main中代码
final double[] sumAndAverage = new TestJNIPrimitive().sumAndAverage(new int[]
{ 1 , 2 , 3 , 4 });
System.out.println("transfer double[] :In Java, the sumAndAverage sum: " +
sumAndAverage[ 0 ] + " average: " + sumAndAverage[ 1 ]);
头文件
/*
* Class: testprimitive_TestJNIPrimitive
* Method: sumAndAverage
* Signature: ([I)[D
*/
JNIEXPORT jdoubleArray JNICALL Java_testprimitive_TestJNIPrimitive_sumAndAverage
(JNIEnv *, jobject, jintArray);
C++实现
JNIEXPORT jdoubleArray JNICALL Java_testprimitive_TestJNIPrimitive_sumAndAverage(JNIEnv *env, jobject obj, jintArray jarry) {
jint *inCarry = (*env).GetIntArrayElements(jarry, nullptr);
if (nullptr == inCarry) {
return nullptr;
}
jsize length = (*env).GetArrayLength(jarry);
jint sum = 0 ;
for (int i = 0 ; i < length; i++) {
sum += inCarry[i];
}
jdouble avg = (jdouble)sum / length;
(*env).ReleaseIntArrayElements(jarry, inCarry, 0 );
jdouble outCArray[] = {(jdouble)sum, avg};
jdoubleArray p_array = (*env).NewDoubleArray( 2 );
if (p_array == nullptr) {
return nullptr;
}
(*env).SetDoubleArrayRegion(p_array, 0 , 2 , outCArray);
return p_array;
}
执行效果如下:
transfer double[] :In Java, the sumAndAverage sum: 10.0 average: 2.
在Java中,array是指一种类型,类似于类。一共有 9 种java的array, 8 个基本类型的array和一个object的array。JNI针对java的基本类型都定义了相应的array:jintArray, jbyteArray, jshortArray, jlongArray,jfloatArray, jdoubleArray, jcharArray, jbooleanArray,并且也有面向object的jobjectArray。同样地,你需要在JNIarray和Native array之间进行转换,JNI系统已经为我们提供了一系列的接口函数:
- 使用jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)将jintarray转换成C的jint[]
- 使用jintArray NewIntArray(JNIEnv *env, jsize len)函数来分配一个len字节大小的空间,然后再使用void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)函数讲jint[]中的数据拷贝到jintArray中去。一共有 8 对类似上面的函数,分别对应java的 8 个基本数据类型。因此,native程序需要:
- 接受来自java的JNI array,然后转换成本地array
- 进行需要的数据操作
- 将需要返回的数据转换成jni的array,然后返回
JNI基本类型的array函数
JNI基本类型的array(jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray,jcharArray 和 jbooleanArray)函数如下:
// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray,jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array,jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array,NativeType *elems, jint mode);
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start,jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start,jsize length, const NativeType *buffer);
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
同样地,在get函数和release函数之间也不能always block。
访问Java对象变量和回调Java方法
访问Java对象实例的变量和静态变量
java代码
//接口声明
private native void modifyInstanceVariable();
private static int staticNumber = 21 ;
private int number = 88 ;
private String message = "Hello from Java";
//main中代码
final TestJNIPrimitive jniPrimitive = new TestJNIPrimitive();
jniPrimitive.modifyInstanceVariable();
System.out.println("modifyInstanceVariable:In Java, int is " +jniPrimitive.number);
System.out.println("modifyInstanceVariable:In Java, String is " +jniPrimitive.message);
System.out.println("modifyInstanceVariable:In Java, static number is " +staticNumber);
头文件
/*
* Class: testprimitive_TestJNIPrimitive
* Method: modifyInstanceVariable
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_testprimitive_TestJNIPrimitive_modifyInstanceVariable
(JNIEnv *, jobject);
C++实现
JNIEXPORT void JNICALL
Java_testprimitive_TestJNIPrimitive_modifyInstanceVariable(JNIEnv *env, jobject obj) {
jclass p_jclass = (*env).GetObjectClass(obj);
jfieldID p_number = (*env).GetFieldID(p_jclass, "number", "I");
if (NULL == p_number) {
return;
}
jint number = (*env).GetIntField(p_jclass, p_number);
cout << "In c++ ,the int is " << number << endl;
number = 99 ;
(*env).SetIntField(obj, p_number, number);
jfieldID messageId = (*env).GetFieldID(p_jclass, "message","Ljava/lang/String;");
if (messageId == NULL) {
return;
}
auto message = (jstring)((*env).GetObjectField(obj, messageId));
const char *cStr = (*env).GetStringUTFChars(message, NULL);
if (cStr == nullptr) {
return;
}
}
执行效果:
In c++ ,the int is 0
In c++,the string is Hello from Java
In c++ , sNumber is 21
modifyInstanceVariable:In Java, int is 99
modifyInstanceVariable:In Java, String is hello from c++
modifyInstanceVariable:In Java, static number is 233
为了访问对象中的变量,我们需要:
- 调用GetObjectClass()获得目标对象的类引用
- 从上面获得的类引用中获得Field ID来访问变量,你需要提供这个变量的名字,变量的描述符(也称为签名)。对于java类而言,描述符是这样的形式:“Lfully-qualified-name;”(注意最后有一个英文半角分号),其中的包名点号换成斜杠(/),比如java的Stirng类的描述符就是“Ljava/lang/String;”。对于基本类型而言,I代表int,B代表byte,S代表short,J代表long,F代表float,D代表double,C代表char,Z代表boolean。对于array而言,使用左中括号”[“来表示,比如“[Ljava/lang/Object;”表示Object的array,“[I”表示int型的array。
- 基于上面获得的Field ID,使用GetObjectField() 或者 Get_primitive-type_Field()函数来从中解析出我们想要的数据
- 使用SetObjectField() 或者 Set_primitive-type_Field()函数来修改变量JNI中用来访问实例变量的函数有:
-
访问类中的static变量类似于上面访问普通的实例变量,只是我们这里使用的函数是GetStaticFieldID(), Get|SetStaticObjectField(),
jclass GetObjectClass(JNIEnv *env, jobject obj); // Returns the class of an object. jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig); // Returns the field ID for an instance variable of a class. NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID); void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeTypevalue); // Get/Set the value of an instance variable of an object // <type> includes each of the eight primitive types plus Object.
回调实例的普通和static方法
java
// Declare a native method that calls back the Java methods below
private native void nativeMethod();
// To be called back by the native code
private void callback() {
System.out.println("In Java");
}
private void callback(String message) {
System.out.println("In Java with " + message);
}
private double callbackAverage(int n1, int n2) {
return ((double) n1 + n2) / 2.0;
}
// Static method to be called back
private static String callbackStatic() {
return "From static Java method";
}
//main
final TestJNIPrimitive jniPrimitive1 = new TestJNIPrimitive();
jniPrimitive1.nativeMethod();
头文件
/*
* Class: testprimitive_TestJNIPrimitive
* Method: nativeMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_testprimitive_TestJNIPrimitive_nativeMethod(JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_testprimitive_TestJNIPrimitive_nativeMethod(JNIEnv *env, jobject obj) {
jclass p_jclass = env->GetObjectClass(obj);
}
c++实现
jmethodID callMethodId = env->GetMethodID(p_jclass, "callback", "()V");
if (callMethodId == NULL) {
return;
}
cout << "callback() execute in c++" << endl;
env->CallVoidMethod(obj, callMethodId);
jmethodID callBackStr = env->GetMethodID(p_jclass, "callback", "(Ljava/lang/String;)V");
jstring message = env->NewStringUTF("hello from c++");
cout << " callBack(str) execute in c++:" << endl;
env->CallVoidMethod(obj, callBackStr, message);
jmethodID midCallBackAverage = env->GetMethodID(p_jclass,"callbackAverage", "(II)D");
if (NULL == midCallBackAverage) return;
jdouble average = env->CallDoubleMethod(obj, midCallBackAverage, 2 , 3 );
cout << "In C++, the average is " << average << endl;
jmethodID midCallBackStatic = env->GetStaticMethodID(p_jclass,"callbackStatic", "()Ljava/lang/String;");
if (NULL == midCallBackStatic) return;
jstring resultJNIStr = (jstring)env->CallStaticObjectMethod(p_jclass,
midCallBackStatic);
const char *resultCStr = env->GetStringUTFChars(resultJNIStr, NULL);
if (NULL == resultCStr) return;
cout << "In C++, the returned string is " << resultCStr << endl;
env->ReleaseStringUTFChars(resultJNIStr, resultCStr);
}
执行效果
callback() execute in c++
In Java
callBack(str) execute in c++:
In Java with hello from c++
In C++, the average is 2.
In C++, the returned string is From static Java method
为了能够回调实例中的方法,我们需要:
- 通过GetObjectClass()函数获得这个实例的类对象
-
从上面获得类对象中,调用GetMethodID()函数来获得Method ID,Method ID表示了实例中的某个方法的抽象。你需要提供这个方法的名字和签名信息,签名规则和变量类似。签名的格式是这样的:(parameters)return-type。如果我们实在觉得jni的签名不好记忆的话,我们可以是用JDK为我们提供的工具javap来获得某个class类中的所有方法的签名,使用-s选项表示打印签名,-p表示显示private成员:
从上面的输出我们可以清楚地看到类中每一个方法的签名。
- 基于上面我们获得的Method ID,我们可以调用_Primitive-type_Method() 或者 CallVoidMethod()或者 CallObjectMethod()来调用这个方法。如果某个方法需要参数的话,就在后面跟上参数即可。
- 如果想要调用一个static方法的话,使用GetMethodID(), CallStatic_Primitive-type_Method(),CallStaticVoidMethod() 或者 CallStaticObjectMethod()。
JNI中用来回调实例和static方法的所有函数(两类,普通的和static的):
jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char*sig);
// Returns the method ID for an instance method of a class or interface.
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID,va_list args);
// Invoke an instance method of the object.
// The <type> includes each of the eight primitive and Object.
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, constchar *sig);
// Returns the method ID for an instance method of a class or interface.
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID,...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
// Invoke an instance method of the object.
// The <type> includes each of the eight primitive and Object.
回调复写的父类实例方法
JNI提供了一系列的形如 CallNonvirtual_Type_Method()之类的函数来调用父类实例的方法:
- 首先获得Method ID,使用GetMethodID()
- 基于上获得的Method ID,通过调用 CallNonvirtual_Type_Method()函数来调用相应的方法,并且在参数中给出object,父类和参数列表。JNI中用来访问父类方法的函数:
创建Object
在native代码中构造jobject和jobjectarray,通过调用NewObject() 和 newObjectArray()函数,然后将
它们返回给java代码
java
private native Integer getIntegerObject(int number);
//main
final TestJNIPrimitive jniPrimitive2 = new TestJNIPrimitive();
System.out.println("In Java, the number is :" +
jniPrimitive2.getIntegerObject( 9999 ));
头文件
/*
* Class: testprimitive_TestJNIPrimitive
* Method: getIntegerObject
* Signature: (I)Ljava/lang/Integer;
*/
JNIEXPORT jobject JNICALL Java_testprimitive_TestJNIPrimitive_getIntegerObject(JNIEnv *, jobject, jint);
c++实现
JNIEXPORT jobject JNICALL Java_testprimitive_TestJNIPrimitive_getIntegerObject(JNIEnv *env, jobject obj, jint number) {
jclass cls = (*env).FindClass("java/lang/Integer");
// Get the Method ID of the constructor which takes an int
jmethodID midInit = (*env).GetMethodID(cls, "<init>", "(I)V");
if (NULL == midInit) return NULL;
// Call back constructor to allocate a new instance, with an int argument
jobject newObj = (*env).NewObject(cls, midInit, number);
// Try runnning the toString() on this newly create object
jmethodID midToString = (*env).GetMethodID(cls, "toString", "()Ljava/lang/String;");
if (NULL == midToString) return NULL;
jstring resultStr = (jstring)(*env).CallObjectMethod(newObj, midToString);
const char *resultCStr = (*env).GetStringUTFChars(resultStr, NULL);
cout << "In C++: the number is " << resultCStr;
return newObj;
}
执行效果
In Java, the number is :9999
JNI中用于创建对象(jobject)的函数有:
创建Object arrays
java
private native Integer getIntegerObject(int number);
//main
Integer[] numbers = { 11 , 22 , 32 };
Double[] results = new TestJNIPrimitive().sumAndAverageToo(numbers);
System.out.println("In Java, the sum is " + results[ 0 ]);
System.out.println("In Java, the average is " + results[ 1 ]);
头文件
/*
* Class: testprimitive_TestJNIPrimitive
* Method: getIntegerObject
* Signature: (I)Ljava/lang/Integer;
*/
JNIEXPORT jobject JNICALL Java_testprimitive_TestJNIPrimitive_getIntegerObject
(JNIEnv *, jobject, jint);
c++实现
JNIEXPORT jobject JNICALL Java_testprimitive_TestJNIPrimitive_getIntegerObject(JNIEnv *env, jobject obj, jint number) {
jclass cls = (*env).FindClass("java/lang/Integer");
// Get the Method ID of the constructor which takes an int
jmethodID midInit = (*env).GetMethodID(cls, "<init>", "(I)V");
if (NULL == midInit) return NULL;
// Call back constructor to allocate a new instance, with an int argument
jobject newObj = (*env).NewObject(cls, midInit, number);
// Try runnning the toString() on this newly create object
jmethodID midToString = (*env).GetMethodID(cls, "toString", "()Ljava/lang/String;");
if (NULL == midToString) return NULL;
jstring resultStr = (jstring)(*env).CallObjectMethod(newObj, midToString);
const char *resultCStr = (*env).GetStringUTFChars(resultStr, NULL);
cout << "In C++: the number is " << resultCStr;
return newObj;
}
执行结果
In C++: the number is 9999In C++, the sum is 65
In C++, the average is 21.
In Java, the sum is 65.
In Java, the average is 21.
不像基本数据类型的array那样,你需要使用Get|SetObjectArrayElement()函数来处理每一个元素。
JNI提供了创建对象array(jobjectArray)的函数如下:
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass,
jobject initialElement);
// Constructs a new array holding objects in class elementClass.
// All elements are initially set to initialElement.
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
// Returns an element of an Object array.
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject
value);
// Sets an element of an Object array.
JNA
Java Native Access ( JNA ) 是一个由社区开发的库,它使Java程序无需使用Java Native Interface即可轻松访问本地共享库。JNA的设计旨在以最少的努力以本地的方式提供本地访问,且不需要样板代码或胶水代码。从上面的文档可以看出JNI使用起来还是相当繁琐的。因此JNA就在此条件下孕育而出,但是JNA并不是银弹。它只能实现java调用c++反过来却不行。可以说它是JNI的替代者,是因为JNA大大简化了调用本地方法的过程,使用很方便,基本上不需要脱离Java环境就可以完成。JNA调用C/C++的过程大致如下:
可以看到步骤减少了很多,最重要的是我们不需要重写我们的动态链接库文件,而是有直接调用的API,大大简化了我们的工作量。
2 、原理
JNA使用一个小型的JNI库插桩程序来动态调用本地代码。开发者使用java接口描述目标本地库的功能和结构,这使得它很容易利用本机平台的功能,而不会产生多平台配置和生成JNI代码的高开销。这样的性能、准确性和易用性显然受到很大的重视。此外JNA包括一个已与许多本地函数映射的平台库,以及一组简化本地访问的共用接口。
类型对照
实战
java部分
public interface MYTestJNA extends Library {
MYTestJNA INSTANCE =(MYTestJNA) Native.load("libtest-jna",
MYTestJNA.class);
void sayHello();
int testReturnInt();
void returnArray(double[] d, int[] a, String b, int length);
void sendString(String val);
void getString(PointerByReference val);
void cleanup(Pointer p);
}
Java Test代码
@Test
void testJNA3() {
MYTestJNA.INSTANCE.sayHello();
System.out.println("return from c++: " +
MYTestJNA.INSTANCE.testReturnInt());
int[] a = { 1 , 2 , 3 , 4 };
double[] d = new double[ 2 ];
MYTestJNA.INSTANCE.returnArray(d, a, "lucky", 4 );
for (double lucky : d) {
System.out.println(lucky);
}
}
C++
void sayHello() {
cout << "hello jna" << endl;
}
int testReturnInt() {
return 1 ;
}
char *returnArray(double d[], int a[], const char *str, int length) {
for (int i = 0 ; i < length; i++) {
cout << "from c++ value is d" << a[i] << endl;
}
cout << "from C++ str value: " << str << endl;
while (*str) {
cout << *str++ << endl;
}
d[ 0 ] = 1.0;
d[ 1 ] = 4.0;
char *return_str = "hello from c++";
return return_str;
}
执行效果
return from c++: 1
1.0
4.0
hello jna
from c++ value is d1
from c++ value is d2
from c++ value is d3
from c++ value is d4
from C++ str value: lucky
l
u
c
k
y
是不是感觉意外的简单,我当时也是这样的。但是随着深入也发现不少问题,比如当你想返回double[]的他就给报错。仔细信息jni也不是直接返回doule[],而且也涉及到资源释放问题。因此JNA虽然大大简化了native编程,但他也不是万能药。最终还是要二者结合使用。但是JNA本身封装好了大量C++本身的库,比如kernel32等等,如果有需求就可以直接拿来使用。详细的JNA使用方法,请看此链接
总结
一般需求使用JNA,特殊需求JNI。
\