Leitfaden zu JNI (Java Native Interface)
1. Einführung
Wie wir wissen, liegt eine der Hauptstärken von Java in der Portabilität. Das heißt, sobald wir Code geschrieben und kompiliert haben, ist das Ergebnis dieses Prozesses plattformunabhängiger Bytecode.
Einfach ausgedrückt, kann dies auf jeder Maschine oder jedem Gerät ausgeführt werden, auf dem eine Java Virtual Machine ausgeführt werden kann, und es wird so nahtlos funktionieren, wie wir es erwarten können.
Manchmal jedochwe do actually need to use code that’s natively-compiled for a specific architecture.
Es kann einige Gründe dafür geben, systemeigenen Code zu verwenden:
-
Die Notwendigkeit, mit Hardware umzugehen
-
Leistungssteigerung für einen sehr anspruchsvollen Prozess
-
Eine vorhandene Bibliothek, die wir wiederverwenden möchten, anstatt sie in Java neu zu schreiben.
To achieve this, the JDK introduces a bridge between the bytecode running in our JVM and the native code (normalerweise in C oder C ++ geschrieben).
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 Schlüsselwortnativebereit, mit dem angegeben wird, dass die Methodenimplementierung von einem nativen Code bereitgestellt wird.
Normalerweise können wir bei der Erstellung eines nativen ausführbaren Programms zwischen statischen und gemeinsam genutzten Bibliotheken wählen:
-
Statische Bibliotheken - Alle Bibliotheksbinärdateien werden während des Verknüpfungsprozesses als Teil unserer ausführbaren Datei aufgenommen. Daher benötigen wir die Bibliotheken nicht mehr, erhöhen jedoch die Größe unserer ausführbaren Datei.
-
Freigegebene Bibliotheken - Die endgültige ausführbare Datei enthält nur Verweise auf die Bibliotheken, nicht auf den Code selbst. Voraussetzung ist, 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 behält unsere gemeinsam genutzte Bibliothek den nativen Code separat in der.so/.dll/.dylib-Datei (abhängig vom verwendeten Betriebssystem) bei, anstatt Teil unserer Klassen zu sein.
Das Schlüsselwortnativeverwandelt unsere Methode in eine Art abstrakte Methode:
private native void aNativeMethod();
Mit dem Hauptunterschied, dassinstead of being implemented by another Java class, it will be implemented in a separated native shared library.
Eine Tabelle mit Zeigern im Speicher auf die Implementierung aller unserer nativen Methoden wird so erstellt, dass 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 umfassen mindestens einenative-Methode.
-
Nativer Code - die eigentliche Logik unserer nativen Methoden, normalerweise in C oder C ++ codiert.
-
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 – 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-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++ elements (many of them defined within jni.h)
-
JNIEXPORT- markiert die Funktion in der gemeinsam genutzten Bibliothek als exportierbar, damit sie in die Funktionstabelle aufgenommen wird und somit von JNI gefunden werden kann
-
JNICALL - In Kombination mitJNIEXPORT wird sichergestellt, dass unsere Methoden für das JNI-Framework verfügbar sind
-
JNIEnv - eine Struktur, die Methoden enthält, mit denen wir unseren nativen Code für den Zugriff auf Java-Elemente verwenden können
-
JavaVM - eine Struktur, mit der wir eine laufende JVM manipulieren (oder sogar eine neue starten) können, indem wir ihr Threads hinzufügen, sie zerstören usw.
3. Hallo Welt JNI
Als nächsteslet’s look at how JNI works in practice.
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 - Befehl“sudo apt-get install build-essential” in einem Terminal ausführen
-
Windows -Install 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.
Zunächst erstellen wir die folgende Java-Klasse, die die systemeigene Methode enthält, mit der die Arbeit ausgeführt wird:
package com.example.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();
}
Wie wir sehen können,we load the shared library in a static block. Dies stellt sicher, dass es bereit ist, wenn wir es brauchen und von wo auch immer wir es brauchen.
Alternativ können wir in diesem einfachen Programm stattdessen die Bibliothek laden, bevor wir unsere native Methode aufrufen, da wir die native Bibliothek nirgendwo anders verwenden.
3.2. Implementieren einer Methode in C ++
Jetzt müssen wir die Implementierung unserer nativen Methode in C ++ erstellen.
In C ++ werden die Definition und die Implementierung normalerweise in.h- bzw..cpp-Dateien gespeichert.
Erstensto create the definition of the method, we have to use the -h flag of the Java compiler:
javac -h . HelloWorldJNI.java
Dadurch wird einecom_example_jni_HelloWorldJNI.h-Datei mit allen nativen Methoden generiert, die in der als Parameter übergebenen Klasse enthalten sind. In diesem Fall nur eine:
JNIEXPORT void JNICALL Java_com_example_jni_HelloWorldJNI_sayHello
(JNIEnv *, jobject);
Wie wir sehen können, wird der Funktionsname automatisch unter Verwendung des vollständig qualifizierten Paket-, Klassen- und Methodennamens generiert.
Interessant ist auch, dass zwei Parameter an unsere Funktion übergeben werden. Ein Zeiger auf das aktuelleJNIEnv; und auch auf das Java-Objekt, an das die Methode angehängt ist, die Instanz unsererHelloWorldJNI-Klasse.
Jetzt müssen wir eine neue.cpp-Datei für die Implementierung dersayHello-Funktion erstellen. This is where we’ll perform actions that print “Hello World” to console.
Wir benennen unsere.cpp-Datei mit demselben Namen wie die .h-Datei, die den Header enthält, und fügen diesen Code hinzu, um die native Funktion zu implementieren:
JNIEXPORT void JNICALL Java_com_example_jni_HelloWorldJNI_sayHello
(JNIEnv* env, jobject thisObject) {
std::cout << "Hello from C++ !!" << std::endl;
}
3.3. Kompilieren und Verknüpfen
Zu diesem Zeitpunkt haben wir alle Teile, die wir benötigen, und haben eine Verbindung zwischen ihnen.
Wir müssen unsere gemeinsam genutzte Bibliothek aus dem C ++ - Code erstellen und ausführen!
Dazu müssen wir den G ++ - Compilernot forgetting to include the JNI headers from our Java JDK installation verwenden.
Ubuntu-Version:
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o
Windows-Version:
g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o
MacOS-Version;
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o
Sobald wir den Code für unsere Plattform in die Dateicom_example_jni_HelloWorldJNI.o kompiliert haben, müssen wir ihn in eine neue gemeinsam genutzte Bibliothek aufnehmen. Whatever we decide to name it is the argument passed into the method System.loadLibrary.
Wir haben unsere "native" genannt und werden sie laden, wenn wir unseren Java-Code ausführen.
Der G-Linker verknüpft dann die C-Objektdateien mit unserer überbrückten Bibliothek.
Ubuntu-Version:
g++ -shared -fPIC -o libnative.so com_example_jni_HelloWorldJNI.o -lc
Windows-Version:
g++ -shared -o native.dll com_example_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias
MacOS-Version:
g++ -dynamiclib -o libnative.dylib com_example_jni_HelloWorldJNI.o -lc
Und das ist es!
Wir können unser Programm jetzt von der Kommandozeile aus starten.
we need to add the full path to the directory containing the library we’ve just generated. Auf diese Weise weiß Java, wo nach unseren nativen Bibliotheken gesucht werden muss:
java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.example.jni.HelloWorldJNI
Konsolenausgabe:
Hello from C++ !!
4. Verwenden erweiterter JNI-Funktionen
Hallo zu sagen ist nett, aber nicht sehr nützlich. Usually, we would like to exchange data between Java and C++ code and manage this data in our program.
4.1. Hinzufügen von Parametern zu unseren systemeigenen Methoden
Wir werden unseren nativen Methoden einige Parameter hinzufügen. Erstellen wir eine neue Klasse namensExampleParametersJNI mit zwei nativen Methoden unter Verwendung von Parametern und Rückgaben verschiedener Typen:
private native long sumIntegers(int first, int second);
private native String sayHelloToMe(String name, boolean isFemale);
Wiederholen Sie anschließend den Vorgang, um eine neue .h-Datei mit "javac -h" zu erstellen.
Erstellen Sie nun die entsprechende CPP-Datei mit der Implementierung der neuen C ++ - Methode:
...
JNIEXPORT jlong JNICALL Java_com_example_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_example_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());
}
...
Wir haben den Zeiger*env vom TypJNIEnv verwendet, um auf die von der JNI-Umgebungsinstanz bereitgestellten Methoden zuzugreifen.
MitJNIEnv können wir in diesem Fall JavaStrings an unseren C ++ - Code übergeben und zurücksetzen, ohne uns um die Implementierung kümmern zu müssen.
Wir können die Äquivalenz von Java-Typen und C JNI-Typen inOracle official documentation. überprüfen
Um unseren Code zu testen, müssen wir alle Kompilierungsschritte des vorherigen Beispiels vonHelloWorldwiederholen.
4.2. Verwenden von Objekten und Aufrufen von Java-Methoden aus nativem Code
In diesem letzten Beispiel werden wir sehen, wie wir Java-Objekte in unseren nativen C ++ - Code bearbeiten können.
Wir werden eine neue KlasseUserDataerstellen, in der einige Benutzerinformationen gespeichert werden:
package com.example.jni;
public class UserData {
public String name;
public double balance;
public String getUserInfo() {
return "[name]=" + name + ", [balance]=" + balance;
}
}
Anschließend erstellen wir eine weitere Java-Klasse namensExampleObjectsJNI mit einigen nativen Methoden, mit denen wir Objekte vom TypUserData verwalten:
...
public native UserData createUser(String name, double balance);
public native String printUserData(UserData user);
Lassen Sie uns noch einmal den.h-Header und dann die C ++ - Implementierung unserer nativen Methoden für eine neue.cpp-Datei erstellen:
JNIEXPORT jobject JNICALL Java_com_example_jni_ExampleObjectsJNI_createUser
(JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {
// Create the object of the class UserData
jclass userDataClass = env->FindClass("com/example/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_example_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;
}
Wieder verwenden wir denJNIEnv *env-Zeiger, um über die laufende JVM auf die erforderlichen Klassen, Objekte, Felder und Methoden zuzugreifen.
Normalerweise müssen wir nur den vollständigen Klassennamen für den Zugriff auf eine Java-Klasse oder den korrekten Methodennamen und die korrekte Signatur für den Zugriff auf eine Objektmethode angeben.
Wir erstellen sogar eine Instanz der Klassecom.example.jni.UserDatain unserem nativen Code. Once we have the instance, we can manipulate all its properties and methods in a way similar to Java reflection.
Wir können alle anderen Methoden vonJNIEnv inOracle official documentation einchecken.
4. Nachteile der Verwendung von JNI
JNI-Brücken haben ihre Tücken.
Der Hauptnachteil ist die Abhängigkeit von der zugrunde liegenden Plattform; we essentially lose the “write once, run anywhere” Funktion von Java. Dies bedeutet, dass wir für jede neue Kombination aus Plattform und Architektur, die wir unterstützen möchten, eine neue Bibliothek erstellen müssen. Stellen Sie sich vor, welche Auswirkungen dies auf den Erstellungsprozess haben könnte, wenn wir Windows, Linux, Android, MacOS ... unterstützen würden.
JNI fügt unserem Programm nicht nur eine Schicht Komplexität hinzu. It also adds a costly layer of communication zwischen dem in die JVM ausgeführten Code und unserem nativen Code: Wir müssen die Daten, die auf beide Arten zwischen Java und C ++ ausgetauscht werden, in einem Marshalling- / Unmarshaling-Prozess konvertieren.
Manchmal gibt es nicht einmal eine direkte Konvertierung zwischen Typen, daher müssen wir unser Äquivalent schreiben.
5. Fazit
Durch Kompilieren des Codes für eine bestimmte Plattform wird dieser (normalerweise) schneller ausgeführt als durch Ausführen von Bytecode.
Dies ist nützlich, wenn wir einen anspruchsvollen Prozess beschleunigen müssen. Auch wenn wir keine anderen Alternativen haben, z. B. wenn wir eine Bibliothek verwenden müssen, die ein Gerät verwaltet.
Dies hat jedoch einen Preis, da wir für jede von uns unterstützte Plattform zusätzlichen Code verwalten müssen.
Deshalb ist es normalerweise eine gute Idee,only use JNI in the cases where there’s no Java alternative.
Wie immer ist der Code für diesen Artikelover on GitHub verfügbar.