跳至主要內容

apzs...大约 55 分钟

JNI快速入门

文章原始链接:https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.htmlopen in new window

引言

有的时候我们需要使用本地代码(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()方法来加载一个nativeopen in new window库“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.htmlopen in new window

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.htmlopen in new window

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方法)实现。

核心任务

  1. 告知 JVM 我们使用的 JNI 版本。
  2. 捕获并保存 JavaVM \*vm 指针到全局变量
  3. 动态注册 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方法。

核心任务

  1. 使用 C++ 标准库创建后台线程。
  2. 在后台线程中利用全局 g_vm 将自己附加(Attach)到 JVM。
  3. 执行 Java 方法。
  4. 执行完毕后分离(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
jmethodIDGetMethodID不需要类卸载时自动失效
jfieldIDGetFieldID不需要类卸载时自动失效

JNI签名规则

以下是JNI方法签名的详细对照表,涵盖常见数据类型及组合情况:

Java声明JNI签名格式规则说明
基本类型
void method()()VV表示void
int method(int)(I)II表示int
double method(double)(D)DD表示double
boolean method(boolean)(Z)ZZ表示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中视为数组

常见错误场景示例

  1. 泛型陷阱

    // Java方法:public void setMap(Map<String, Integer> map)
    // 错误签名:(Ljava/util/Map<Ljava/lang/String;Ljava/lang/Integer;>;)V
    // 正确签名:(Ljava/util/Map;)V
    
  2. 多维数组

    // Java方法:public int[][] getMatrix()
    // 正确签名:()[[I
    
  3. 混合参数

    // Java方法:public void update(int id, String name, double[] scores)
    // 正确签名:(ILjava/lang/String;[D)V
    

签名生成技巧

  1. 使用javap工具自动生成:

    javap -s -p YourClassName.class
    
  2. IDE插件(如IntelliJ IDEA的JNI Helper)可自动生成签名。

JNI调试技巧

首选准备一个示例代码,该代码的主要功能为获取用户,设置最后的用户,返回用户;获取职位,设置最后的职位,返回职位:

image-20250421091406897
image-20250421091406897

准备工作

生成class文件

推荐使用Ctrl + F9快捷键使用IDEA自带的构建工具进行构建以加快生成class文件的速度

如果是maven项目或Gradle项目可以直接点击compile进行编译

image-20250421092025225
image-20250421092025225

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

image-20250421092328404
image-20250421092328404
获取头文件

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

image-20250421093501295
image-20250421093501295

下面是各文件的内容

Generate Header File
生成JNI头文件
$JDKPath$/bin/javah
-jni -classpath "$OutputPath$;$Classpath$" -d $FileParentDir$\$FilePackage$ $FileClass$
$ProjectFileDir$
image-20250421093527141
image-20250421093527141

生成class文件的有些鸡肋,只能生成当前java文件的class,如果该java文件依赖了新添加的java类且新添加的java类没有在classes和jar文件里、或着依赖的类里添加了方法并且使用了该方法 则会报错,还是用其他的构建工具比较好。

Generate Class File
生成class文件
$JDKPath$\bin\javac
$FilePath$ -d $OutputPath$ -classpath "$Classpath$"
$ProjectFileDir$
image-20250421093537060
image-20250421093537060
Generate Signature File
生成方法签名
$JDKPath$\bin\javap
-s -p $OutputPath$\$FileDirRelativeToSourcepath$\$FileNameWithoutExtension$.class
$ProjectFileDir$
image-20250421093708670
image-20250421093708670

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

image-20250421094449165
image-20250421094449165

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

image-20250421094740917
image-20250421094740917
编写c语言代码

打开Clion,选择C++ Library,选择一个路径并设置Library typestatic,然后点击Create

image-20250421095649449
image-20250421095649449

CMakeLists.txt文件里添加jdk的头文件,然后点击右上角的刷新

include_directories("D:\\software\\jdk\\include")
include_directories("D:\\software\\jdk\\include\\win32")
image-20250421095800457
image-20250421095800457

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

image-20250421115054091
image-20250421115054091

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

image-20250421115422195
image-20250421115422195

然后点击Modify options,勾选Add VM options,然后左边的VM options里添加dll所在文件夹

-Djava.library.path=D:\JNI\clion\jni-project\cmake-build-debug
image-20250421131709986
image-20250421131709986

添加加载dll的代码,并打上断点

static{
    System.loadLibrary("libjni_project");
}
image-20250421134418721
image-20250421134418721

Clion使用CMake

Clion默认使用CMake因此什么都不用配置。

以debugger方式启动,此时可以看到processId为23980

image-20250421134710246
image-20250421134710246

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

image-20250421134825448
image-20250421134825448

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

image-20250421134952456
image-20250421134952456

IDEA里点击Resume Program按钮

image-20250421135042196
image-20250421135042196

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

image-20250421135210862
image-20250421135210862

Clion使用MSVC

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

image-20250421135648276
image-20250421135648276

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

image-20250421135352825
image-20250421135352825

Visual Studio的路径为如下内容:

image-20250421135813354
image-20250421135813354

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

image-20250421140020870
image-20250421140020870

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

image-20250421140623944
image-20250421140623944

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

image-20250421140713954
image-20250421140713954

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

image-20250421141133958
image-20250421141133958

点击Modify options,勾选Add VM options,然后左边的VM options里添加dll所在文件夹

-Djava.library.path=D:\JNI\clion\jni-project\cmake-build-debug-visual-studio\Debug
image-20250421141231808
image-20250421141231808

修改加载dll的代码,并打上断点

static{
    System.loadLibrary("jni_project");
}

以debugger方式启动,此时可以看到processId为248208

image-20250421141438995
image-20250421141438995

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

image-20250421141530367
image-20250421141530367

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

image-20250421141820722
image-20250421141820722

IDEA里点击Resume Program按钮

image-20250421141941861
image-20250421141941861

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

image-20250421142004407
image-20250421142004407

使用Visual Studio

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

image-20250421175031221
image-20250421175031221

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

image-20250421175107905
image-20250421175107905

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

image-20250421175202225
image-20250421175202225

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

image-20250421175559425
image-20250421175559425

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

image-20250421180036050
image-20250421180036050

添加jdk里的includeinclude\\win32目录。

image-20250421180132037
image-20250421180132037

下面是配置后的样子。

image-20250421180140184
image-20250421180140184

等加载完毕后,添加代码,修改平台为x64。注意Visual Studio是把所有东西都放在了一个目录下,而不是我们创建的目录层级,所以#include的时候不要添加相应的目录。

在方法的开头打上断点,然后右键项目名称,点击生成。

image-20250422100349387
image-20250422100349387

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

image-20250422100505424
image-20250422100505424

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

image-20250422100515377
image-20250422100515377

修改一下加载的dll名称。

static{
	System.loadLibrary("jni-project");
}
image-20250422100540420
image-20250422100540420

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

image-20250422100644928
image-20250422100644928

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

image-20250422100716897
image-20250422100716897

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

image-20250422100757262
image-20250422100757262

IDEA里点击Resume Program按钮。

image-20250422100816430
image-20250422100816430

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

image-20250422100847725
image-20250422100847725

只有部分源码调试

其实只有dll和pdb文件和部分源码也可以正常调试,即使这部分源码跑不起来,只要该源码的信息和pdb文件对应就行。

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

image-20250422102327423
image-20250422102327423

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

image-20250422103853607
image-20250422103853607
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.0.0-alpha.8