JNI(Java Native Interface)指南

原文:https://www.baeldung.com/jni

1. 介绍

我们知道,Java一个很大的优势就是可移植性-意思是一旦我们写下代码,我们就能编译它,生成的结果是平台无关的字节码。

之后,它就能运行在任何能够运行Java虚拟机的平台或者设备上,并且能够无锋地运行我们想要的结果。

然而,有时,我们确实想要使用一些本地编译的代码。

其中,有几个原因:

  • 需要控制一些硬件
  • 因为一些过程需要有性能提升
  • 使用一些已经存在的库,而不是用Java重写

为了达到这样的目标,JDK引入了在字节码和本地编码(通常是C或者C++编写的)之间的桥梁。这个工具就叫做Java Native Interface. 在本文中,我们将看到如何写这样的代码。

2. 如何工作

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

Java提供了native关键字,该关键字用来表明方法的实现是本地代码提供的。

通常,当作一个本地的可执行程序,我们能够选择使用静态链接库或者共享库;

  • 静态库-在链接的过程中,库中的二进制会被包含进我们的可执行文件。因此,我们可以在运行的时候不再需要这些库。但是,它会增加可执行文件的大小。
  • 共享库-最后的可执行文件必须引用这些库文件,而不是代码。它需要整个运行可执行文件的环境必须能够访问这些库。

后者对JNI而言就显得合理了,因为我们不能混合字节码和本地编译好的代码在同样的二进制文件。

因此,我们的共享库得保持本地代码在一个.so/.dll/.dylib文件中(这3种不同的文件类型对应不同的操作系统),而不是把本地代码写在我们的类中。

native关键字转换我们的方法成为一种抽象方法

private native void aNativeMethod();

跟其他的由Java类实现的方法最大的不同是它会在另外的本地共享库中实现。

Java会构造一个表,该表包含了一系列的指针,这些指针指向了所有的本地方法实现,所以,我们能够从我们的Java代码中调用这些本地方法。

2.2 需要的组件

下面是对需要的关键组件的简单描述,我们会在后面解释为什么要这些组件

  • Java代码-我们的类。它们会包含至少1个本地方法。
  • 本地代码-我们本地方法的真实逻辑,通常以C或者C++编写。
  • JNI头文件-C/C++的头文件(在JDK的目录下include/jni.h)包含所有的JNI元素的定义,这些定义我们可能会在我们的本地程序中用到。
  • C/C++编译器-我们能够在GCC,Clang,Visual Studio, 或者任何我们到目前为止能够生成平台相关的共享库的编译器。

2.3 在代码中的JNI元素(Java 和 C/C++)

Java元素:

  • "native"关键字-正如我们前面已经说过的,任何标记为native的方法必须在一个本地的,共享库实现。
  • System.loadLibrary(共享库名称的字符串)-一个静态方法,该方法能够从文件系统加载共享库到内存并且导出相关方法到我们的Java代码。

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

  • JNIEXPORT - 在共享库中标记该方法是可导出的,所以它会被包含在方法表中,从而JNI能够找到它
  • JNICALL-配合JNIEXPORT。它能够保证我们的方法能够在JNI框架中使用
  • JNIEnv-该结构包含了一些方法,这些方法使得我们能够用我们的本地代码访问Java元素
  • Java虚拟机-该结构能够是我们控制一个运行的Java虚拟机(甚至启动一个新的虚拟机),增加线程,销毁该虚拟机,等等

3. Hello Word JNI

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

在本教程中,我们会使用C++作为我们的本地代码,G++作为编译器和链接器。

虽然我们能够用任何我们喜欢的编译器,但是下面是如何在Ubuntu,Windows,和MacOS

译者:作者的语言逻辑似乎有问题,为什么是转折?

  • Ubuntu Linux - 在终端中运行命令“sudo apt-get install build-essential”
  • WIndows-Install MinGW
  • MacOS- 在终端中运行命令"g++",如果没有显示任何信息,它会安装

3.1 创建Java类

让我们创建我们第一个JNI程序,该程序实现了一个典型的“Hello World”.

首先,我们编写下面的代码

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();
	}

译者: 注意java的语法,最前面的包名表示文件夹结构,如果不需要可以删除

如上代码所示,我们在一个静态代码块中加载共享库。这能保证不管何时只要我们需要的时候都已经准备好。

相对地,在这段实验程序中,我们在调用我们的本地库之前加载库,因为我们不会在其他地方使用本地库。

3.2 在C++中实现一个方法

现在,我们需要用C++写一个本地的方法

在C++中,定义和实现分别放在.h和.cpp文件中。

首先,为了定义方法,我们使用Java编译器的-h标志

javac -h . HelloWorldJNI.java

这会产生一个com_baeldung_jni_HelloWorldJNI.h文件,该文件中包含类中定义的本地方法,这些方法会把类作为对象传递,在本例中,只有一个。

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
	  (JNIEnv *, jobject);

如同我们所看到的那样,一个函数的名字会被自动产生,这个名字会有所有的包,类和函数名

同样,我们也能注意到一些有趣的事情,我们有2个参数传递给我们的本地方法,一个指向JNIEnv的指针和一个Java object,

现在,我们还得为实现sayHello函数创建一个新的.cpp文件。在这个函数中将会在控制台中打印"Hello World"

.cpp的文件名和.h的文件名一样,在.cpp中会包含.h头文件并且添加实现的代码。

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

译者:把*%JAVA_HOME%/include目录下的“jni.h”%JAVA_HOME%/include/win32目录下的“jni_md.h”*两 个文件复制到Hello目录下,用刚才添加“com_xrq_test1_TestMain.h”一样的方式添加这两个.h文件到头文件目录中,这样目录 下Hello目录下应该多出了两个文件,VS2010工程目录下应该有3个.h文件。JAVA_HOME就是JDK安装目录。这一步很重要,没有这一步,C++程序是无法运行的

3.3 编译和链接

在该步中,我们有已经有了所有需要的东西,之后,我们就得在它们之间建立起联系。

我们需要从C++代码中构建一个共享库,然后,运行它!

为了达到这样的目标,我们必须使用G++编译器,不要忘记在Java 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

Once we have the code compiled for our platform into the file com_baeldung_jni_HelloWorldJNI.o, we have to include it in a new shared library. Whatever we decide to name it is the argument passed into the method System.loadLibrary.

We named ours “native”, and we’ll load it when running our Java code.

The G++ linker then links the C++ object files into our bridged library.

Ubuntu version:

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

Windows version:

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

MacOS version:

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

*译者:如果在当前目录运行,可以将/NATIVE_SHARED_LIB_FOLDER替换成. *

控制台输出:

Hello from C++ !!

4 添加高级JNI特性

Saying hello已经不错了,但是不是特别有用。通常,我们希望在Java和C++之间交换数据,并且在我们的程序中管理这些数据。

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

我们将会添加一些参数到我们的本地方法。让我们创建一个新的类ExampleParametersJNI,该类有2个本地方法,这两个方法使用参数并且返回不同的类型。

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

然后,重复上述的过程创建新的.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字符串到我们的C++代码并且返回一个String类型,在这期间不用考虑两种语言怎么实现对接。

我们可以在Oracle的官方文档中查看有关的对应关系。

为了测试我们的代码,我们还得重复上述的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;
    }
}

然后,我们创建另一个Java类 ExampleObjectsJNI ,该类所涉及的本地方法中,我们会有一些UserData类型的参数

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

该类有一些本地方法,其cpp文件中的实现如下:

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的反射一样控制所有的属性和方法。

我们所有其他的JNIEnv方法在Oracle官方文档。

5. 使用JNI的缺点

JNI桥梁也有它的缺点。

主要的缺点是依赖指定的平台;**我们必须抛弃“编写一次,处处运行”的java特点。**这也意味着我们必须为每个将要支持的新平台和架构构建一个新的库。想象一下如果我们支持Windows, Linux, Android, MacOS …

JNI不仅给我们的程序添加了复杂的一层,它也增加了在本地代码和运行在JVM中的代码之间通讯的额外的开销:我们需要在打包和解压的过程中转换Java和C++之间的数据。

有时,类型之间并没有等价的转化,所以,我们必须写我们自己的等式。

6 总结

为指定平台编译的代码通常比运行字节码要快。

这对我们要加速某个想要处理的过程时特别有用。同样,同样,当我们没有其他替代方案时,例如,我们需要某个库文件去控制某个设备。

然而,这也带来了一部分代价,因为我们必须去维护特定平台下的代码。

这就是为什么在没有Java替换物的情况下用JNI是一个不错的主意。

代码可以在GitHub上获得。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值