使用 JNI 调用 Java(C++)
掌握使用 C++ 进行 Java Native Interface 的教程
引言
C++ 和 Java 是两种主流语言,各有优缺点,并且存在大量有趣的、可移植的代码。那么,为什么不享受两全其美的好处呢?
Java Native Interface (JNI) 是一种以可移植方式集成 C++ 和 Java 代码的标准。它支持双向调用:您可以从 Java 调用 C++ 库,也可以从 C++ 调用 Java 组件。在本教程中,我将解释后一种方法。
背景
JNI 经常被 Java 开发者用于调用少量 C++ 代码,以满足超高性能的需求。但 JNI 的用途远不止于此。它还允许您将现有的 Java 组件嵌入到您用 C++ 开发的软件中。
在本文中,您将学习如何仅使用原生 JNI 来实现这种集成。不会使用任何第三方包装器。
阅读本教程后,您将不再遗憾某些尖端软件组件首先为 Java 开发,然后才提供 C++ 版本。当需要紧密且时间关键的相互关系时,您将不再编写复杂的基于文件或网络的接口来连接 Java 和 C++ 代码。您将无缝集成这两个世界。
教程
必备组件
对于本教程,您需要具备
- 已安装 Java Developer's Kit (JDK)。我们将安装目录称为
<JDK-DIR>
- 已安装 Java Runtime environment (JRE)。我们假设安装目录是
<JRE-DIR>
。请注意,JDK 安装会自动设置 JRE。 - 一个工作的 C++ 工具链。我的解释是针对 Windows 上的 MSVC13 定制的。但由于代码是标准的,因此很容易将其改编到其他编译器和操作系统。
您必须将 <JRE>/bin/server
添加到 PATH
。除非允许您将 Java 虚拟机动态库 (JVM.dll) 复制到可执行文件的路径中,否则必须执行此操作。
为了方便起见,您应该确保 <JDK-DIR>/bin
中的 JDK 工具包含在 PATH
中:这样您就可以轻松编译 Java 代码。
项目设置
对于本教程中的每个 C++ 项目,您都必须将 <JDK-DIR>/include
和 <JDK-DIR>/include/win32
目录添加到编译器的包含目录中。请注意,win32 目录是平台相关的。
您还应将 <JDK-DIR>/lib/jvm.lib 添加为链接器输入文件的附加依赖项。
使用 MSVC2103,您可以通过右键单击项目来显示其属性(请参阅截图)。
ZIP 文件包含一个 MSVC2013 解决方案,其中包含本教程的全部 7 个示例。下载 Article-JNI-1.zip
第一个示例:加载和初始化 JVM
在使用 C++ 代码中的 JNI 之前,您必须加载和初始化 Java 虚拟机 (JVM)。以下代码将向您展示如何执行此操作
#include <jni.h>
int main()
{
Using namespace std;
JavaVM *jvm; // Pointer to the JVM (Java Virtual Machine)
JNIEnv *env; // Pointer to native interface
//================== prepare loading of Java VM ============================
JavaVMInitArgs vm_args; // Initialization arguments
JavaVMOption* options = new JavaVMOption[1]; // JVM invocation options
options[0].optionString = "-Djava.class.path=."; // where to find java .class
vm_args.version = JNI_VERSION_1_6; // minimum Java version
vm_args.nOptions = 1; // number of options
vm_args.options = options;
vm_args.ignoreUnrecognized = false; // invalid options make the JVM init fail
//=============== load and initialize Java VM and JNI interface =============
jint rc = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); // YES !!
delete options; // we then no longer need the initialisation options.
if (rc != JNI_OK) {
// TO DO: error processing...
cin.get();
exit(EXIT_FAILURE);
}
//=============== Display JVM version =======================================
cout << "JVM load succeeded: Version ";
jint ver = env->GetVersion();
cout << ((ver>>16)&0x0f) << "."<<(ver&0x0f) << endl;
// TO DO: add the code that will use JVM <============ (see next steps)
jvm->DestroyJavaVM();
cin.get();
}
此代码显示 JVM 的版本。示例 1
代码已包含一些错误处理,应能帮助您解决任何意外问题。
如果您没有收到错误消息,但程序突然中断,最可能的原因是jvm.dll 在路径中找不到(请参阅上面的先决条件)。
示例 2:访问简单的 Java 静态方法
让我们编写我们可以想象的最简单的 Java 方法:一个简单的 static
方法,不接受任何参数,也不返回任何内容。在 Java 中,所有内容都嵌入在一个类中。因此,我们将把以下代码写入文件 MyTest.java
public class MyTest {
private static int magic_counter=777;
public static void mymain() { // <=== We will call this
System.out.println("Hello, World in java from mymain");
System.out.println(magic_counter);
}
}
我们从命令行编译此代码
javac MyTest.java
然后我们检查没有错误,并且成功生成了文件 MyTest.class 。顺便说一句,我不会再说,因为我们将在后续的示例中都这样做。
现在,我们准备丰富我们之前的 C++ 代码
...
jclass cls2 = env->FindClass("MyTest"); // try to find the class
if(cls2 == nullptr) {
cerr << "ERROR: class not found !";
}
else { // if class found, continue
cout << "Class MyTest found" << endl;
jmethodID mid = env->GetStaticMethodID(cls2, "mymain", "()V"); // find method
if(mid == nullptr)
cerr << "ERROR: method void mymain() not found !" << endl;
else {
env->CallStaticVoidMethod(cls2, mid); // call method
cout << endl;
}
}
这是如何工作的?
- 我们首先必须使用
FindClass()
找到正确的类,它充当类加载器。它将在 JVM 初始化时提供的目录列表中搜索适当的 .class 文件。如果 Java 类包含在包中,则应提供其完整名称。 - 然后将类传递给
GetStaticMethod()
,它应该在该类中找到正确的方法。该函数的最后一个参数是最难的:方法签名。如果此处不匹配,将找不到该方法。"()"
表示没有参数的函数,"V"
表示返回类型为void
。 static
方法独立于任何对象。因此,我们可以使用CallStaticVoidMethod()
调用该方法。
示例 3:接受参数并返回值 的 Java 静态方法
JNI 按值传递和返回基本 Java 类型(如 int
、long
、double
)的对象。这非常容易处理。您只需使用相应的 JNI 本地类型 jint
、jlong
、jdouble
等。
因此,让我们看看示例 3。Java 类已添加了以下方法
Class MyTest {
...
public static int mymain2(int n) { // <== add this new function
for (int i=0; i<n; i++) {
System.out.print (i);
System.out.println("Hello, World !");
}
return n*2; // return twice the param
}
}
从 C++ 调用此函数与前面的示例非常相似
//... we already have the class
jmethodID mid2 = env->GetStaticMethodID(cls2, "mymain2", "(I)I");
if(mid2 == nullptr) {
cerr << "ERROR: method it main2(int) not found !" << endl;
}
else {
env->CallStaticVoidMethod(cls2, mid2, (jint)5);
cout << endl;
}
传递给 GetStaticMethodID()
的签名现在是 "(I)"
,表示它是一个带有一个整数参数的函数,后面是 "I"
,即返回一个整数。如果您想尝试其他类型,请查看 Oracle 的 JNI 规范中记录的完整签名参考:Oracle 的 JNI 规范。
示例 4:Java 数组和对象
一旦您处理的是一个函数,该函数接受或返回非基本类型的对象,该对象就会按引用传递。因此,让我们以调用 Java main()
函数为例,该函数如下所示:
class MyTest {
...
public static void main (String[] args) { // test in java
//… some code here.
}
}
从 C++ 调用此函数会更复杂一些。首先,方法的签名:数组在 JNI 签名参数中用 "["
表示。非内置类型用 "L"
后跟完整的类名,再后跟分号表示。由于函数返回 void
,因此签名是:"([Ljava/lang/String;)V"
。是的!现在,我们可以检索该方法
//... we still have the class from the previous examples
jmethodID mid3 = env->GetStaticMethodID(cls2, "main", "([Ljava/lang/String;)V");
if(mid3 == nullptr) {
cerr << "ERROR: method not found !" << endl;
}
为了调用该方法,我们首先需要构建一个 Java 数组,以及用于填充该数组的 string
。我们这样做如下:
else {
jobjectArray arr = env->NewObjectArray(5, // constructs java array of 5
env->FindClass("java/lang/String"), // Strings
env->NewStringUTF("str")); // each initialized with value "str"
env->SetObjectArrayElement( arr, 1, env->NewStringUTF("MYOWNSTRING")); // change an element
env->CallStaticVoidMethod(cls2, mid3, arr); // call the method with the arr as argument.
env->DeleteLocalRef(arr); // release the object
}
此处要理解的关键点是 Java 对象是由 JVM 创建的。因此,JVM 负责在不再使用时释放内存。一旦您不再需要某个对象,就应该调用 DeleteLocalRef()
来告知 JVM 您不再需要它了。如果您不这样做,将会发生内存泄漏(请参阅此 StackOverflow 问题中的解释)。
示例 5:对象和方法
到目前为止,我们一直保持简单:我们只调用了 static
Java 方法。这些方法与对象无关。但这并非面向对象编程中最自然的方式。因此,很有可能有一天您需要创建一个对象并调用该对象的方法。
让我们在示例 5 中为我们的 Java 类添加一个构造函数和一个简单的方法
Class MyTest {
...
private int uid; // private data of the object: it's ID
public MyTest() { // constructor
uid = magic_counter++ * 2;
}
public void showId() { // simple method that shows the id of the object
System.out.println(uid);
}
}
从 C++ 开始,您可以通过查找并调用构造函数来创建 MyTest
对象
jmethodID ctor = env->GetMethodID(cls2, "<init>", "()V"); // FIND AN OBJECT CONSTRUCTOR
if(ctor == nullptr) {
cerr << "ERROR: constructor not found !" << endl;
}
else {
cout << "Object succesfully constructed !"<<endl;
jobject myo = env->NewObject(cls2, ctor);
如果对象成功构造,我们就可以搜索要调用的方法,并为该对象调用它
if (myo) {
jmethodID show = env->GetMethodID(cls2, "showId", "()V");
if(show == nullptr)
cerr << "No showId method !!" << endl;
else env->CallVoidMethod(myo, show);
}
}
现在,您知道如何启动 JVM、运行 static
方法、创建对象以及调用它们的方法。您可以完全控制任何您想与 C++ 代码集成的 Java 组件。
但是,还有最后一件事我们需要全面了解……
示例 6:回调和实例变量
在您的 Java 代码中,您可能需要回调 C++ 函数。这通过 Java 原生方法实现。以下是我们 Java 示例的最终增强
MyTest {
...
public native void doTest(); // to be supplied in C++ trhough JNI
public void showId() { // replace the previous version of example 5
System.out.println(uid);
doTest(); // <==== invoke the native method
}
}
原生函数在 Java 中声明,但在 Java 对象创建之前必须在 C++ 中定义和注册。
以下是在 C++ 中声明此类回调函数的方法
void doTestCPP(JNIEnv*e, jobject o) {
std::cout << "C++callback activated" << std::endl;
jfieldID f_uid = e->GetFieldID(e->GetObjectClass(o), "uid", "I");
if (f_uid)
std::cout << "UID data member: " << e->GetIntField(o, f_uid) << std::endl;
else std::cout << "UID not found" << std::endl;
}
顺便说一句,您可以看到,我们可以使用 GetFieldId()
轻松访问对象变量。
要注册原生函数映射,我们使用以下代码片段
JNINativeMethod methods[] { { "doTest", "()V", (void *)&doTestCPP } }; // mapping table
if(env->RegisterNatives(cls2, methods, 1) < 0) { // register it
if(env->ExceptionOccurred()) // verify if it's ok
cerr << " OOOOOPS: exception when registreing naives" << endl;
else
cerr << " ERROR: problem when registreing naives" << endl;
}
现在,您可以像上一个示例一样再次调用 showId()
方法。但新版本将调用 doTest()
,它将从 Java 调用我们新的 C++ 回调。
关注点
我们现在可以组织双向集成:从 C++ 到 Java,再返回。您已经学习了最基本的 JNI 生存技术。现在轮到您运用这些新知识了。这里是 JNI 函数的完整参考。
虽然您现在可以想象任何集成方式,但您应该意识到一些性能限制。JNI 意味着一些最小的开销。
我编写了一个小型基准测试,分别从 Java 和 C++ 调用同一个非常小的 Java 函数。它包含在示例 7 中。
在我的 core i7 上,结果如下
Java called from Java: 14 nanoseconds/iteration
Java called from C++: 23 nanoseconds/iteration
C++ 100% native: 2 nanoseconds/iteration
每次通过 JNI 从 C++ 调用 Java 都会产生 9 到 10 ns 的开销。对于像本基准测试中这样的小型函数,这种开销是过度的。因此,不应考虑这种集成来实现高频率、低延迟的函数调用。但是,许多 JNI 应用程序涉及集成高级 Java 组件或接口。在这种情况下,与轻松集成的巨大好处相比,JNI 开销可以忽略不计。
最后需要记住的一点是 Java 对象内存管理的复杂性。如果在 C++ 中创建 Java 对象,并且指向它的 C++ 变量超出范围,则可能会发生内存泄漏。对于像这里这样的小型演示来说,这是可以管理的。但是,为了在更复杂的软件中保证可靠性,确实应该考虑一个实现 RAII 的 C++ 包装器来处理 Java 对象。
历史
- 2015/5/19 初始版本