JNI(JAVA NATIVE INTERFACE) 指南

本文介绍Java Native Interface (JNI),这是一种使Java代码能够调用本地代码(如C或C++)的机制。通过一个Hello World示例,文章详细展示了如何在Java中声明本地方法,并在C++中实现这些方法。此外,还介绍了如何处理不同类型的数据交换,包括基本类型、字符串和对象。

1.介绍

众所周知,Java的强项之一就是跨平台,一次编写,到处运行。

简而言之,它可以在任何运行JVM的机器或设备上无差别的运行。

然而,有时候我们在一些特殊的架构下,的确需要使用本地库的代码。

可能是基于一下几点原因:

  • 操作硬件的需要
  • 提升一个要求很高的程序的性能
  • 不使用Java重写一个已经实现的库

为了实现这些,JDK引进了一个桥梁来链接我们JVM中运行的代码和本地代码(通常是C或C++)。

这个工具叫做Java Native Interface。这篇文章,我们将会看到它是什么以及用它写一点代码

2. 它是如何工作的

2.1 本地方法:当JVM遇到编译好的代码

Java 提供了 native 关键字来标明一个方法的实现是有本地代码提供的。

通常,但我们需要使用一个本地可执行的程序,有静态库和本地库两种选择:

  • 静态库(Static libs)- 所有的库二进制代码都将在我们链接的处理中被包含到我们的可执行程序中。因此,当我们不在需要这些库的时候,它仍然在我们的可执行文件中,让我们的可执行文件变得很大。
  • 共享库(Shared libs) - 最终的可执行文件只有对这个库的引用,而不是代码本身。这就需要我们可执行文件用到的所有的库都能被我们的程序读取到

后面的是为什么JNI让我们不必将字节码和本地代码混合到同一个二进制文件中。

因此,我们的共享库仍然独立保有它的 .so/.dll/.dylib 文件(文件类型取决于操作系统)的本地代码,而不必将其打包到我么的classes中。

native 关键字将我们的方法映射到一个抽象方法中

private native void aNativeMethod();

最主要的区别是代码不需要我们用另一个Java class来实现这个方法,它会被一个分离的本地共享库实现。

将在内存中构造一个表,其中包含执行我们本地方法的实现的指针,以便可以从我们的Java代码中调用它们。

2.2 需要的组件

一下是我们需要考虑的关键组件的简要说明。我们将在本文后看进一步解释它们。

  • Java代码(Java Code) - 我们的类。至少包含一个本地方法。
  • 本地方法(Native Code)- 本地方法的真实逻辑,同时用C或C++编码实现。
  • JNI头文件(JNI header file)- C/C++的头文件(包含 JDK目录下的 jni.h)包含我们可能用到的所有JNI元素的定义。
  • C/C++编译器(C/C++ Compiler) - 我们可以选择GCC、Clang、VIsual Studio 或者任何能够为我们使用的平台生成本地共享库的工具。
2.3 JNI编码中的元素(Java 和 C/C++)

Java元素:

  • **“native”**关键字 - 正如我们介绍过的,任何标记为本地的方法都必须在共享库中实现。
  • System.loadLibrary(String libName) - 一个静态方法,它将共享库从文件系统加载到内存中,并使其导出的函数可用于我们的Java代码

C/C++元素(很多都在jni.h中定义了)

  • JNIEXPORT - 将共享库中的函数标记为可导出来将函数包含在函数表中,以便JNI可以找到它
  • JNICALL – 结合JNIEXPORT使用,确保我们的方法对JNI可用
  • JNIEnv - 一个包含方法的结构体,我们可以使用我们的本地代码访问Java元素
  • JavaVM - 一个让我们可以操作正在运行的 JVM(或者甚至启动一个新的)的结构,向它添加线程,销毁它,等等…

3. Hello World JNI

接下来,我们看一下实践中JNI是如何工作的

在这个教程中,我们使用C++作为本地语言,G++作为编译器和链接器。

我们可以随喜好的使用其他任何的编译器,下面是在Ubuntu、Windows,MacOS下暗转G++:

  • Ubtuntu Linux - run command “sudo apt-get install build-essential” in a terminal
  • Windows – Install MinGW
  • MacOS – run command “g++” in a terminal and if it’s not yet present, it will install it.
3.1 创建一个Java类

让我们开始创建第一个JNI程序,从传统的"Hello World" 开始.

首先,我们创建一下Java类,其中包含将会工作的本地方法

package com.baeldung.jni;

public class HelloWorldJNI {

    static {
        System.loadLibrary("native");
    }
    
    public static void main(String[] args) {
        new HelloWorldJNI().sayHello();
    }

    // Declare a native method sayHello() that receives no arguments and returns void
    private native void sayHello();
}

我们可以看到,我们在静态代码块中加载了一个共享库.这能够确保它在我们需要时随时随地准备就绪。

或者,在这个简单的程序中,我们可以在调用我们的本机方法之前加载库,因为我们没有在其他任何地方使用本机库。

3.2 用C++实现一个方法

现在我们需要用C++来创建一个本地方法的实现。

在 C++ 中,定义和实现通常分别存储在 .h 和 .cpp 文件中。

首先,要创建方法的定义,我们必须使用 Java 编译器的 -h 标志。需要注意的是,对于java 9之前的版本,我们应该使用javah工具,而不是javac -h命令

javac -h . HelloWorldJNI.java

这将生成一个 com_baeldung_jni_HelloWorldJNI.h 文件,其中包含作为参数传递的类中包含的所有本地方法,在这个例子中,只有一个:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);

如我们所见,这个函数名称使用包名+类名+方法名生成

还有,我们还可以看到一些有趣的现象就是我们的函数带有两个参数:一个只想当前JNIEnv的指针;以及该方法附属的 Java 对象,即我们的 HelloWorldJNI 类的实例。

现在,我们必须要为创建一个新的 .cpp 文件来实现 sayHell 函数。这是我们将要执行打印"Hello World"到控制台的地方

我们将使用与包含标头的 .h 相同的名称命名我们的 .cpp 文件,并添加此代码以实现本地方法:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    std::cout << "Hello from C++ !!" << std::endl;
}
3.3 编译和链接

到此为止,我们有了所有需要的部分并将它们之间连接了起来。

我们需要使用C++的代码 构建(build)我们的共享库并执行它!

为了做到这一点,我们必须要使用G++编译器,不要忘记将JDK安装路径下的JNI头文件包含进去.

Ubuntu版本:

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Windows 版本:

g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

MacOS 版本:

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

一旦我们在我们的平台上将代码编译成文件com_baeldung_jni_HelloWorldJNI.o,我们必须将它包含斤一个新的共享库。无论我们如何命名,都是将其作为参数传递给 System.loadLibrary

我们命名自己的 “native”,并在执行Java代码时将其加载。

然后 G++ 链接器将 C++ 目标文件链接到我们的桥接库中。

Ubuntu 版本:

g++ -shared -fPIC -o libnative.so com_baeldung_jni_HelloWorldJNI.o -lc

Windows 版本:

g++ -shared -o native.dll com_baeldung_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias

MacOS 版本:

g++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc

到此结束!

现在我们可以从控制台运行我们的程序。

然而,我们需要将完整路径添加到包含我们刚刚生成的库的目录。这样Java就知道去哪里找我们的本地库:

java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.baeldung.jni.HelloWorldJNI

控制台输出:

Hello from C++ !!

4.使用JNI高级功能

说hello很好但是没啥用。我们通常需要在Java和C++代码之间交换并管理数据。

4.1给我们的本地方法添加参数

我们将要给我们的本地方法添加一些参数。我们来创建一个包含两个本地方法并且拥有不同出入参的ExampleParametersJNI

private native long sumIntegers(int first, int second);
    
private native String sayHelloToMe(String name, boolean isFemale);

然后,重复我们上面的步骤:用 "javac -h"创建一个新的**.h**文件。

现在创建相同的 .cpp文件来实现新的C++方法:

...
JNIEXPORT jlong JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers 
  (JNIEnv* env, jobject thisObject, jint first, jint second) {
    std::cout << "C++: The numbers received are : " << first << " and " << second << std::endl;
    return (long)first + (long)second;
}
JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sayHelloToMe 
  (JNIEnv* env, jobject thisObject, jstring name, jboolean isFemale) {
    const char* nameCharPointer = env->GetStringUTFChars(name, NULL);
    std::string title;
    if(isFemale) {
        title = "Ms. ";
    }
    else {
        title = "Mr. ";
    }

    std::string fullName = title + nameCharPointer;
    return env->NewStringUTF(fullName.c_str());
}
...

*我们使用了 JNIEnv 类型的指针 env 来访问 JNI 环境实例提供的方法

在这个例子中,JNIEnv 允许我们传递一个Java的String类型给我们的C++代码并返回,而且我们无需关心实现。

我们可以在Oracle官方文档中检查Java和JNI C 类型的对比

为了测试我们的代码,我们需要重复之前HelloWorld例子的所有编译步骤。

4.2使用对象和从本地方法调用Java代码

最后一个例子,我们看一下在我们本地C++代码中如何操纵Java对象。

我们开始创建一下新的UserData的对象来存储一些用户信息:

package com.baeldung.jni;

public class UserData {
    
    public String name;
    public double balance;
    
    public String getUserInfo() {
        return "[name]=" + name + ", [balance]=" + balance;
    }
}

然后,我们创建一个包含对UserData操作的本地方法的Java 类- ExampleObjectsJNI

...
public native UserData createUser(String name, double balance);
    
public native String printUserData(UserData user);

接下来,我们创建一个*.h的头文件和新的.cpp*的C++代码实现:

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {
  
    // Create the object of the class UserData
    jclass userDataClass = env->FindClass("com/baeldung/jni/UserData");
    jobject newUserData = env->AllocObject(userDataClass);
	
    // Get the UserData fields to be set
    jfieldID nameField = env->GetFieldID(userDataClass , "name", "Ljava/lang/String;");
    jfieldID balanceField = env->GetFieldID(userDataClass , "balance", "D");
	
    env->SetObjectField(newUserData, nameField, name);
    env->SetDoubleField(newUserData, balanceField, balance);
    
    return newUserData;
}

JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData
  (JNIEnv *env, jobject thisObject, jobject userData) {
  	
    // Find the id of the Java method to be called
    jclass userDataClass=env->GetObjectClass(userData);
    jmethodID methodId=env->GetMethodID(userDataClass, "getUserInfo", "()Ljava/lang/String;");

    jstring result = (jstring)env->CallObjectMethod(userData, methodId);
    return result;
}

再一次,我们使用 JNIEnv 的指针 *env 来访问运行中的JVM中的我们需要的类、对象、字段和方法。

通常,我们只需要提供全类名来访问Java类或者正确的方法名和签名来访问方法对象。

我们已经在们的本地代码中创建了一个com.baeldung.jni.UserData类的实例。一旦我们有了这个实例,我们可以以一种近似于Java反射的方式来操纵它所有的属性和方法。

我们可以在Oracle官方文档中查看JNIEnv的所有其他方法。

5. 使用JNI的带来的不利

JNI 桥接确实有其缺陷。

主要缺点是对底层平台的依赖;我们基本上失去了 Java 的“一次编写,随处运行”的特性。这意味着我们必须为每个我们想要支持的平台和架构的新组合构建一个新的库。想象一下,如果我们支持 Windows、Linux、Android、MacOS……,这会对构建过程产生怎样的影响?

JNI不仅给我们的程序增加了一层复杂度。它还在运行到 JVM 的代码和我们的本机代码之间增加了一个代价高昂的通信层:我们需要在编码/解码过程中转换 Java 和 C++ 之间以两种方式交换的数据。

有时甚至没有类型之间的直接转换,所以我们必须编写我们的等价物。

6. 结论

为特定平台编译代码(通常)比运行字节码更快。这在我们需要加快要求苛刻的过程时非常有用。此外,当我们没有其他选择时,例如当我们需要使用管理设备的库时。然而,这是有代价的,因为我们必须为我们支持的每个不同平台维护额外的代码。

这就是为什么在没有 Java 替代品的情况下只使用 JNI 通常是个好主意。As always the code for this article is available over on GitHub

原文链接

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值