1. Introduction
1.介绍
As we know, one of the main strengths of Java is its portability – meaning that once we write and compile code, the result of this process is platform-independent bytecode.
我们知道,Java的主要优势之一是它的可移植性–意味着一旦我们编写和编译代码,这个过程的结果就是与平台无关的字节码。
Simply put, this can run on any machine or device capable of running a Java Virtual Machine, and it will work as seamlessly as we could expect.
简单地说,这可以在任何能够运行Java虚拟机的机器或设备上运行,而且它将像我们所期望的那样无缝工作。
However, sometimes we do actually need to use code that’s natively-compiled for a specific architecture.
然而,有时我们确实需要使用为特定架构原生编译的代码。
There could be some reasons for needing to use native code:
可能有一些需要使用本地代码的原因。
- The need to handle some hardware
- Performance improvement for a very demanding process
- An existing library that we want to reuse instead of rewriting it in Java.
To achieve this, the JDK introduces a bridge between the bytecode running in our JVM and the native code (usually written in C or C++).
为了实现这一目标,JDK在我们的JVM中运行的字节码和本地代码(通常用C或C++编写)之间引入了一座桥梁。
The tool is called Java Native Interface. In this article, we’ll see how it is to write some code with it.
这个工具被称为Java Native Interface。在这篇文章中,我们将看到如何用它来写一些代码。
2. How It Works
2.它是如何工作的
2.1. Native Methods: the JVM Meets Compiled Code
2.1.本地方法:JVM与编译后的代码相遇
Java provides the native keyword that’s used to indicate that the method implementation will be provided by a native code.
Java提供了native关键字,用来表示方法的实现将由本地代码提供。
Normally, when making a native executable program, we can choose to use static or shared libs:
通常,在制作一个本地可执行程序时,我们可以选择使用静态库或共享库。
- Static libs – all library binaries will be included as part of our executable during the linking process. Thus, we won’t need the libs anymore, but it’ll increase the size of our executable file.
- Shared libs – the final executable only has references to the libs, not the code itself. It requires that the environment in which we run our executable has access to all the files of the libs used by our program.
The latter is what makes sense for JNI as we can’t mix bytecode and natively compiled code into the same binary file.
后者对JNI来说是有意义的,因为我们不能把字节码和原生编译的代码混合在同一个二进制文件中。
Therefore, our shared lib will keep the native code separately within its .so/.dll/.dylib file (depending on which Operating System we’re using) instead of being part of our classes.
因此,我们的共享库将在其.so/.dll/.dylib文件(取决于我们使用的操作系统)中单独保留本地代码,而不是作为我们类的一部分。
The native keyword transforms our method into a sort of abstract method:
native关键字将我们的方法转化为一种抽象方法:。
private native void aNativeMethod();
With the main difference that instead of being implemented by another Java class, it will be implemented in a separated native shared library.
主要区别在于它不是由另一个Java类来实现,而是在一个独立的本地共享库中实现。
A table with pointers in memory to the implementation of all of our native methods will be constructed so they can be called from our Java code.
我们将在内存中构建一个表,其中有指向所有本地方法的实现的指针,因此它们可以从我们的Java代码中调用。
2.2. Components Needed
2.2.所需组件
Here’s a brief description of the key components that we need to take into account. We’ll explain them further later in this article
以下是对我们需要考虑的关键部分的简要描述。我们将在本文后面进一步解释它们
- Java Code – our classes. They will include at least one native method.
- Native Code – the actual logic of our native methods, usually coded in C or C++.
- JNI header file – this header file for C/C++ (include/jni.h into the JDK directory) includes all definitions of JNI elements that we may use into our native programs.
- C/C++ Compiler – we can choose between GCC, Clang, Visual Studio, or any other we like as far as it’s able to generate a native shared library for our platform.
2.3. JNI Elements in Code (Java And C/C++)
2.3.代码中的JNI元素(Java和C/C++)
Java elements:
Java元素。
- “native” keyword – as we’ve already covered, any method marked as native must be implemented in a native, shared lib.
- System.loadLibrary(String libname) – a static method that loads a shared library from the file system into memory and makes its exported functions available for our Java code.
C/C++ elements (many of them defined within jni.h)
C/C++元素(其中许多定义在jni.h中)。
- JNIEXPORT- marks the function into the shared lib as exportable so it will be included in the function table, and thus JNI can find it
- JNICALL – combined with JNIEXPORT, it ensures that our methods are available for the JNI framework
- JNIEnv – a structure containing methods that we can use our native code to access Java elements
- JavaVM – a structure that lets us manipulate a running JVM (or even start a new one) adding threads to it, destroying it, etc…
3. Hello World JNI
3.Hello World JNI
Next, let’s look at how JNI works in practice.
接下来,我们来看看JNI在实践中是如何工作的。
In this tutorial, we’ll use C++ as the native language and G++ as compiler and linker.
在本教程中,我们将使用C++作为本地语言,G++作为编译器和链接器。
We can use any other compiler of our preference, but here’s how to install G++ on Ubuntu, Windows, and MacOS:
我们可以使用任何其他我们喜欢的编译器,但这里是如何在Ubuntu、Windows和MacOS上安装G++。
- Ubuntu 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. Creating the Java Class
3.1.创建Java类
Let’s start creating our first JNI program by implementing a classic “Hello World”.
让我们开始创建我们的第一个JNI程序,实现一个经典的 “Hello World”。
To begin, we create the following Java class that includes the native method that will perform the work:
首先,我们创建以下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();
}
As we can see, we load the shared library in a static block. This ensures that it will be ready when we need it and from wherever we need it.
正如我们所见,我们在一个静态块中加载共享库。这确保了在我们需要它的时候,无论我们在哪里需要它,它都会准备好。
Alternatively, in this trivial program, we could instead load the library just before calling our native method because we’re not using the native library anywhere else.
另外,在这个微不足道的程序中,我们可以在调用本地方法之前加载库,因为我们在其他地方没有使用本地库。
3.2. Implementing a Method in C++
3.2.在C++中实现一个方法
Now, we need to create the implementation of our native method in C++.
现在,我们需要在C++中创建本地方法的实现。
Within C++ the definition and the implementation are usually stored in .h and .cpp files respectively.
在C++中,定义和实现通常分别存储在.h和.cpp文件中。
First, to create the definition of the method, we have to use the -h flag of the Java compiler:
首先,为了创建方法的定义,我们必须使用Java编译器的-h标志。
javac -h . HelloWorldJNI.java
This will generate a com_baeldung_jni_HelloWorldJNI.h file with all the native methods included in the class passed as a parameter, in this case, only one:
这将生成一个com_baeldung_jni_HelloWorldJNI.h文件,其中包括作为参数传递的类中的所有本地方法,在这种情况下,只有一个。
JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
(JNIEnv *, jobject);
As we can see, the function name is automatically generated using the fully qualified package, class and method name.
正如我们所看到的,函数名称是使用完全合格的包、类和方法名称自动生成的。
Also, something interesting that we can notice is that we’re getting two parameters passed to our function; a pointer to the current JNIEnv; and also the Java object that the method is attached to, the instance of our HelloWorldJNI class.
另外,我们可以注意到一些有趣的事情,那就是我们得到了两个传递给我们函数的参数;一个指向当前JNIEnv;的指针,还有一个方法所依附的Java对象,即我们的HelloWorldJNI类的实例。
Now, we have to create a new .cpp file for the implementation of the sayHello function. This is where we’ll perform actions that print “Hello World” to console.
现在,我们必须创建一个新的.cpp文件来实现sayHello函数。在这里,我们将执行向控制台打印 “Hello World “的动作。
We’ll name our .cpp file with the same name as the .h one containing the header and add this code to implement the native function:
我们将命名我们的.cpp文件,与包含头文件的.h文件同名,并添加这段代码来实现本地函数。
JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
(JNIEnv* env, jobject thisObject) {
std::cout << "Hello from C++ !!" << std::endl;
}
3.3. Compiling and Linking
3.3.编译和连接
At this point, we have all parts we need in place and have a connection between them.
在这一点上,我们所需要的所有部件都已经到位,而且它们之间有了联系。。
We need to build our shared library from the C++ code and run it!
我们需要从C++代码中构建我们的共享库,并运行它。
To do so, we have to use G++ compiler, not forgetting to include the JNI headers from our Java JDK installation.
要做到这一点,我们必须使用G++编译器,不要忘记从我们的Java JDK安装中包含JNI头文件。
Ubuntu version:
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 version:
Windows版本。
g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
MacOS version;
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.
一旦我们把适合我们平台的代码编译成com_baeldung_jni_HelloWorldJNI.o文件,我们就必须把它纳入一个新的共享库。无论我们决定如何命名它,都是传入方法System.loadLibrary的参数。
We named ours “native”, and we’ll load it when running our Java code.
我们把我们的命名为 “本地”,我们将在运行我们的Java代码时加载它。
The G++ linker then links the C++ object files into our bridged library.
然后G++链接器将C++对象文件链接到我们的桥接库。
Ubuntu version:
Ubuntu版本。
g++ -shared -fPIC -o libnative.so com_baeldung_jni_HelloWorldJNI.o -lc
Windows version:
Windows版本。
g++ -shared -o native.dll com_baeldung_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias
MacOS version:
MacOS版本。
g++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc
And that’s it!
就这样吧!
We can now run our program from the command line.
现在我们可以从命令行运行我们的程序。
However, we need to add the full path to the directory containing the library we’ve just generated. This way Java will know where to look for our native libs:
然而,我们需要添加包含我们刚刚生成的库的目录的完整路径。这样,Java将知道在哪里寻找我们的本地库。
java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.baeldung.jni.HelloWorldJNI
Console output:
控制台输出。
Hello from C++ !!
4. Using Advanced JNI Features
4.使用先进的JNI功能
Saying hello is nice but not very useful. Usually, we would like to exchange data between Java and C++ code and manage this data in our program.
打招呼很好,但作用不大。通常情况下,我们希望在Java和C++代码之间交换数据,并在我们的程序中管理这些数据。
4.1. Adding Parameters To Our Native Methods
4.1.为我们的本地方法添加参数
We’ll add some parameters to our native methods. Let’s create a new class called ExampleParametersJNI with two native methods using parameters and returns of different types:
我们将为我们的本地方法添加一些参数。让我们创建一个名为ExampleParametersJNI的新类,其中有两个使用不同类型的参数和回报的本地方法。
private native long sumIntegers(int first, int second);
private native String sayHelloToMe(String name, boolean isFemale);
And then, repeat the procedure to create a new .h file with “javac -h” as we did before.
然后,重复我们之前的步骤,用 “javac -h “创建一个新的.h文件。
Now create the corresponding .cpp file with the implementation of the new C++ method:
现在用新的C++方法的实现创建相应的.cpp文件。
...
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());
}
...
We’ve used the pointer *env of type JNIEnv to access the methods provided by the JNI environment instance.
我们使用了*env类型的指针JNIEnv来访问JNI环境实例所提供的方法。
JNIEnv allows us, in this case, to pass Java Strings into our C++ code and back out without worrying about the implementation.
在这种情况下,JNIEnv允许我们将Java Strings传入我们的C++代码,然后再传出,而不必担心实现问题。
We can check the equivalence of Java types and C JNI types into Oracle official documentation.
我们可以在Oracle官方文档中检查Java类型和C JNI类型的等效性。。
To test our code, we’ve to repeat all the compilation steps of the previous HelloWorld example.
为了测试我们的代码,我们必须重复前面HelloWorld例子的所有编译步骤。
4.2. Using Objects and Calling Java Methods From Native Code
4.2.使用对象和从本地代码调用Java方法
In this last example, we’re going to see how we can manipulate Java objects into our native C++ code.
在这最后一个例子中,我们将看到我们如何在我们的本地C++代码中操作Java对象。
We’ll start creating a new class UserData that we’ll use to store some user info:
我们将开始创建一个新的类UserData,我们将用它来存储一些用户信息。
package com.baeldung.jni;
public class UserData {
public String name;
public double balance;
public String getUserInfo() {
return "[name]=" + name + ", [balance]=" + balance;
}
}
Then, we’ll create another Java class called ExampleObjectsJNI with some native methods with which we’ll manage objects of type UserData:
然后,我们将创建另一个名为ExampleObjectsJNI的Java类,其中有一些本地方法,我们将用这些方法管理UserData类型的对象。
...
public native UserData createUser(String name, double balance);
public native String printUserData(UserData user);
One more time, let’s create the .h header and then the C++ implementation of our native methods on a new .cpp file:
再来一次,让我们创建.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;
}
Again, we’re using the JNIEnv *env pointer to access the needed classes, objects, fields and methods from the running JVM.
同样,我们使用JNIEnv *env指针来访问运行中的JVM中需要的类、对象、域和方法。
Normally, we just need to provide the full class name to access a Java class, or the correct method name and signature to access an object method.
通常情况下,我们只需要提供完整的类名来访问一个Java类,或者提供正确的方法名和签名来访问一个对象方法。
We’re even creating an instance of the class com.baeldung.jni.UserData in our native code. Once we have the instance, we can manipulate all its properties and methods in a way similar to Java reflection.
我们甚至要在本地代码中创建一个com.baeldung.jni.UserData类的实例。一旦我们有了这个实例,我们就可以用类似于Java反射的方式来操作它的所有属性和方法。
We can check all other methods of JNIEnv into the Oracle official documentation.
我们可以在Oracle官方文档中查看JNIEnv的所有其他方法。
4. Disadvantages of Using JNI
4.使用JNI的劣势
JNI bridging does have its pitfalls.
JNI桥接确实有其缺陷。
The main downside being the dependency on the underlying platform; we essentially lose the “write once, run anywhere” feature of Java. This means that we’ll have to build a new lib for each new combination of platform and architecture we want to support. Imagine the impact that this could have on the build process if we supported Windows, Linux, Android, MacOS…
主要的缺点是对底层平台的依赖性;我们基本上失去了Java的 “一次编写,随处运行 “的特点。这意味着我们必须为我们想要支持的每个新的平台和架构组合建立一个新的库。想象一下,如果我们支持Windows、Linux、Android、MacOS,这将对构建过程产生怎样的影响……
JNI not only adds a layer of complexity to our program. It also adds a costly layer of communication between the code running into the JVM and our native code: we need to convert the data exchanged in both ways between Java and C++ in a marshaling/unmarshaling process.
JNI不仅给我们的程序增加了一层复杂性。它还在运行到JVM中的代码和我们的本地代码之间增加了一个昂贵的通信层:我们需要在Marshaling/unmarshaling过程中转换Java和C++之间以两种方式交换的数据。
Sometimes there isn’t even a direct conversion between types so we’ll have to write our equivalent.
有时候,类型之间甚至没有直接的转换,所以我们必须编写我们的等价物。
5. Conclusion
5.结论
Compiling the code for a specific platform (usually) makes it faster than running bytecode.
为特定的平台(通常)编译代码,使其比运行字节码更快。
This makes it useful when we need to speed up a demanding process. Also, when we don’t have other alternatives such as when we need to use a library that manages a device.
这使得它在我们需要加快一个苛刻的过程时很有用。另外,当我们没有其他的选择时,例如我们需要使用一个管理设备的库时。
However, this comes at a price as we’ll have to maintain additional code for each different platform we support.
然而,这是有代价的,因为我们将不得不为我们支持的每个不同的平台维护额外的代码。
That’s why it’s usually a good idea to only use JNI in the cases where there’s no Java alternative.
这就是为什么通常只在没有Java替代物的情况下使用JNI是个好主意。
As always the code for this article is available over on GitHub.
像往常一样,本文的代码可在GitHub上获得over。