JNI快速入门
文章原始链接:https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html
引言
有的时候我们需要使用本地代码(C/C++)来克服Java中的内存管理和性能问题,Java通过JNI机制来支持内地代码的使用。 想要比较好地理解JNI是比较难的,因为它包含了两种语言和运行时机制。 在继续之前,我应该假设你具备以下知识点和技能: 1. java 2. C/C++和gcc编译器 3. 对于windows而言,熟悉Gygwin或者MinGW 4. 对于IDE而言,熟悉Eclipse C/C++ Development Tool (CDT)
开始
使用C来实现JNI
步骤1,编写一个使用C实现函数的java类,HelloJNI.java:
public class HelloJNI {
static {
System.loadLibrary("hello"); // Load native library at runtime
// hello.dll (Windows) or libhello.so (Unixes)
}
// Declare a native method sayHello() that receives nothing and returns void
private native void sayHello();
// Test Driver
public static void main(String[] args) {
new HelloJNI().sayHello(); // invoke the native method
}
}
上面代码的静态代码块在这个类被类加载器加载的时候调用了System.loadLibrary()方法来加载一个native库“hello”(这个库中实现了sayHello函数)。这个库在windows品台上对应了“hello.dll”,而在类UNIX平台上对应了“libhello.so”。这个库应该包含在Java的库路径(使用java.library.path系统变量表示)上,否则这个上面的程序会抛出UnsatisfiedLinkError错误。你应该使用VM的参数-Djava.library.path=path_to_lib来指定包含native库的路径。 接下来,我们使用native关键字将sayHello()方法声明为本地实例方法,这就很明显地告诉JVM:这个方法实现在另外一个语言中(C/C++),请去那里寻找他的实现。注意,一个native方法不包含方法体,只有声明。上面代码中的main方法实例化了一个HelloJJNI类的实例,然后调用了本地方法sayHello()。 下面,我们编译HelloJNI.java成HelloJNI.class
javac HelloJNI.java
接下来,我们利用上面生成的class文件生成用于编写C/C++代码的头文件,使用jdk中的javah工具完成:
javah HelloJNI
上面的命令执行完之后生成了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)。包名中的点换成单下划线。需要说明的是生成函数中的两个参数: 1. JNIEnv *:这是一个指向JNI运行环境的指针,后面我们会看到,我们通过这个指针访问JNI函数 2. jobject:这里指代java中的this对象 下面我们给出的例子中没有使用上面的两个参数,不过后面我们的例子会使用的。到目前为止,你可以先忽略JNIEXPORT和JNICALL这两个玩意。 上面头文件中有一个extern “C”,同时上面还有C++的条件编译语句,这么一来大家就明白了,这里的函数声明是要告诉C++编译器:这个函数是C函数,请使用C函数的签名协议规则去编译!因为我们知道C++的函数签名协议规则和C的是不一样的,因为C++支持重写和重载等面向对象的函数语法。 接下来,我们给出C语言的实现,以实现上面的函数: C语言实现:
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
}
将上面的代码保存为HelloJNI.c。jni.h头文件在 “\include” 和 “\include\win32”目录下,这里的JAVA_HOME是指你的JDK安装目录。 这段C代码的作用很简单,就是在终端上打印Hello Word!这句话。 下面我们编译这段代码,使用GCC编译器: 对于windows上的MinGW:
> set JAVA_HOME=C:\Program Files\Java\jdk1.7.0_{xx}
// Define and Set environment variable JAVA_HOME to JDK installed directory
// I recommend that you set JAVA_HOME permanently, via "Control Panel" ⇒ "System" ⇒ "Environment Variables"
> echo %JAVA_HOME%
// In Windows, you can refer a environment variable by adding % prefix and suffix
> gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c
// Compile HellJNI.c into shared library hello.dll
也可以分步编译:
// Compile-only with -c flag. Output is HElloJNI.o
> gcc -c -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" HelloJNI.c
// Link into shared library "hello.dll"
> gcc -Wl,--add-stdcall-alias -shared -o hello.dll HelloJNI.o
下面,我们使用nm命令来查看生成hello.dll中的函数:
> nm hello.dll | grep say
624011d8 T _Java_HelloJNI_sayHello@8
对于windows上的Cygwin: 首先,你需要讲__int64定义成“long long”类型,通过-D _int64=”long long选项实现。 对于gcc-3,请包含选项-nmo -cygwin来编译dll库,这些库是不依赖于Cygwin dll的。
> gcc-3 -D __int64="long long" -mno-cygwin -Wl,--add-stdcall-alias
-I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o hello.dll HelloJNI.c
对于gcc-4,我目前还没有找到正确的编译选项。 ==这部分是笔者添加的== 原文中只给出了windows平台上编译方法,下面我给出Linux等类UNIX上编译方法:
gcc -fPIC --shared HelloJNI.c -o libhello.so -I /usr/lib/jvm/java-7-openjdk-amd64/include/
上面的命令编译生成一个libhello.so共享库在当前目录下 ==这部分是笔者添加的== 接下来,让我们运行一下上面的代码吧:
> java HelloJNI
or
> java -Djava.library.path=. HelloJNI
有的时候,你可能需要使用-Djava.library.path来指定加载库的位置,因为可能报出java.lang.UnsatisfiedLinkError错误. ==这部分是笔者添加的== 我们首先使用nm命令(关于nm请自行Google或者man)查看libhello.so中都有那些函数: 
可以看到我们的sayHello函数已经在这个里面,这说明我们编译的基本没有问题。 下面,我给出在我电脑上运行的效果(原文作者没有给出): 首先我们执行java HelloJNI,看看能不能运行: 
果然,出现了UnsatisfiedLinkError错误,原因是VM去标准路径下查找这个库,发现找不到,然后就挂了。因此我们还是需要使用-Djava.library.path来明确告诉VM我们的库在哪里(当然,你也可以将你编译出来的库放到系统标准路径中,比如/usr/lib目录下): 
现在OK了,因为我们明确告诉VM,我们的libhello.so就在当前目录下,不用傻傻地去系统中找啦!! 这部分是笔者添加的
使用C/C++混合实现JNI
第一步:编写一个使用本地代码的java类:HelloJNICpp.java
public class HelloJNICpp {
static {
System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so (Unixes)
}
// Native method declaration
private native void sayHello();
// Test Driver
public static void main(String[] args) {
new HelloJNICpp().sayHello(); // Invoke native method
}
}
同样地,我们使用javac来编译这个代码:
> javac HelloJNICpp.java
步骤2:生成C/C++的头文件
> javah HelloJNICpp
上面命令会生成一个HelloJNICpp.h的文件,并且这个文件中声明了这个本地函数:
JNIEXPORT void JNICALL Java_HelloJNICpp_sayHello(JNIEnv *, jobject);
步骤3:C/C++编码实现,HelloJNICppImpl.h, HelloJNICppImpl.cpp, 和 HelloJNICpp.c 这里,我们使用C++来实现真正的函数(”HelloJNICppImpl.h” 和 “HelloJNICppImpl.cpp”),而使用C来和java进行交互。(译者注:这样就可以把JNI的代码逻辑和我们真正的业务逻辑分离开了!) C++头文件:”HelloJNICppImpl.h”
#ifndef _HELLO_JNI_CPP_IMPL_H
#define _HELLO_JNI_CPP_IMPL_H
#ifdef __cplusplus
extern "C" {
#endif
void sayHello ();
#ifdef __cplusplus
}
#endif
#endif
C++的代码实现:”HelloJNICppImpl.cpp”
#include "HelloJNICppImpl.h"
#include <iostream>
using namespace std;
void sayHello () {
cout << "Hello World from C++!" << endl;
return;
}
C代码实现和Java的交互:”HelloJNICpp.c”
#include <jni.h>
#include "HelloJNICpp.h"
#include "HelloJNICppImpl.h"
JNIEXPORT void JNICALL Java_HelloJNICpp_sayHello (JNIEnv *env, jobject thisObj) {
sayHello(); // invoke C++ function
return;
}
讲上面的代码编译成一个共享库(在windows上是hello.dll)。 使用windows上的MinGW GCC:
> set JAVA_HOME=C:\Program Files\Java\jdk1.7.0_{xx}
> g++ -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32"
-shared -o hello.dll HelloJNICpp.c HelloJNICppImpl.cpp
步骤4:运行java代码
> java HelloJNICpp
or
> java -Djava.library.path=. HelloJNICpp
java package中的JNI
在真正的产品化中,所有的java类都是有自己的包的,而不是一个默认的没有名字的包。下面我们说明一下java中的package怎么在JNI中使用。 步骤1:使用JNI的程序, myjni\HelloJNI.java
package myjni; // 多了包名定义
public class HelloJNI {
static {
System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so (Unixes)
}
// A native method that receives nothing and returns void
private native void sayHello();
public static void main(String[] args) {
new HelloJNI().sayHello(); // invoke the native method
}
}
上面的这个类应该放在myjni目录下。然后我们编译这个代码:
// change directory to package base directory
> javac myjni\HelloJNI.java
步骤2:生成C/C++头文件 如果你的java代码是放在一个包中的,那么你需要使用完全限定名称来生成C/C++头文件的。你可能会需要使用-classpath选项来指定JNI程序的classpath路径,并且可能会使用-d选项来指定生成头文件的目标文件夹。
> javah --help
......
// Change directory to package base directory
> javah -d include myini.HelloJNI
在上面的例子中,我们选择将生层的头文件放在include目录下,因此,我们输出的就是:”include\myjni_HelloJNI.h”.这个头文件声明了这样的本地函数:
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *, jobject);
我们看到,和上面的例子相比,这里的名字规则是这样的:Java__methodName,同时,点号换成单下划线。 步骤3:C代码实现:HelloJNI.c
#include <jni.h>
#include <stdio.h>
#include "include\myjni_HelloJNI.h"
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
}
编译C代码:
> gcc -Wl,--add-stdcall-alias -I<JAVA_HOME>\include -I<JAVA_HOME>\include\win32 -shared -o hello.dll HelloJNI.c
运行代码:
> java myjni.HelloJNI
在Eclipse中开发JNI
这部分作者写的非常易懂,由于个人时间问题,就不翻译了,大家看看就明白(其实就是一些Eclipse的设置问题,没有什么复杂的),或者百度看国内的也行。(抱歉了!) 原文链接:https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html
JNI基础知识
上面我们简单演示了怎么使用JNI,现在我们来系统梳理一下JNI中涉及的基本知识。 JNI定义了以下数据类型,这些类型和Java中的数据类型是一致的: 1. Java原始类型:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean这些分别对应这java的int, byte, short, long, float, double, char and boolean。 2. Java引用类型:jobject用来指代java.lang.Object,除此之外,还定义了以下子类型: a. jclass for java.lang.Class. b. jstring for java.lang.String. c. jthrowable for java.lang.Throwable. d. jarray对java的array。java的array是一个指向8个基本类型array的引用类型。于是,JNI中就有8个基本类型的array:jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray 和 jbooleanArray,还有一个就是指向Object的jobjectarray。 Native函数会接受上面类型的参数,并且也会返回上面类型的返回值。然而,本地函数(C/C++)是需要按照它们自己的方式处理类型的(比如C中的string,就是char *)。因此,需要在JNI类型和本地类型之间进行转换。通常来讲,本地函数需要: 1. 加收JNI类型的参数(从java代码中传来) 2. 对于JNI类型参数,需要讲这些数据转换或者拷贝成本地数据类型,比如讲jstring转成char *, jintArray转成C的int[]。需要注意的是,原始的JNI类型,诸如jint,jdouble之类的不用进行转换,可以直接使用,参与计算。 3. 进行数据操作,以本地的方式 4. 创建一个JNI的返回类型,然后讲结果数据拷贝到这个JNI数据中 5. returnJNI类型数据
这其中最麻烦的事莫过于在JNI类型(如jstring, jobject, jintArray, jobjectArray)和本地类型(如C-string, int[])之间进行转换这件事情了。不过所幸的是,JNI环境已经为我们定义了很多的接口函数来做这种烦人的转换。(译者注:这里就需要使用上面我们提到的JNIEnv*那个参数了!)
在Java和Native代码之间传递参数和返回值
传递基本类型
传递java的基本类型是非常简单而直接的,一个jxxx之类的类型已经定义在本地系统中了,比如:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar 和 jboolean分别对应java的int, byte, short, long, float, double, char 和 boolean基本类型。 Java JNI 程序:TestJNIPrimitive.java
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));
}
}
这个JNI程序加载了myjni.dll(windows)库或者libmyjni.so(类UNIX)库。并且声明了一个native方法,这个方法接受两个int类型的参数,并且返回一个double类型的返回值,这个值是两个int型数的平均值。mian方法调用了average函数。 下面,我们将上面的java代码编译成TestJNIPrimitive.class,进而生成C/C++头文件TestJNIPrimitive.h:
> javac TestJNIPrimitive.java
> javah TestJNIPrimitive // Output is TestJNIPrimitive.h
C实现:TestJNIPrimitive.c
头文件TestJNIPrimitive.h中包含了一个函数声明:
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);
可以看到,这里的jint和jdouble分别表示java中的int和double。 jni.h(windows上是win32/jni_mh.h)头文件包含了这些数据类型的定义,同时多了一个jsize的定义:
// In "win\jni_mh.h" - machine header which is machine dependent
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
// In "jni.h"
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;
有趣的是,jint对应到C的long类型(至少是32bit的),而不是C的int类型(至少是16bit的)。于是,在C代码中要使用jint而不是int是很重要的。同时,CygWin不支持__int64类型。 TestJNIPrimitive.c的实现如下:
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitive.h"
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject thisObj, jint n1, jint n2) {
jdouble result;
printf("In C, the numbers are %d and %d\n", n1, n2);
result = ((jdouble)n1 + n2) / 2.0;
// jint is mapped to int, jdouble is mapped to double
return result;
}
然后,我们编译代码成一个共享库:
// MinGW GCC under Windows
> set JAVA_HOME={jdk-installed-directory}
> gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o myjni.dll TestJNIPrimitive.c
最后,我们运行这个java代码:
> java TestJNIPrimitive
C++实现 TestJNIPrimitive.cpp
代码如下:
#include <jni.h>
#include <iostream>
#include "TestJNIPrimitive.h"
using namespace std;
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject obj, jint n1, jint n2) {
jdouble result;
cout << "In C++, the numbers are " << n1 << " and " << n2 << endl;
result = ((jdouble)n1 + n2) / 2.0;
// jint is mapped to int, jdouble is mapped to double
return result;
}
使用g++来编译上面的代码:
// MinGW GCC under Windows
> g++ -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o myjni.dll TestJNIPrimitive.cpp
传递字符串
Java JNI 程序:TestJNIString.java
public class TestJNIString {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Native method that receives a Java String and return a Java String
private native String sayHello(String msg);
public static void main(String args[]) {
String result = new TestJNIString().sayHello("Hello from Java");
System.out.println("In Java, the returned string is: " + result);
}
}
上面的代码声明了一个native函数sayHello,这个函数接受一个java的String,然后返回一个Java string,main方法调用了sayHello函数。 然后,我们编译上面的代码,并且生成C/C++的头文件:
> javac TestJNIString.java
> javah TestJNIString
C代码实现:TestJNIString.c
上面的头文件TestJNIString.h声明了这样的一个函数:
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring);
JNI定义了jstring类型应对java的String类型。上面声明中的最后一个参数jstring就是来自Java代码中的String参数,同时,返回值也是一个jstring类型。 传递一个字符串比传递基本类型要复杂的多,因为java的String是一个对象,而C的string是一个NULL结尾的char数组。因此,我们需要将Java的String对象转换成C的字符串表示形式:char *。 前面我们提到,JNI环境指针JNIEnv *已经为我们定义了非常丰富的接口函数用来处理数据的转换: 1. 调用const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)来将JNI的jstring转换成C的char * 2. 调用jstring NewStringUTF(JNIEnv*, char*)来将C的char *转换成JNI的jstring 因此我们的C程序基本过程如下: 1. 使用GetStringUTFChars()函数来将jstring转换成char * 2. 然后进行需要的数据处理 3. 使用NewStringUTF()函数来将char *转换成jstring,并且返回
#include <jni.h>
#include <stdio.h>
#include "TestJNIString.h"
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
// Step 1: Convert the JNI String (jstring) into C-String (char*)
const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
if (NULL == inCSt) return NULL;
// Step 2: Perform its intended operations
printf("In C, the received string is: %s\n", inCStr);
(*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr); // release resources
// Prompt user for a C-string
char outCStr[128];
printf("Enter a String: ");
scanf("%s", outCStr); // not more than 127 characters
// Step 3: Convert the C-string (char*) into JNI String (jstring) and return
return (*env)->NewStringUTF(env, outCStr);
}
将上面的代码编译成共享库:
// MinGW GCC under Windows
> gcc -Wl,--add-stdcall-alias -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.c
最后,运行代码:
> java TestJNIString
In C, the received string is: Hello from Java
Enter a String: test
In Java, the returned string is: test
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 modified UTF-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 in modified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
// Returns the length in bytes of the modified UTF-8 representation of a string.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
// Translates len number of Unicode characters beginning at offset start into modified 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 charactersc
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 Unicode characters.
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 the given 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:http://docs.oracle.com/javase/7/docs/technotes/guides/jni/index.html
C++实现:TestJNIString.cpp
#include <jni.h>
#include <iostream>
#include <string>
#include "TestJNIString.h"
using namespace std;
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
// Step 1: Convert the JNI String (jstring) into C-String (char*)
const char *inCStr = env->GetStringUTFChars(inJNIStr, NULL);
if (NULL == inCStr) return NULL;
// Step 2: Perform its intended operations
cout << "In C++, the received string is: " << inCStr << endl;
env->ReleaseStringUTFChars(inJNIStr, inCStr); // release resources
// Prompt user for a C++ string
string outCppStr;
cout << "Enter a String: ";
cin >> outCppStr;
// Step 3: Convert the C++ string to C-string, then to JNI String (jstring) and return
return env->NewStringUTF(outCppStr.c_str());
}
使用g++编译上面的代码:
// MinGW GCC under Windows
> g++ -Wl,--add-stdcall-alias -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.cpp
需要注意的是,在C++中,本地string类的函数调用语法不一样。在C++中,我们使用env->来调用,而不是(env*)->。同时,在C++函数中不需要JNIEnv*这个参数了。
传递基本类型的数组
JNI 代码:TestJNIPrimitiveArray.java
public class TestJNIPrimitiveArray {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Declare a native method sumAndAverage() that receives an int[] and
// return a double[2] array with [0] as sum and [1] as average
private native double[] sumAndAverage(int[] numbers);
// Test Driver
public static void main(String args[]) {
int[] numbers = {22, 33, 33};
double[] results = new TestJNIPrimitiveArray().sumAndAverage(numbers);
System.out.println("In Java, the sum is " + results[0]);
System.out.println("In Java, the average is " + results[1]);
}
}
C语言实现:TestJNIPrimitiveArray.c
头文件TestJNIPrimitiveArray.h包含以下函数声明:
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_average (JNIEnv *, jobject, jintArray);
在Java中,array是指一种类型,类似于类。一共有9种java的array,8个基本类型的array和一个object的array。JNI针对java的基本类型都定义了相应的array:jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray,并且也有面向object的jobjectArray。 同样地,你需要在JNI array和Native array之间进行转换,JNI系统已经为我们提供了一系列的接口函数: 1. 使用jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)将jintarray转换成C的jint[] 2. 使用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程序需要: 1. 接受来自java的JNI array,然后转换成本地array 2. 进行需要的数据操作 3. 将需要返回的数据转换成jni的array,然后返回 下面是C代码实现的TestJNIPrimitiveArray.c:
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitiveArray.h"
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage
(JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
// Step 1: Convert the incoming JNI jintarray to C's jint[]
jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
if (NULL == inCArray) return NULL;
jsize length = (*env)->GetArrayLength(env, inJNIArray);
// Step 2: Perform its intended operations
jint sum = 0;
int i;
for (i = 0; i < length; i++) {
sum += inCArray[i];
}
jdouble average = (jdouble)sum / length;
(*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0); // release resources
jdouble outCArray[] = {sum, average};
// Step 3: Convert the C's Native jdouble[] to JNI jdoublearray, and return
jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2); // allocate
if (NULL == outJNIArray) return NULL;
(*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2, outCArray); // copy
return outJNIArray;
}
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对象实例的变量
为了访问对象中的变量,我们需要: 1. 调用GetObjectClass()获得目标对象的类引用 2. 从上面获得的类引用中获得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。 3. 基于上面获得的Field ID,使用GetObjectField() 或者 Get_primitive-type_Field()函数来从中解析出我们想要的数据 4. 使用SetObjectField() 或者 Set_primitive-type_Field()函数来修改变量 JNI中用来访问实例变量的函数有:
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, NativeType value);
// Get/Set the value of an instance variable of an object
// <type> includes each of the eight primitive types plus Object.
JNI程序:TestJNIInstanceVariable.java
public class TestJNIInstanceVariable {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Instance variables
private int number = 88;
private String message = "Hello from Java";
// Declare a native method that modifies the instance variables
private native void modifyInstanceVariable();
// Test Driver
public static void main(String args[]) {
TestJNIInstanceVariable test = new TestJNIInstanceVariable();
test.modifyInstanceVariable();
System.out.println("In Java, int is " + test.number);
System.out.println("In Java, String is " + test.message);
}
}
这个类包含了两个private实例变量,一个int,一个String对象。然后我们在main中调用本地函数modifyInstanceVariable来修改这两个变量。 C代码实现:TestJNIInstanceVariable.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIInstanceVariable.h"
JNIEXPORT void JNICALL Java_TestJNIInstanceVariable_modifyInstanceVariable
(JNIEnv *env, jobject thisObj) {
// Get a reference to this object's class
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
// int
// Get the Field ID of the instance variables "number"
jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
if (NULL == fidNumber) return;
// Get the int given the Field ID
jint number = (*env)->GetIntField(env, thisObj, fidNumber);
printf("In C, the int is %d\n", number);
// Change the variable
number = 99;
(*env)->SetIntField(env, thisObj, fidNumber, number);
// Get the Field ID of the instance variables "message"
jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
if (NULL == fidMessage) return;
// String
// Get the object given the Field ID
jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);
// Create a C-string with the JNI String
const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
if (NULL == cStr) return;
printf("In C, the string is %s\n", cStr);
(*env)->ReleaseStringUTFChars(env, message, cStr);
// Create a new C-string and assign to the JNI string
message = (*env)->NewStringUTF(env, "Hello from C");
if (NULL == message) return;
// modify the instance variables
(*env)->SetObjectField(env, thisObj, fidMessage, message);
}
访问类中的static变量
JNI中用来访问类中的static变量的函数如下:
jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Returns the field ID for a static variable of a class.
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);
// Get/Set the value of a static variable of a class.
// <type> includes each of the eight primitive types plus Object.
访问类中的static变量类似于上面访问普通的实例变量,只是我们这里使用的函数是GetStaticFieldID(), Get|SetStaticObjectField(), Get|SetStatic_Primitive-type_Field()。 JNI 程序: TestJNIStaticVariable.java
public class TestJNIStaticVariable {
static {
System.loadLibrary("myjni"); // nyjni.dll (Windows) or libmyjni.so (Unixes)
}
// Static variables
private static double number = 55.66;
// Declare a native method that modifies the static variable
private native void modifyStaticVariable();
// Test Driver
public static void main(String args[]) {
TestJNIStaticVariable test = new TestJNIStaticVariable();
test.modifyStaticVariable();
System.out.println("In Java, the double is " + number);
}
}
C语言实现:C Implementation - TestJNIStaticVariable.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIStaticVariable.h"
JNIEXPORT void JNICALL Java_TestJNIStaticVariable_modifyStaticVariable
(JNIEnv *env, jobject thisObj) {
// Get a reference to this object's class
jclass cls = (*env)->GetObjectClass(env, thisObj);
// Read the int static variable and modify its value
jfieldID fidNumber = (*env)->GetStaticFieldID(env, cls, "number", "D");
if (NULL == fidNumber) return;
jdouble number = (*env)->GetStaticDoubleField(env, cls, fidNumber);
printf("In C, the double is %f\n", number);
number = 77.88;
(*env)->SetStaticDoubleField(env, cls, fidNumber, number);
}
回调实例的普通和static方法
为了能够回调实例中的方法,我们需要: 1. 通过GetObjectClass()函数获得这个实例的类对象 2. 从上面获得类对象中,调用GetMethodID()函数来获得Method ID,Method ID表示了实例中的某个方法的抽象。你需要提供这个方法的名字和签名信息,签名规则和变量类似。签名的格式是这样的:(parameters)return-type。如果我们实在觉得jni的签名不好记忆的话,我们可以是用JDK为我们提供的工具javap来获得某个class类中的所有方法的签名,使用-s选项表示打印签名,-p表示显示private成员:
> javap --help
> javap -s -p TestJNICallBackMethod
.......
private void callback();
Signature: ()V
private void callback(java.lang.String);
Signature: (Ljava/lang/String;)V
private double callbackAverage(int, int);
Signature: (II)D
private static java.lang.String callbackStatic();
Signature: ()Ljava/lang/String;
.......
从上面的输出我们可以清楚地看到类中每一个方法的签名。 3. 基于上面我们获得的Method ID,我们可以调用_Primitive-type_Method() 或者 CallVoidMethod() 或者 CallObjectMethod()来调用这个方法。如果某个方法需要参数的话,就在后面跟上参数即可。 4. 如果想要调用一个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, const char *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.
你可以在native代码中回调java中的普通或者static的方法。下面是实例: JNI程序:TestJNICallBackMethod.java
public class TestJNICallBackMethod {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// 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";
}
// Test Driver
public static void main(String args[]) {
new TestJNICallBackMethod().nativeMethod();
}
}
这个类中声明了一个native函数nativeMethod(),并且在main方法中调用了这个函数。nativeMethod()这个函数会回调这个类中定义的各种方法。 C语言实现:TestJNICallBackMethod.c
#include <jni.h>
#include <stdio.h>
#include "TestJNICallBackMethod.h"
JNIEXPORT void JNICALL Java_TestJNICallBackMethod_nativeMethod
(JNIEnv *env, jobject thisObj) {
// Get a class reference for this object
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
// Get the Method ID for method "callback", which takes no arg and return void
jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V");
if (NULL == midCallBack) return;
printf("In C, call back Java's callback()\n");
// Call back the method (which returns void), baed on the Method ID
(*env)->CallVoidMethod(env, thisObj, midCallBack);
jmethodID midCallBackStr = (*env)->GetMethodID(env, thisClass,
"callback", "(Ljava/lang/String;)V");
if (NULL == midCallBackStr) return;
printf("In C, call back Java's called(String)\n");
jstring message = (*env)->NewStringUTF(env, "Hello from C");
(*env)->CallVoidMethod(env, thisObj, midCallBackStr, message);
jmethodID midCallBackAverage = (*env)->GetMethodID(env, thisClass,
"callbackAverage", "(II)D");
if (NULL == midCallBackAverage) return;
jdouble average = (*env)->CallDoubleMethod(env, thisObj, midCallBackAverage, 2, 3);
printf("In C, the average is %f\n", average);
jmethodID midCallBackStatic = (*env)->GetStaticMethodID(env, thisClass,
"callbackStatic", "()Ljava/lang/String;");
if (NULL == midCallBackStatic) return;
jstring resultJNIStr = (*env)->CallStaticObjectMethod(env, thisClass, midCallBackStatic);
const char *resultCStr = (*env)->GetStringUTFChars(env, resultJNIStr, NULL);
if (NULL == resultCStr) return;
printf("In C, the returned string is %s\n", resultCStr);
(*env)->ReleaseStringUTFChars(env, resultJNIStr, resultCStr);
}
回调复写的父类实例方法
JNI提供了一系列的形如 CallNonvirtual_Type_Method()之类的函数来调用父类实例的方法: 1. 首先获得Method ID,使用GetMethodID() 2. 基于上获得的Method ID,通过调用 CallNonvirtual_Type_Method()函数来调用相应的方法,并且在参数中给出object,父类和参数列表。 JNI中用来访问父类方法的函数:
NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);
创建Object和Object arrays
你可以在native代码中构造jobject和jobjectarray,通过调用NewObject() 和 newObjectArray()函数,然后讲它们返回给java代码。
回调Java构造器来创建一个新的java对象
JNI中用于创建对象(jobject)的函数有:
jclass FindClass(JNIEnv *env, const char *name);
jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
// Constructs a new Java object. The method ID indicates which constructor method to invoke
jobject AllocObject(JNIEnv *env, jclass cls);
// Allocates a new Java object without invoking any of the constructors for the object.
回调一个构造器和回调其他的方法是类似的,首先通过init作为方法名,V作为返回值来获得Method ID,然后通过NewObject()函数来构建一个java类对象。 JNI程序:TestJavaConstructor.java
public class TestJNIConstructor {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Native method that calls back the constructor and return the constructed object.
// Return an Integer object with the given int.
private native Integer getIntegerObject(int number);
public static void main(String args[]) {
TestJNIConstructor obj = new TestJNIConstructor();
System.out.println("In Java, the number is :" + obj.getIntegerObject(9999));
}
}
这个类声明了一个getIntegerObject的native方法,这个方法接受一个int的数据,然后在native代码中创建一个Integer类型的对象,其中的值就是这个值。 C代码实现:TestJavaConstructor.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIConstructor.h"
JNIEXPORT jobject JNICALL Java_TestJNIConstructor_getIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
// Get a class reference for java.lang.Integer
jclass cls = (*env)->FindClass(env, "java/lang/Integer");
// Get the Method ID of the constructor which takes an int
jmethodID midInit = (*env)->GetMethodID(env, 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(env, cls, midInit, number);
// Try runnning the toString() on this newly create object
jmethodID midToString = (*env)->GetMethodID(env, cls, "toString", "()Ljava/lang/String;");
if (NULL == midToString) return NULL;
jstring resultStr = (*env)->CallObjectMethod(env, newObj, midToString);
const char *resultCStr = (*env)->GetStringUTFChars(env, resultStr, NULL);
printf("In C: the number is %s\n", resultCStr);
return newObj;
}
对象(object)的array
不像基本数据类型的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.
JNI程序:TestJNIObjectArray.java
import java.util.ArrayList;
public class TestJNIObjectArray {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Native method that receives an Integer[] and
// returns a Double[2] with [0] as sum and [1] as average
private native Double[] sumAndAverage(Integer[] numbers);
public static void main(String args[]) {
Integer[] numbers = {11, 22, 32}; // auto-box
Double[] results = new TestJNIObjectArray().sumAndAverage(numbers);
System.out.println("In Java, the sum is " + results[0]); // auto-unbox
System.out.println("In Java, the average is " + results[1]);
}
}
为了简单起见,这个类声明了一个native方法,这个方法接受一个Integer类型的array,然后在本地代码中计算这个array中的数的和与平均数,然后讲这两个数以Double array的形式返回。 C代码实现:TestJNIObjectArray.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIObjectArray.h"
JNIEXPORT jobjectArray JNICALL Java_TestJNIObjectArray_sumAndAverage
(JNIEnv *env, jobject thisObj, jobjectArray inJNIArray) {
// Get a class reference for java.lang.Integer
jclass classInteger = (*env)->FindClass(env, "java/lang/Integer");
// Use Integer.intValue() to retrieve the int
jmethodID midIntValue = (*env)->GetMethodID(env, classInteger, "intValue", "()I");
if (NULL == midIntValue) return NULL;
// Get the value of each Integer object in the array
jsize length = (*env)->GetArrayLength(env, inJNIArray);
jint sum = 0;
int i;
for (i = 0; i < length; i++) {
jobject objInteger = (*env)->GetObjectArrayElement(env, inJNIArray, i);
if (NULL == objInteger) return NULL;
jint value = (*env)->CallIntMethod(env, objInteger, midIntValue);
sum += value;
}
double average = (double)sum / length;
printf("In C, the sum is %d\n", sum);
printf("In C, the average is %f\n", average);
// Get a class reference for java.lang.Double
jclass classDouble = (*env)->FindClass(env, "java/lang/Double");
// Allocate a jobjectArray of 2 java.lang.Double
jobjectArray outJNIArray = (*env)->NewObjectArray(env, 2, classDouble, NULL);
// Construct 2 Double objects by calling the constructor
jmethodID midDoubleInit = (*env)->GetMethodID(env, classDouble, "<init>", "(D)V");
if (NULL == midDoubleInit) return NULL;
jobject objSum = (*env)->NewObject(env, classDouble, midDoubleInit, (double)sum);
jobject objAve = (*env)->NewObject(env, classDouble, midDoubleInit, average);
// Set to the jobjectArray
(*env)->SetObjectArrayElement(env, outJNIArray, 0, objSum);
(*env)->SetObjectArrayElement(env, outJNIArray, 1, objAve);
return outJNIArray;
}
本地和全局引用
管理引用是编写高效程序的关键。比如,我们会在本地代码中经常使用FindClass(), GetMethodID(), GetFieldID()来会的一个jclass,jmethodID和jfieldID。其实这些变量应该只是在第一次的时候获取,之后直接使用就可以了,而不用每次都去获取一遍,这样可以提高程序执行效率。 JNI讲本地代码中的对象引用分为了两种类型: 本地和全局引用: 1. 本地引用是在本地代码中创建的,并且当函数退出或者返回的时候就被free了。它的有效范围只是这个native函数的内部。你也可以调用DeleteLocalRef()来显式地将某个本地引用作废,这样可以让垃圾回收时能够将这部分回收。作为参数传递到本地函数中的对象引用是本地引用,所有的从JNI函数返回的java对象(jobject)都是本地引用。 2. 全局引用会保留直到程序员调用DeleteGlobalRef()手动free掉他们,你可以使用NewGlobalRef()函数从本地引用创建一个全局引用。 下面我们给出一个例子。
public class TestJNIReference {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// A native method that returns a java.lang.Integer with the given int.
private native Integer getIntegerObject(int number);
// Another native method that also returns a java.lang.Integer with the given int.
private native Integer anotherGetIntegerObject(int number);
public static void main(String args[]) {
TestJNIReference test = new TestJNIReference();
System.out.println(test.getIntegerObject(1));
System.out.println(test.getIntegerObject(2));
System.out.println(test.anotherGetIntegerObject(11));
System.out.println(test.anotherGetIntegerObject(12));
System.out.println(test.getIntegerObject(3));
System.out.println(test.anotherGetIntegerObject(13));
}
}
上面的JNI程序声明了两个native函数,这两个都创建并且返回java.lang.Integer对象。在C代码实现中,我们需要获得java.lang.Integer的类引用,然后我们从中找到构造器的method ID,然后调用构造器。然而,我们希望,将我们获得的class引用和Method ID缓存起来,这样我们下次在使用的时候就不用再次去获取了。下面是我们的C代码,我们希望这样可以OK(然而事实是不行!!!):
#include <jni.h>
#include <stdio.h>
#include "TestJNIReference.h"
// Global Reference to the Java class "java.lang.Integer"
static jclass classInteger;
static jmethodID midIntegerInit;
jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {
// Get a class reference for java.lang.Integer if missing
if (NULL == classInteger) {
printf("Find java.lang.Integer\n");
classInteger = (*env)->FindClass(env, "java/lang/Integer");
}
if (NULL == classInteger) return NULL;
// Get the Method ID of the Integer's constructor if missing
if (NULL == midIntegerInit) {
printf("Get Method ID for java.lang.Integer's constructor\n");
midIntegerInit = (*env)->GetMethodID(env, classInteger, "<init>", "(I)V");
}
if (NULL == midIntegerInit) return NULL;
// Call back constructor to allocate a new instance, with an int argument
jobject newObj = (*env)->NewObject(env, classInteger, midIntegerInit, number);
printf("In C, constructed java.lang.Integer with number %d\n", number);
return newObj;
}
JNIEXPORT jobject JNICALL Java_TestJNIReference_getIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
return getInteger(env, thisObj, number);
}
JNIEXPORT jobject JNICALL Java_TestJNIReference_anotherGetIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
return getInteger(env, thisObj, number);
}
在上面的程序中,我们调用FindClass()获得了java.lang.Integer类引用,然后把它保存在一个全局静态的变量中。然而,在第二次调用中这个引用却无效了(并不是NULL)。这是因为FindClass()返回的是本地类引用,一旦当getInteger函数返回的时候,这个局部引用就失效了。 为了解决这个问题,我们需要从局部引用中创建一个全局引用,然后再赋值给全局static变量:
// Get a class reference for java.lang.Integer if missing
if (NULL == classInteger) {
printf("Find java.lang.Integer\n");
// FindClass returns a local reference
jclass classIntegerLocal = (*env)->FindClass(env, "java/lang/Integer");
// Create a global reference from the local reference
classInteger = (*env)->NewGlobalRef(env, classIntegerLocal);
// No longer need the local reference, free it!
(*env)->DeleteLocalRef(env, classIntegerLocal);
}
需要注意的是,jmethodID和jfieldID并不是jobject,因此他们不能创建一个全局引用!!
JNI常用方法
Java JNI(Java Native Interface)提供了一系列函数,用于在本地代码(C/C++)中与Java虚拟机交互。以下是主要的JNI函数分类:
一、类操作函数
// 根据类名查找类(例如 "java/lang/String")
jclass FindClass(const char *name);
// 获取某个类的父类
jclass GetSuperclass(jclass clazz);
// 检查clazz1是否可以安全地转换为clazz2(即clazz1是否是clazz2的子类或实现类)
jboolean IsAssignableFrom(jclass clazz1, jclass clazz2);
场景:检查 java.util.ArrayList 是否是 java.util.List 的子类。
void TestClassOperations(JNIEnv *env) {
// 1. FindClass: 查找 ArrayList 类
jclass arrayListClass = env->FindClass("java/util/ArrayList");
if (arrayListClass == NULL) return; // 异常处理
// 2. FindClass: 查找 List 类
jclass listClass = env->FindClass("java/util/List");
// 3. GetSuperclass: 获取 ArrayList 的父类 (应该是 AbstractList)
jclass superClass = env->GetSuperclass(arrayListClass);
// 4. IsAssignableFrom: 检查 ArrayList 是否可以转型为 List (即是否实现了 List 接口)
jboolean isAssignable = env->IsAssignableFrom(arrayListClass, listClass);
printf("ArrayList is assignable to List: %d\n", isAssignable);
}
二、对象操作函数
// 分配对象内存但不调用构造函数(非常少用,通常用于绕过构造逻辑)
jobject AllocObject(jclass clazz);
// 创建对象并运行构造函数(标准方式)
jobject NewObject(jclass clazz, jmethodID methodID, ...);
// 创建对象(参数通过 va_list 传递,用于封装JNI函数)
jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args);
// 创建对象(参数通过 jvalue* 联合体数组传递)
jobject NewObjectA(jclass clazz, jmethodID methodID, const jvalue *args);
// 获取对象的类(相当于Java中的 obj.getClass())
jclass GetObjectClass(jobject obj);
// 检查对象是否是某个类的实例(相当于Java中的 instanceof)
jboolean IsInstanceOf(jobject obj, jclass clazz);
// 检查两个引用是否指向同一个Java对象(相当于 ==)
jboolean IsSameObject(jobject ref1, jobject ref2);
场景:绕过构造函数创建一个对象,然后用标准方式创建对象,并比较引用。
void TestObjectOperations(JNIEnv *env) {
jclass clazz = env->FindClass("com/example/MyData");
jmethodID constructorId = env->GetMethodID(clazz, "<init>", "()V");
// 1. AllocObject: 分配内存但不调用构造函数 (极少使用,通常用于反序列化)
jobject rawObj = env->AllocObject(clazz);
// 2. NewObject: 标准创建对象
jobject validObj = env->NewObject(clazz, constructorId);
// 3. NewObjectV / NewObjectA: 变体展示 (通常封装在工具函数中)
// 假设我们有一个 va_list args 或 jvalue args[]
// jobject objV = env->NewObjectV(clazz, constructorId, args);
// jobject objA = env->NewObjectA(clazz, constructorId, args_array);
// 4. GetObjectClass: 反向获取对象的类
jclass objClass = env->GetObjectClass(validObj);
// 5. IsInstanceOf: 检查类型
if (env->IsInstanceOf(rawObj, clazz)) {
// ...
}
// 6. IsSameObject: 检查两个引用是否指向同一对象 (类似于 Java 的 ==)
jboolean same = env->IsSameObject(rawObj, validObj); // 应该是 false
}
三、全局引用和局部引用管理
// 创建全局引用(跨线程、跨函数调用有效,直到显式释放)
jobject NewGlobalRef(jobject obj);
// 删除全局引用
void DeleteGlobalRef(jobject globalRef);
// 创建局部引用(通常用于延长局部引用的生命周期或显式管理)
jobject NewLocalRef(jobject ref);
// 删除局部引用(尽早释放不再使用的对象,避免局部引用表溢出)
void DeleteLocalRef(jobject localRef);
// 创建弱全局引用(不会阻止对象被GC回收)
jweak NewWeakGlobalRef(jobject obj);
// 删除弱全局引用
void DeleteWeakGlobalRef(jweak obj);
// 创建一个新的局部引用帧(管理局部引用的生命周期作用域)
jint PushLocalFrame(jint capacity);
// 弹出当前局部引用帧,释放帧内所有局部引用,仅保留result返回给上一层
jobject PopLocalFrame(jobject result);
// 确保当前线程有能力创建指定数量的局部引用
jint EnsureLocalCapacity(jint capacity);
场景:在函数中管理内存帧,创建全局缓存,并处理弱引用。
jobject globalCache; // 假设这是全局变量
void TestReferences(JNIEnv *env, jobject data) {
// 1. PushLocalFrame: 开启一个新的局部引用作用域,容量至少为16
// 之后创建的所有局部引用都在这个帧里
env->PushLocalFrame(16);
// 2. NewGlobalRef: 创建全局引用 (用于跨函数保存)
if (globalCache == NULL) {
globalCache = env->NewGlobalRef(data);
}
// 3. NewLocalRef: 显式创建局部引用
jobject tempRef = env->NewLocalRef(data);
// 4. NewWeakGlobalRef: 创建弱全局引用 (允许被GC回收)
jweak weakRef = env->NewWeakGlobalRef(data);
// 业务逻辑...
// 5. DeleteLocalRef: 显式删除不再需要的局部引用
env->DeleteLocalRef(tempRef);
// 6. EnsureLocalCapacity: 确保当前帧还能再创建至少10个引用
env->EnsureLocalCapacity(10);
// 7. PopLocalFrame: 销毁帧,释放帧内所有局部引用,
// 但保留 NULL 这个对象返回给上一层 (此处不需要返回值)
env->PopLocalFrame(NULL);
// 清理全局引用 (通常在析构或 cleanup 函数中)
// env->DeleteGlobalRef(globalCache);
// env->DeleteWeakGlobalRef(weakRef);
}
四、访问字段函数
实例字段
// 获取实例字段的ID
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
// 获取字段值(根据字段类型调用不同函数)
jobject GetObjectField(jobject obj, jfieldID fieldID);
jboolean GetBooleanField(jobject obj, jfieldID fieldID);
jbyte GetByteField(jobject obj, jfieldID fieldID);
jchar GetCharField(jobject obj, jfieldID fieldID);
jshort GetShortField(jobject obj, jfieldID fieldID);
jint GetIntField(jobject obj, jfieldID fieldID);
jlong GetLongField(jobject obj, jfieldID fieldID);
jfloat GetFloatField(jobject obj, jfieldID fieldID);
jdouble GetDoubleField(jobject obj, jfieldID fieldID);
// 设置字段值(根据字段类型调用不同函数)
void SetObjectField(jobject obj, jfieldID fieldID, jobject value);
void SetBooleanField(jobject obj, jfieldID fieldID, jboolean value);
void SetByteField(jobject obj, jfieldID fieldID, jbyte value);
void SetCharField(jobject obj, jfieldID fieldID, jchar value);
void SetShortField(jobject obj, jfieldID fieldID, jshort value);
void SetIntField(jobject obj, jfieldID fieldID, jint value);
void SetLongField(jobject obj, jfieldID fieldID, jlong value);
void SetFloatField(jobject obj, jfieldID fieldID, jfloat value);
void SetDoubleField(jobject obj, jfieldID fieldID, jdouble value);
静态字段
// 获取静态字段ID
jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);
// 获取静态字段值(对象类型,其他基本类型需调用对应GetStatic<Type>Field)
jobject GetStaticObjectField(jclass clazz, jfieldID fieldID);
// 设置静态字段值(对象类型,其他基本类型需调用对应SetStatic<Type>Field)
void SetStaticObjectField(jclass clazz, jfieldID fieldID, jobject value);
场景:读取和修改 Java 对象的 int 实例字段和 String 静态字段。
void TestFieldAccess(JNIEnv *env, jobject obj) {
jclass clazz = env->GetObjectClass(obj);
// --- 实例字段操作 ---
// 1. GetFieldID: 获取 int age; 的ID
jfieldID fidAge = env->GetFieldID(clazz, "age", "I");
// 2. GetIntField: 获取值
jint age = env->GetIntField(obj, fidAge);
// 3. SetIntField: 修改值
env->SetIntField(obj, fidAge, age + 1);
// (其他类型如 SetObjectField, SetDoubleField 用法完全一致,只需换函数名)
// --- 静态字段操作 ---
// 4. GetStaticFieldID: 获取 static String name; 的ID
jfieldID fidName = env->GetStaticFieldID(clazz, "name", "Ljava/lang/String;");
// 5. GetStaticObjectField: 获取静态对象
jstring nameStr = (jstring)env->GetStaticObjectField(clazz, fidName);
// 6. SetStaticObjectField: 修改静态对象
env->SetStaticObjectField(clazz, fidName, nameStr);
}
五、调用方法函数
实例方法
// 获取实例方法ID
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
// 调用实例方法(标准可变参数版本)
void CallVoidMethod(jobject obj, jmethodID methodID, ...);
jobject CallObjectMethod(jobject obj, jmethodID methodID, ...);
jboolean CallBooleanMethod(jobject obj, jmethodID methodID, ...);
// ... (其他基本类型如 CallIntMethod, CallFloatMethod 等以此类推)
// 调用实例方法(va_list 参数版本)
void CallVoidMethodV(jobject obj, jmethodID methodID, va_list args);
// ... (其他类型以此类推)
// 调用实例方法(jvalue* 数组参数版本)
void CallVoidMethodA(jobject obj, jmethodID methodID, const jvalue *args);
// ... (其他类型以此类推)
非虚方法调用 (Non-virtual)
通常用于调用父类方法,即 Java 中的 super.method(),忽略子类的重写。
// 调用指定类clazz中的方法实现,而不走多态机制
void CallNonvirtualVoidMethod(jobject obj, jclass clazz, jmethodID methodID, ...);
jobject CallNonvirtualObjectMethod(jobject obj, jclass clazz, jmethodID methodID, ...);
// ... (其他类型如 CallNonvirtualIntMethod 等以此类推)
静态方法
// 获取静态方法ID
jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig);
// 调用静态方法
void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...);
jobject CallStaticObjectMethod(jclass clazz, jmethodID methodID, ...);
// ... (其他类型以此类推)
场景:调用普通方法、父类方法(非虚调用)和静态方法。
void TestMethodCalls(JNIEnv *env, jobject obj) {
jclass clazz = env->GetObjectClass(obj);
// --- 1. 实例方法 ---
jmethodID midPrint = env->GetMethodID(clazz, "printInfo", "()V");
// CallVoidMethod: 调用返回 void 的方法
env->CallVoidMethod(obj, midPrint);
// 变体展示:CallVoidMethodA (使用 jvalue 数组)
// jvalue args[1];
// args[0].i = 10;
// env->CallVoidMethodA(obj, midPrint, args);
// --- 2. 非虚方法 (Non-virtual) ---
// 场景:调用父类的实现 (super.toString())
jclass superClazz = env->GetSuperclass(clazz);
jmethodID midToString = env->GetMethodID(superClazz, "toString", "()Ljava/lang/String;");
// 即使 obj 是子类实例,也强制执行父类代码
jstring str = (jstring)env->CallNonvirtualObjectMethod(obj, superClazz, midToString);
// --- 3. 静态方法 ---
jmethodID midStatic = env->GetStaticMethodID(clazz, "staticHelper", "(I)Z");
// CallStaticBooleanMethod
jboolean result = env->CallStaticBooleanMethod(clazz, midStatic, 100);
}
六、字符串操作函数
// 从Unicode字符数组创建 java.lang.String
jstring NewString(const jchar *unicodeChars, jsize len);
// 从UTF-8编码的C字符串创建 java.lang.String
jstring NewStringUTF(const char *bytes);
// 获取字符串长度(字符数)
jsize GetStringLength(jstring str);
// 获取字符串的UTF-8字节长度
jsize GetStringUTFLength(jstring str);
// 获取Unicode字符指针(可能导致内存复制)
const jchar* GetStringChars(jstring str, jboolean *isCopy);
// 获取UTF-8字符指针(常用,可能导致内存复制)
const char* GetStringUTFChars(jstring str, jboolean *isCopy);
// 释放 GetStringChars 获取的指针
void ReleaseStringChars(jstring str, const jchar *chars);
// 释放 GetStringUTFChars 获取的指针
void ReleaseStringUTFChars(jstring str, const char* utf);
// 将字符串的指定区域复制到缓冲区(避免分配内存)
void GetStringRegion(jstring str, jsize start, jsize len, jchar *buf);
void GetStringUTFRegion(jstring str, jsize start, jsize len, char *buf);
// 获取字符串指针(尝试禁止GC,速度快但有限制,期间不能调用其他JNI函数)
const jchar* GetStringCritical(jstring str, jboolean *isCopy);
// 释放关键字符串指针
void ReleaseStringCritical(jstring str, const jchar *carray);
场景:将 Java 字符串转换为 C 字符串处理,使用 Critical 方式优化,并截取区域。
void TestStringOperations(JNIEnv *env, jstring jStr) {
// 1. GetStringLength: 获取字符数
jsize len = env->GetStringLength(jStr);
// 2. GetStringUTFLength: 获取UTF-8字节数
jsize utfLen = env->GetStringUTFLength(jStr);
// 3. NewStringUTF: 创建新字符串
jstring newStr = env->NewStringUTF("Hello from C++");
// 4. GetStringUTFChars: 获取 UTF-8 缓冲区 (常用)
const char *utfChars = env->GetStringUTFChars(jStr, NULL);
printf("Java String: %s\n", utfChars);
// 5. ReleaseStringUTFChars: 必须释放
env->ReleaseStringUTFChars(jStr, utfChars);
// 6. GetStringCritical:以此方式获取尽可能不发生GC,速度快
// 注意:在此对之间严禁调用其他JNI函数
const jchar *criticalChars = env->GetStringCritical(jStr, NULL);
// ... 处理字符 ...
// 7. ReleaseStringCritical: 释放
env->ReleaseStringCritical(jStr, criticalChars);
// 8. GetStringRegion: 将部分内容拷贝到 buffer,不分配新内存
jchar buffer[10];
env->GetStringRegion(jStr, 0, 5, buffer); // 拷贝前5个字符
}
七、数组操作函数
基本数组操作
// 获取数组长度
jsize GetArrayLength(jarray array);
// 对象数组操作
// 创建对象数组
jobjectArray NewObjectArray(jsize length, jclass elementClass, jobject initialElement);
// 获取对象数组中的元素
jobject GetObjectArrayElement(jobjectArray array, jsize index);
// 设置对象数组中的元素
void SetObjectArrayElement(jobjectArray array, jsize index, jobject value);
原始类型数组创建
// 创建各种基础类型的数组
jbooleanArray NewBooleanArray(jsize length);
jbyteArray NewByteArray(jsize length); // 常用
jcharArray NewCharArray(jsize length);
jshortArray NewShortArray(jsize length);
jintArray NewIntArray(jsize length);
jlongArray NewLongArray(jsize length);
jfloatArray NewFloatArray(jsize length);
jdoubleArray NewDoubleArray(jsize length);
数组元素访问
// 获取数组元素的指针(可能会复制数组,isCopy返回是否复制)
jboolean* GetBooleanArrayElements(jbooleanArray array, jboolean *isCopy);
jbyte* GetByteArrayElements(jbyteArray array, jboolean *isCopy); // 常用
// ... (其他类型类似)
// 释放数组指针(并根据mode决定是否将修改写回Java数组)
// mode: 0(写回并释放), JNI_COMMIT(写回不释放), JNI_ABORT(不写回并释放)
void ReleaseBooleanArrayElements(jbooleanArray array, jboolean *elems, jint mode);
void ReleaseByteArrayElements(jbyteArray array, jbyte *elems, jint mode);
// ... (其他类型类似)
// 将数组的指定区域复制到C缓冲区(推荐用于少量数据拷贝,无需管理指针释放)
void GetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len, jboolean *buf);
// 将C缓冲区的数据写入数组指定区域
void SetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len, const jboolean *buf);
// ... (其他类型类似)
// 类似于 GetStringCritical,暂停GC直接访问数组内存(极快但有锁限制)
void* GetPrimitiveArrayCritical(jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(jarray array, void *carray, jint mode);
场景:处理基本类型数组(求和)和对象数组(创建字符串数组)。
void TestArrayOperations(JNIEnv *env, jintArray intArr) {
// 1. GetArrayLength
jsize len = env->GetArrayLength(intArr);
// --- 基本类型数组 ---
// 2. GetIntArrayElements: 获取指针操作整个数组
jint *body = env->GetIntArrayElements(intArr, NULL);
for (int i=0; i<len; i++) body[i]++; // 修改数组
// 3. ReleaseIntArrayElements: 释放并提交修改 (0模式)
env->ReleaseIntArrayElements(intArr, body, 0);
// 4. GetIntArrayRegion: 仅拷贝一小块 (更高效,无需 Release)
jint buf[4];
env->GetIntArrayRegion(intArr, 0, 4, buf);
// 5. SetIntArrayRegion: 写回
env->SetIntArrayRegion(intArr, 0, 4, buf);
// 6. GetPrimitiveArrayCritical: 类似字符串的 Critical,锁GC
void* criticalData = env->GetPrimitiveArrayCritical(intArr, NULL);
// ... 快速读写 ...
env->ReleasePrimitiveArrayCritical(intArr, criticalData, 0);
// --- 对象数组 ---
// 7. NewObjectArray: 创建 String[]
jclass strCls = env->FindClass("java/lang/String");
jobjectArray strArr = env->NewObjectArray(5, strCls, NULL);
// 8. SetObjectArrayElement: 赋值
jstring val = env->NewStringUTF("Item");
env->SetObjectArrayElement(strArr, 0, val);
// 9. GetObjectArrayElement: 取值
jstring elem = (jstring)env->GetObjectArrayElement(strArr, 0);
}
八、异常处理函数
// 抛出一个现有的异常对象
jint Throw(jthrowable obj);
// 创建并抛出指定类型的异常(带消息)
jint ThrowNew(jclass clazz, const char *message);
// 检查是否有异常发生(返回异常对象,若无则返回NULL)
jthrowable ExceptionOccurred();
// 将异常堆栈信息打印到标准错误输出
void ExceptionDescribe();
// 清除当前挂起的异常
void ExceptionClear();
// 快速检查是否有异常发生(返回布尔值)
jboolean ExceptionCheck();
// 抛出致命错误并终止JVM
void FatalError(const char *msg);
场景:尝试调用不存在的方法,捕获异常,并抛出一个自定义的新异常。
void TestExceptions(JNIEnv *env) {
// 1. 制造一个异常:查找不存在的类
env->FindClass("com/nonexistent/Class");
// 2. ExceptionCheck: 快速检查是否有异常
if (env->ExceptionCheck()) {
// 3. ExceptionDescribe: 打印堆栈到 stderr (调试用)
env->ExceptionDescribe();
// 4. ExceptionOccurred: 获取异常对象
jthrowable ex = env->ExceptionOccurred();
// 5. ExceptionClear: 清除异常状态,以便后续能继续调用JNI函数
env->ExceptionClear();
// 6. ThrowNew: 抛出一个新的 Java 异常
jclass illegalState = env->FindClass("java/lang/IllegalStateException");
env->ThrowNew(illegalState, "JNI Error recovered and rethrown");
// 7. Throw: 如果想抛出之前获取的 ex 对象
// env->Throw(ex);
}
// 8. FatalError: 发生无法挽回的错误,直接挂掉虚拟机
// env->FatalError("Core dump needed");
}
九、JavaVM管理函数
// 获取JavaVM接口指针(用于跨线程环境)
jint GetJavaVM(JavaVM **vm);
// 获取当前JNI版本(例如 JNI_VERSION_1_6)
jint GetVersion();
// 动态注册本地方法(替代javah生成的长函数名,建立Java方法名与C函数指针的映射)
jint RegisterNatives(jclass clazz, const JNINativeMethod *methods, jint nMethods);
// 取消注册本地方法
jint UnregisterNatives(jclass clazz);
场景:标准的 JNI_OnLoad 函数(Java程序启动时会自动调用JNI的JNI_OnLoad方法)实现。
核心任务:
- 告知 JVM 我们使用的 JNI 版本。
- 捕获并保存
JavaVM \*vm指针到全局变量。 - 动态注册 Native 方法。
#include <jni.h>
#include <stdio.h>
// 1. 定义全局变量,用于在其他地方(如后台线程)访问虚拟机实例
JavaVM *g_vm = NULL;
// 假设的本地实现函数
void nativeMethodImpl(JNIEnv *env, jobject obj) {
printf("Native method called!\n");
}
// 注册方法所需的结构体映射表
static JNINativeMethod methods[] = {
// Java方法名, 签名, C++函数指针
{"nativeMethod", "()V", (void*)&nativeMethodImpl}
};
// 【场景核心】:JNI_OnLoad 是库加载的入口
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
// -----------------------------------------------------------
// 关键点:在此处保存 JavaVM 指针!
// -----------------------------------------------------------
g_vm = vm;
JNIEnv *env;
// 获取当前线程(主线程)的 JNIEnv
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// 查找需要注册方法的类
jclass clazz = env->FindClass("com/example/MyClass");
if (clazz == NULL) return JNI_ERR;
// 动态注册本地方法
if (env->RegisterNatives(clazz, methods, 1) < 0) {
return JNI_ERR;
}
// 返回版本号
return JNI_VERSION_1_6;
}
// 对应卸载时的清理(可选)
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) return;
// 如果需要,可以在这里取消注册,但通常类卸载时会自动处理
// jclass clazz = env->FindClass("com/example/MyClass");
// env->UnregisterNatives(clazz);
}
十、虚拟机操作函数
// 将当前C线程附加到JVM(使其成为Java线程)
jint AttachCurrentThread(JavaVM *vm, void **penv, void *args);
// 将当前线程从JVM分离
jint DetachCurrentThread(JavaVM *vm);
场景:在 C++ 开启的后台线程中,获取 JNIEnv 指针以调用 Java 代码。
C++ 开启的后台线程默认无法通过vm 指针调用Java方法,将本地线程附加到JVM后才能调用Java方法。
核心任务:
- 使用 C++ 标准库创建后台线程。
- 在后台线程中利用全局
g_vm将自己附加(Attach)到 JVM。 - 执行 Java 方法。
- 执行完毕后分离(Detach),防止内存泄漏。
#include <jni.h>
#include <thread> // C++11 线程库
#include <iostream>
// 引用上面保存的全局变量
extern JavaVM *g_vm;
// --- 线程执行体 ---
void BackgroundTask() {
JNIEnv *env;
// 1. 检查全局 vm 是否已初始化
if (g_vm == NULL) {
return;
}
// 2. AttachCurrentThread: 将当前的 C++ 后台线程附加到 JVM
// 成功后,env 指针会被赋值,该线程就变成了"Java线程"
jint res = g_vm->AttachCurrentThread((void**)&env, NULL);
if (res != JNI_OK) {
return; // 附加失败
}
// --- 业务逻辑开始 ---
// 现在可以使用 env 调用 Java 了,例如打印日志
jclass cls = env->FindClass("java/lang/System");
jfieldID fid = env->GetStaticFieldID(cls, "out", "Ljava/io/PrintStream;");
jobject out = env->GetStaticObjectField(cls, fid);
jclass clsPrintStream = env->FindClass("java/io/PrintStream");
jmethodID midPrintln = env->GetMethodID(clsPrintStream, "println", "(Ljava/lang/String;)V");
jstring msg = env->NewStringUTF("Hello from C++ Background Thread!");
env->CallVoidMethod(out, midPrintln, msg);
// 释放局部引用(好习惯,尤其是循环中)
env->DeleteLocalRef(msg);
// --- 业务逻辑结束 ---
// 3. DetachCurrentThread: 极其重要!
// 线程退出前必须分离,否则 JVM 无法回收该线程资源,甚至导致 JVM 崩溃
g_vm->DetachCurrentThread();
}
// --- 触发线程启动的 JNI 函数 ---
extern "C" JNIEXPORT void JNICALL
Java_com_example_MyClass_startNativeThread(JNIEnv *env, jobject obj) {
// 使用 C++ std::thread 创建并启动线程
std::thread t(BackgroundTask);
// detach 使线程在后台独立运行
t.detach();
// 或者使用 t.join() 等待线程结束,视业务需求而定
}
十一、监视器操作函数
// 进入对象的Monitor(相当于 synchronized(obj) { ... )
jint MonitorEnter(jobject obj);
// 退出对象的Monitor(相当于 ... } )
jint MonitorExit(jobject obj);
场景:模拟 Java 的 synchronized(obj) 代码块。
void TestMonitor(JNIEnv *env, jobject obj) {
// 1. MonitorEnter: 加锁
if (env->MonitorEnter(obj) != JNI_OK) {
return; // 加锁失败
}
// --- 临界区代码 ---
// 修改共享数据...
printf("Inside synchronized block\n");
// 2. MonitorExit: 解锁
if (env->MonitorExit(obj) != JNI_OK) {
// 这是一个严重错误,通常意味着逻辑不对
};
}
十二、反射支持函数
// 将 java.lang.reflect.Method/Constructor 对象转换为 jmethodID
jmethodID FromReflectedMethod(jobject method);
// 将 jmethodID 转换为 java.lang.reflect.Method/Constructor 对象
jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic);
// 将 java.lang.reflect.Field 对象转换为 jfieldID
jfieldID FromReflectedField(jobject field);
// 将 jfieldID 转换为 java.lang.reflect.Field 对象
jobject ToReflectedField(jclass cls, jfieldID fieldID, jboolean isStatic);
场景:将 JNI ID 转换为 Java 反射 API 对象。
void TestReflection(JNIEnv *env, jclass clazz, jmethodID mid) {
// 1. ToReflectedMethod: jmethodID -> java.lang.reflect.Method
jobject reflectMethod = env->ToReflectedMethod(clazz, mid, JNI_FALSE);
// 2. FromReflectedMethod: java.lang.reflect.Method -> jmethodID
jmethodID backToMid = env->FromReflectedMethod(reflectMethod);
// (字段 Field 的 From/To 用法完全一致)
}
十三、模块操作函数(Java 9+)
// 获取类所在的模块(java.lang.Module)
jobject GetModule(jclass clazz);
场景:获取类所在的模块
void TestModule(JNIEnv *env, jclass clazz) {
// 1. GetModule: 获取该类所属的 Module 对象
jobject module = env->GetModule(clazz);
// 可以进一步调用 module 对象的方法检查导出状态等
}
JNI释放规则
| 引用类型 | 创建方式 | 是否需要手动释放 | 生命周期 |
|---|---|---|---|
| 局部引用 | FindClass, NewObject | 可选(建议显式) | 方法结束自动释放 |
| 全局引用 | NewGlobalRef | 必须 | 直到调用 DeleteGlobalRef |
| 弱全局引用 | NewWeakGlobalRef | 必须 | 直到调用 DeleteWeakGlobalRef |
jmethodID | GetMethodID | 不需要 | 类卸载时自动失效 |
jfieldID | GetFieldID | 不需要 | 类卸载时自动失效 |
JNI签名规则
以下是JNI方法签名的详细对照表,涵盖常见数据类型及组合情况:
| Java声明 | JNI签名格式 | 规则说明 |
|---|---|---|
| 基本类型 | ||
void method() | ()V | V表示void |
int method(int) | (I)I | I表示int |
double method(double) | (D)D | D表示double |
boolean method(boolean) | (Z)Z | Z表示boolean |
| 数组类型 | ||
int[] method(int[]) | ([I)[I | [I表示int[] |
String[] method(String[]) | ([Ljava/lang/String;)[Ljava/lang/String; | [L全类名;表示对象数组 |
double[][] method(double[][]) | ([[D)[[D | [[D表示double[][] |
| 对象类型 | ||
String method(String) | (Ljava/lang/String;)Ljava/lang/String; | L全类名;表示对象类型 |
Object method(Object) | (Ljava/lang/Object;)Ljava/lang/Object; | 注意末尾必须带分号; |
| 容器类型 | ||
Map method(Map) | (Ljava/util/Map;)Ljava/util/Map; | 泛型会被擦除,签名中不需要体现 |
List<String> method(List<String>) | (Ljava/util/List;)Ljava/util/List; | 泛型信息在JNI签名中不保留 |
| 多参数组合 | ||
void method(int, double) | (ID)V | 按参数顺序拼接:I + D |
String method(int[], String) | ([ILjava/lang/String;)Ljava/lang/String; | 数组[I + 对象Ljava/lang/String; |
| 构造方法 | ||
new MyClass(int, String) | (ILjava/lang/String;)V | 构造方法固定名为<init>,返回类型为V |
| 特殊类型 | ||
void method(MyClass.Inner) | (Lcom/example/MyClass$Inner;)V | 内部类用$分隔 |
void method(int... varargs) | ([I)V | 可变参数在JNI中视为数组 |
常见错误场景示例
泛型陷阱:
// Java方法:public void setMap(Map<String, Integer> map) // 错误签名:(Ljava/util/Map<Ljava/lang/String;Ljava/lang/Integer;>;)V // 正确签名:(Ljava/util/Map;)V多维数组:
// Java方法:public int[][] getMatrix() // 正确签名:()[[I混合参数:
// Java方法:public void update(int id, String name, double[] scores) // 正确签名:(ILjava/lang/String;[D)V
签名生成技巧
使用
javap工具自动生成:javap -s -p YourClassName.classIDE插件(如IntelliJ IDEA的
JNI Helper)可自动生成签名。
JNI调试技巧
首选准备一个示例代码,该代码的主要功能为获取用户,设置最后的用户,返回用户;获取职位,设置最后的职位,返回职位:

准备工作
生成class文件
推荐使用
Ctrl + F9快捷键使用IDEA自带的构建工具进行构建以加快生成class文件的速度
如果是maven项目或Gradle项目可以直接点击compile进行编译

如果没有构建工具可以使用IDEA自带的构建,点击工具栏里的Build->Build Project或使用快捷键Ctrl + F9即可构建,由于Build Project是增量构建即只有改代码的文件会重新构建,有时会有问题,此时可以点击Rebuild Project即可全部重新构建。

获取头文件
点击IDEA的设置,找到Tools -> External Tools,添加如下三个工具

下面是各文件的内容
Generate Header File
生成JNI头文件
$JDKPath$/bin/javah
-jni -classpath "$OutputPath$;$Classpath$" -d $FileParentDir$\$FilePackage$ $FileClass$
$ProjectFileDir$

生成class文件的有些鸡肋,只能生成当前java文件的class,如果该java文件依赖了新添加的java类且新添加的java类没有在classes和jar文件里、或着依赖的类里添加了方法并且使用了该方法 则会报错,还是用其他的构建工具比较好。
Generate Class File
生成class文件
$JDKPath$\bin\javac
$FilePath$ -d $OutputPath$ -classpath "$Classpath$"
$ProjectFileDir$

Generate Signature File
生成方法签名
$JDKPath$\bin\javap
-s -p $OutputPath$\$FileDirRelativeToSourcepath$\$FileNameWithoutExtension$.class
$ProjectFileDir$

右键想要生成头文件的类,比如在UserService.java文件上右键,选择Extenal Tools -> Generate Header File即可生成该类的头文件

同理,可以生成EmployeeService的头文件

编写c语言代码
打开Clion,选择C++ Library,选择一个路径并设置Library type为static,然后点击Create

在CMakeLists.txt文件里添加jdk的头文件,然后点击右上角的刷新
include_directories("D:\\software\\jdk\\include")
include_directories("D:\\software\\jdk\\include\\win32")

签名可以使用前面添加的Generate Signature File工具生成,编写完代码后,点击Build->Build Project即可生成dll文件

然后先运行以下java项目,此时肯定会报错,我们点击右上角的Application,点击Edit Configurations...

然后点击Modify options,勾选Add VM options,然后左边的VM options里添加dll所在文件夹
-Djava.library.path=D:\JNI\clion\jni-project\cmake-build-debug

添加加载dll的代码,并打上断点
static{
System.loadLibrary("libjni_project");
}

Clion使用CMake
Clion默认使用CMake因此什么都不用配置。
以debugger方式启动,此时可以看到processId为23980

打开Clion,选择Run->Attach to Process...

输入java的进程id,并点击Attach with Bundled GDB,然后在方法的开头打上断点

IDEA里点击Resume Program按钮

此时就跳转到Clion对应源码的方法开头了

Clion使用MSVC
点击Clion的设置,选择Bulid, Execution, Deployment,添加一个Vusuak Studio

输入Visual Studio的路径,并把它防到最上面。

Visual Studio的路径为如下内容:

如果刚刚使用了CMake再次Build还是使用的CMake,此时可以点击右上角的Debug,选择Edit CMake Profiles...

在Toolchain里选择Visual Studio,Generator里选择你的Vusual Studio版本,然后重新构建即可

构建完成后会生成jni_project.dll。此时cmake-build-debug-visual-studio目录会变黄,说明当前使用的是visual studio进行的构建。

IDEA里我们点击右上角的Application,点击Edit Configurations...

点击Modify options,勾选Add VM options,然后左边的VM options里添加dll所在文件夹
-Djava.library.path=D:\JNI\clion\jni-project\cmake-build-debug-visual-studio\Debug

修改加载dll的代码,并打上断点
static{
System.loadLibrary("jni_project");
}
以debugger方式启动,此时可以看到processId为248208

Clion里点击Run -> Attach to Process...

输入Java的进程ID,然后下拉选择Attach with Bundled LLDB并点击。然后在方法的开头打上断点。

IDEA里点击Resume Program按钮

此时就跳转到Clion对应源码的方法开头了

使用Visual Studio
打开Visual Studio,创建一个新的空项目。

然后配置一下项目名称和位置,点击创建。

选择菜单里的调试 -> jni-project 调试属性

在配置属性 -> 常规里修改配置类型为动态库(.dll)

在配置属性 -> C/C++ -> 常规里,点击附加包含目录选项的右侧倒三角,选择<编辑...>

添加jdk里的include和include\\win32目录。

下面是配置后的样子。

等加载完毕后,添加代码,修改平台为x64。注意Visual Studio是把所有东西都放在了一个目录下,而不是我们创建的目录层级,所以#include的时候不要添加相应的目录。
在方法的开头打上断点,然后右键项目名称,点击生成。

生成成功后会显示生成的dll的具体位置。

修改IDEA里的java.library.path路径为当前dll的所在目录。

修改一下加载的dll名称。
static{
System.loadLibrary("jni-project");
}

然后以Debug方式启动项目,并可以查看到进程的id为6692。

在Visual Studio里点击菜单里的调试 -> 附加到进程。

输入java的进程id,并选择该进程,然后点击附加。

IDEA里点击Resume Program按钮。

此时就跳转到Visual Studio对应源码的方法开头了

只有部分源码调试
其实只有dll和pdb文件和部分源码也可以正常调试,即使这部分源码跑不起来,只要该源码的信息和pdb文件对应就行。
新建个空的项目,使用Visual Studio构建,然后就可以把其他的都删掉,保留cmake-build-debug-visual-studio里面的Debug目录。粘贴前面使用Visual Studio编写代码并构建的dll和pdb文件到Debug目录,复制一份EmployeeService.cpp和UserService.cpp到项目根目录,在UserService.cpp的方法开头打上断点,再使用相同的步骤设置java.library.path并调试项目,附件到进程后跳转到Clion这边就会自动跳转到生成pdb文件的对应源码的位置。

如果生成pdb文件的源码不存在就不会跳转了,而是使用当前的文件。
