Anleitung zu JNI (Java Native Interface)

1. Einleitung

Wie wir wissen, ist eine der Hauptstärken von Java seine Portabilität - das heißt, sobald wir Code schreiben und kompilieren, ist das Ergebnis dieses Prozesses plattformunabhängiger Bytecode.

Einfach ausgedrückt, kann dies auf jedem Computer oder Gerät ausgeführt werden, auf dem eine Java Virtual Machine ausgeführt werden kann, und es funktioniert so nahtlos, wie wir es erwarten könnten.

Manchmal müssen wir jedoch tatsächlich Code verwenden, der für eine bestimmte Architektur nativ kompiliert wurde .

Es kann einige Gründe geben, nativen Code zu verwenden:

  • Die Notwendigkeit, mit Hardware umzugehen
  • Leistungsverbesserung für einen sehr anspruchsvollen Prozess
  • Eine vorhandene Bibliothek, die wir wiederverwenden möchten, anstatt sie in Java neu zu schreiben.

Um dies zu erreichen, führt das JDK eine Brücke zwischen dem in unserer JVM ausgeführten Bytecode und dem nativen Code (normalerweise in C oder C ++ geschrieben) ein.

Das Tool heißt Java Native Interface. In diesem Artikel werden wir sehen, wie es ist, Code damit zu schreiben.

2. Wie es funktioniert

2.1. Native Methoden: Die JVM trifft auf kompilierten Code

Java stellt das native Schlüsselwort bereit , das angibt, dass die Methodenimplementierung von einem nativen Code bereitgestellt wird.

Normalerweise können wir beim Erstellen eines nativen ausführbaren Programms wählen, ob statische oder gemeinsam genutzte Bibliotheken verwendet werden sollen:

  • Statische Bibliotheken - Alle Bibliotheksbinärdateien werden während des Verknüpfungsprozesses als Teil unserer ausführbaren Datei aufgenommen. Daher werden wir die Bibliotheken nicht mehr benötigen, aber es wird die Größe unserer ausführbaren Datei erhöhen.
  • Freigegebene Bibliotheken - Die endgültige ausführbare Datei enthält nur Verweise auf die Bibliotheken, nicht auf den Code selbst. Es erfordert, dass die Umgebung, in der wir unsere ausführbare Datei ausführen, Zugriff auf alle Dateien der von unserem Programm verwendeten Bibliotheken hat.

Letzteres ist für JNI sinnvoll, da wir Bytecode und nativ kompilierten Code nicht in derselben Binärdatei mischen können.

Daher wird unsere native Bibliothek den nativen Code separat in ihrer .so / .dll / .dylib- Datei (abhängig vom verwendeten Betriebssystem) aufbewahren, anstatt Teil unserer Klassen zu sein.

Das native Schlüsselwort verwandelt unsere Methode in eine Art abstrakte Methode:

private native void aNativeMethod();

Mit dem Hauptunterschied, dass es nicht von einer anderen Java-Klasse implementiert wird, sondern in einer separaten nativen gemeinsam genutzten Bibliothek .

Eine Tabelle mit Zeigern im Speicher auf die Implementierung aller unserer nativen Methoden wird erstellt, damit sie aus unserem Java-Code aufgerufen werden können.

2.2. Erforderliche Komponenten

Hier finden Sie eine kurze Beschreibung der wichtigsten Komponenten, die wir berücksichtigen müssen. Wir werden sie später in diesem Artikel näher erläutern

  • Java Code - unsere Klassen. Sie enthalten mindestens eine native Methode.
  • Native Code - die eigentliche Logik unserer nativen Methoden, die normalerweise in C oder C ++ codiert sind.
  • JNI-Header-Datei - Diese Header-Datei für C / C ++ ( include / jni.h im JDK-Verzeichnis) enthält alle Definitionen von JNI-Elementen, die wir in unseren nativen Programmen verwenden können.
  • C / C ++ - Compiler - Wir können zwischen GCC, Clang, Visual Studio oder einem anderen Compiler wählen, um eine native gemeinsam genutzte Bibliothek für unsere Plattform zu generieren.

2.3. JNI-Elemente im Code (Java und C / C ++)

Java-Elemente:

  • Schlüsselwort "native" - ​​Wie bereits erwähnt, muss jede als native gekennzeichnete Methode in einer nativen, gemeinsam genutzten Bibliothek implementiert werden.
  • System.loadLibrary (String libname) - eine statische Methode, die eine gemeinsam genutzte Bibliothek aus dem Dateisystem in den Speicher lädt und ihre exportierten Funktionen für unseren Java-Code verfügbar macht.

C / C ++ - Elemente (viele davon in jni.h definiert )

  • JNIEXPORT - markiert die Funktion in der gemeinsam genutzten Bibliothek als exportierbar, damit sie in die Funktionstabelle aufgenommen wird und JNI sie finden kann
  • JNICALL - In Kombination mit JNIEXPORT wird sichergestellt, dass unsere Methoden für das JNI-Framework verfügbar sind
  • JNIEnv - eine Struktur mit Methoden, mit denen wir mit unserem nativen Code auf Java-Elemente zugreifen können
  • JavaVM - eine Struktur, mit der wir eine laufende JVM manipulieren (oder sogar eine neue starten) können, indem wir Threads hinzufügen, sie zerstören usw.

3. Hallo Welt JNI

Schauen wir uns als nächstes an, wie JNI in der Praxis funktioniert.

In diesem Tutorial verwenden wir C ++ als Muttersprache und G ++ als Compiler und Linker.

Wir können jeden anderen Compiler unserer Wahl verwenden, aber hier erfahren Sie, wie Sie G ++ unter Ubuntu, Windows und MacOS installieren:

  • Ubuntu Linux - Führen Sie den Befehl "sudo apt-get install build-essential" in einem Terminal aus
  • Windows - Installieren Sie MinGW
  • MacOS - Führen Sie den Befehl "g ++" in einem Terminal aus. Wenn er noch nicht vorhanden ist, wird er installiert.

3.1. Erstellen der Java-Klasse

Beginnen wir mit der Erstellung unseres ersten JNI-Programms, indem wir eine klassische „Hallo Welt“ implementieren.

To begin, we create the following Java class that includes the native method that will perform the work:

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++

Now, we need to create the implementation of our native method in C++.

Within C++ the definition and the implementation are usually stored in .h and .cpp files respectively.

First, to create the definition of the method, we have to use the -h flag of the Java compiler:

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:

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.

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.

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:

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

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!

To do so, we have to use G++ compiler, not forgetting to include the JNI headers from our Java JDK installation.

Ubuntu version:

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:

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

MacOS version;

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

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 -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.baeldung.jni.HelloWorldJNI

Console output:

Hello from C++ !!

4. Using Advanced JNI Features

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.

4.1. Adding Parameters To Our Native Methods

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:

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.

Now create the corresponding .cpp file with the implementation of the new C++ method:

... 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 
    
     NewStringUTF(fullName.c_str()); } ...
    

We've used the pointer *env of type JNIEnv to access the methods provided by the JNI environment instance.

JNIEnv allows us, in this case, to pass Java Strings into our C++ code and back out without worrying about the implementation.

We can check the equivalence of Java types and C JNI types into Oracle official documentation.

To test our code, we've to repeat all the compilation steps of the previous HelloWorld example.

4.2. Using Objects and Calling Java Methods From Native Code

In this last example, we're going to see how we can manipulate Java objects into our native C++ code.

We'll start creating a new class UserData that we'll use to store some user info:

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:

... 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:

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.

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.

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.

We can check all other methods of JNIEnv into the Oracle official documentation.

4. Disadvantages Of Using JNI

JNI bridging does have its pitfalls.

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…

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.

Sometimes there isn't even a direct conversion between types so we'll have to write our equivalent.

5. Conclusion

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.

Aus diesem Grund ist es normalerweise eine gute Idee, JNI nur in Fällen zu verwenden, in denen es keine Java-Alternative gibt .

Wie immer ist der Code für diesen Artikel auf GitHub verfügbar.