65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 JNI 调用 Java(C++)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (30投票s)

2015年5月19日

Zlib

8分钟阅读

viewsIcon

247080

downloadIcon

3990

掌握使用 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 目录是平台相关的。

[SCREENSHOT]

您还应将 <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 类型(如 intlongdouble)的对象。这非常容易处理。您只需使用相应的 JNI 本地类型 jintjlongjdouble 等。

因此,让我们看看示例 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 初始版本
© . All rights reserved.