Guide til JNI (Java Native Interface)

1. Introduktion

Som vi ved er en af ​​de største styrker ved Java dets bærbarhed - hvilket betyder, at når vi først skriver og kompilerer kode, er resultatet af denne proces platformuafhængig bytecode.

Kort sagt kan dette køre på enhver maskine eller enhed, der er i stand til at køre en Java Virtual Machine, og det fungerer så problemfrit, som vi kunne forvente.

Imidlertid nogle gange Vi skal faktisk bruge kode, der er oprindeligt kompileret til en bestemt arkitektur.

Der kan være nogle grunde til at skulle bruge oprindelig kode:

  • Behovet for at håndtere noget hardware
  • Ydelsesforbedring til en meget krævende proces
  • Et eksisterende bibliotek, som vi vil genbruge i stedet for at omskrive det i Java.

For at opnå dette introducerer JDK en bro mellem bytekoden, der kører i vores JVM, og den oprindelige kode (normalt skrevet i C eller C ++).

Værktøjet kaldes Java Native Interface. I denne artikel vil vi se, hvordan det er at skrive en kode med den.

2. Sådan fungerer det

2.1. Indfødte metoder: JVM opfylder kompileret kode

Java leverer hjemmehørende nøgleord, der bruges til at indikere, at implementeringen af ​​metoden leveres af en native kode.

Normalt, når vi laver et oprindeligt eksekverbart program, kan vi vælge at bruge statiske eller delte libs:

  • Statiske libs - alle biblioteksbinarier inkluderes som en del af vores eksekverbare under sammenkædningsprocessen. Således har vi ikke brug for libs længere, men det vil øge størrelsen på vores eksekverbare fil.
  • Delt libs - den endelige eksekverbare har kun referencer til libs, ikke selve koden. Det kræver, at miljøet, hvor vi kører vores eksekverbare, har adgang til alle de filer på libs, der bruges af vores program.

Sidstnævnte er det, der giver mening for JNI, da vi ikke kan blande bytecode og indbygget kode i den samme binære fil.

Derfor holder vores delte lib den oprindelige kode separat inden for sin .so / .dll / .dylib fil (afhængigt af hvilket operativsystem vi bruger) i stedet for at være en del af vores klasser.

Det hjemmehørende nøgleord omdanner vores metode til en slags abstrakt metode:

privat indfødt ugyldigt aNativeMethod ();

Med den største forskel i stedet for at blive implementeret af en anden Java-klasse, vil den blive implementeret i et separat indbygget delt bibliotek.

En tabel med pekere i hukommelsen til implementeringen af ​​alle vores native metoder vil blive konstrueret, så de kan kaldes fra vores Java-kode.

2.2. Nødvendige komponenter

Her er en kort beskrivelse af de nøglekomponenter, som vi skal tage i betragtning. Vi forklarer dem yderligere senere i denne artikel

  • Java-kode - vores klasser. De vil omfatte mindst en hjemmehørende metode.
  • Native Code - den faktiske logik for vores oprindelige metoder, normalt kodet i C eller C ++.
  • JNI header-fil - denne header-fil til C / C ++ (inkluderer / jni.h i JDK-biblioteket) indeholder alle definitioner af JNI-elementer, som vi kan bruge i vores oprindelige programmer.
  • C / C ++ Compiler - vi kan vælge mellem GCC, Clang, Visual Studio eller andre, som vi kan lide, så vidt det er i stand til at generere et indbygget delt bibliotek til vores platform.

2.3. JNI-elementer i kode (Java og C / C ++)

Java-elementer:

  • "Native" nøgleord - som vi allerede har dækket, skal enhver metode, der er markeret som native, implementeres i et native, delt lib.
  • System.loadLibrary (String libname) - en statisk metode, der indlæser et delt bibliotek fra filsystemet i hukommelsen og gør dets eksporterede funktioner tilgængelige for vores Java-kode.

C / C ++ - elementer (mange af dem defineret inden for jni.h)

  • JNIEXPORT- markerer funktionen i den delte lib som eksporterbar, så den vil blive inkluderet i funktionstabellen, og dermed kan JNI finde den
  • JNICALL - kombineret med JNIEXPORT, det sikrer, at vores metoder er tilgængelige for JNI-rammen
  • JNIEnv - en struktur, der indeholder metoder, som vi kan bruge vores oprindelige kode til at få adgang til Java-elementer
  • JavaVM - en struktur, der lader os manipulere en kørende JVM (eller endda starte en ny), der tilføjer tråde til den, ødelægger den osv ...

3. Hej Verden JNI

Næste, lad os se på, hvordan JNI fungerer i praksis.

I denne vejledning bruger vi C ++ som modersmål og G ++ som compiler og linker.

Vi kan bruge enhver anden kompilator, som vi foretrækker, men her skal du installere G ++ på Ubuntu, Windows og MacOS:

  • Ubuntu Linux - kør kommando “Sudo apt-get install build-essential” i en terminal
  • Windows - Installer MinGW
  • MacOS - kør kommando “G ++” i en terminal, og hvis den endnu ikke er til stede, installerer den den.

3.1. Oprettelse af Java-klassen

Lad os begynde at oprette vores første JNI-program ved at implementere et klassisk "Hello World".

Til at begynde med opretter vi følgende Java-klasse, der inkluderer den indfødte metode, der udfører arbejdet:

pakke com.baeldung.jni; offentlig klasse HelloWorldJNI {statisk {System.loadLibrary ("native"); } offentlig statisk ugyldig hoved (String [] args) {new HelloWorldJNI (). sayHello (); } // Erklær en native metode sayHello (), der ikke modtager argumenter og returnerer ugyldige private native void sayHello (); }

Som vi kan se, vi indlæser det delte bibliotek i en statisk blok. Dette sikrer, at det vil være klar, når vi har brug for det, og hvorfra vi har brug for det.

Alternativt, i dette trivielle program, kunne vi i stedet indlæse biblioteket lige før vi kalder vores oprindelige metode, fordi vi ikke bruger det oprindelige bibliotek andre steder.

3.2. Implementering af en metode i C ++

Nu skal vi oprette implementeringen af ​​vores oprindelige metode i C ++.

Inden for C ++ er definitionen og implementeringen normalt gemt i .h og .cpp filer henholdsvis.

Først, for at skabe definitionen af ​​metoden skal vi bruge -h Java-kompilatorens flag:

javac -h. HelloWorldJNI.java

Dette vil generere en com_baeldung_jni_HelloWorldJNI.h fil med alle de indfødte metoder inkluderet i klassen bestået som parameter, i dette tilfælde kun en:

JNIEXPORT ugyldigt JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject); 

Som vi kan se, genereres funktionsnavnet automatisk ved hjælp af den fuldt kvalificerede pakke, klasse og metode navn.

Også noget interessant, som vi kan bemærke, er, at vi får to parametre sendt til vores funktion; en markør til strømmen JNIEnv; og også det Java-objekt, som metoden er knyttet til, forekomsten af ​​vores HelloWorldJNI klasse.

Nu er vi nødt til at oprette en ny .cpp fil til gennemførelse af sig hej fungere. Det er her, vi udfører handlinger, der udskriver "Hello World" til konsol.

Vi navngiver vores .cpp fil med samme navn som .h-filen, der indeholder overskriften, og tilføj denne kode for at implementere den oprindelige funktion:

JNIEXPORT ugyldigt JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv * env, jobject thisObject) {std :: cout << "Hej fra C ++ !!" << std :: endl; } 

3.3. Kompilering og sammenkædning

På dette tidspunkt har vi alle de dele, vi har brug for, på plads og har en forbindelse mellem dem.

Vi er nødt til at opbygge vores delte bibliotek fra C ++ - koden og køre det!

For at gøre det skal vi bruge G ++ compiler, ikke at glemme at inkludere JNI-overskrifter fra vores Java JDK-installation.

Ubuntu-version:

g ++ -c -fPIC -I $ {JAVA_HOME} / inkluderer -I $ {JAVA_HOME} / inkluderer / 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} / inkluderer -I $ {JAVA_HOME} / inkluderer / darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Når vi har koden samlet til vores platform i filen com_baeldung_jni_HelloWorldJNI.o, vi skal inkludere det i et nyt delt bibliotek. Uanset hvad vi beslutter at navngive det, er argumentet, der overføres til metoden System.loadLibrary.

Vi kaldte vores ”native”, og vi indlæser den, når vi kører vores Java-kode.

G ++ linkeren forbinder derefter C ++ objektfiler til vores brobyggede bibliotek.

Ubuntu-version:

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

Og det er det!

Vi kan nu køre vores program fra kommandolinjen.

Imidlertid, vi er nødt til at tilføje den fulde sti til biblioteket, der indeholder det bibliotek, vi lige har genereret. På denne måde ved Java, hvor de skal kigge efter vores oprindelige libs:

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

Konsol output:

Hej fra C ++ !!

4. Brug af avancerede JNI-funktioner

At sige hej er rart, men ikke særlig nyttigt. Normalt vil vi gerne udveksle data mellem Java og C ++ - kode og administrere disse data i vores program.

4.1. Tilføjelse af parametre til vores oprindelige metoder

Vi tilføjer nogle parametre til vores oprindelige metoder. Lad os oprette en ny klasse kaldet EksempelParametreJNI med to native metoder ved hjælp af parametre og returnering af forskellige typer:

private indfødte lange sumIntegers (int første, int anden); privat indfødte String sayHelloToMe (strengnavn, boolsk erKvinde);

Gentag derefter proceduren for at oprette en ny .h-fil med “javac -h” som vi gjorde før.

Opret nu den tilsvarende .cpp-fil med implementeringen af ​​den nye C ++ metode:

... JNIEXPORT jlong ​​JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers (JNIEnv * env, jobject thisObject, jint first, jint second) {std :: cout << "C ++: De modtagne tal er:" << første << "og" << sekund NewStringUTF (fuldnavn.c_str ()); } ...

Vi har brugt markøren * env af typen JNIEnv for at få adgang til de metoder, der leveres af JNI-miljøinstansen.

JNIEnv tillader os, i dette tilfælde, at passere Java Strenge ind i vores C ++ - kode og gå ud uden at bekymre dig om implementeringen.

Vi kan kontrollere ækvivalensen af ​​Java-typer og C JNI-typer i officiel Oracle-dokumentation.

For at teste vores kode skal vi gentage alle kompileringstrinnene i det foregående Hej Verden eksempel.

4.2. Brug af objekter og opkald til Java-metoder fra oprindelig kode

I dette sidste eksempel skal vi se, hvordan vi kan manipulere Java-objekter til vores oprindelige C ++ - kode.

Vi begynder at oprette en ny klasse Brugerdata som vi bruger til at gemme nogle brugeroplysninger:

pakke com.baeldung.jni; offentlig klasse UserData {offentligt strengnavn; offentlig dobbelt balance public String getUserInfo () {return "[name] =" + name + ", [balance] =" + balance; }}

Derefter opretter vi en anden Java-klasse kaldet EksempelObjectsJNI med nogle native metoder, som vi administrerer objekter af typen med Brugerdata:

... offentlige native UserData createUser (strengnavn, dobbelt balance); offentlig native String printUserData (UserData-bruger); 

Lad os endnu en gang oprette .h header og derefter C ++ implementering af vores oprindelige metoder på en ny .cpp fil:

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser (JNIEnv * env, jobject thisObject, jstring name, jdouble balance) {// Opret objektet til klassen UserData jclass userDataClass = env-> FindClass ("com / baeldung /" jobject newUserData = env-> AllocObject (userDataClass); // Få UserData-felterne, der skal indstilles 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); returner newUserData; } JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData (JNIEnv * env, jobject thisObject, jobject userData) {// Find id'et til Java-metoden, der skal kaldes jclass userDataClass = env-> GetObjectClass (userData); jmethodID methodId = env-> GetMethodID (userDataClass, "getUserInfo", "() Ljava / lang / String;"); jstring resultat = (jstring) env-> CallObjectMethod (userData, methodId); returresultat } 

Igen bruger vi JNIEnv * env pointer for at få adgang til de nødvendige klasser, objekter, felter og metoder fra den kørende JVM.

Normalt skal vi bare give det fulde klassenavn for at få adgang til en Java-klasse eller det korrekte metodenavn og signatur for at få adgang til en objektmetode.

Vi opretter endda en instans af klassen com.baeldung.jni.UserData i vores oprindelige kode. Når vi først har forekomsten, kan vi manipulere alle dens egenskaber og metoder på en måde, der ligner Java-refleksion.

Vi kan kontrollere alle andre metoder til JNIEnv ind i den officielle Oracle-dokumentation.

4. Ulemper ved at bruge JNI

JNI-bro har sine faldgruber.

Den største ulempe er afhængigheden af ​​den underliggende platform; vi mister i det væsentlige "skriv en gang, kør hvor som helst" funktion af Java. Dette betyder, at vi bliver nødt til at opbygge en ny lib for hver nye kombination af platform og arkitektur, vi vil støtte. Forestil dig hvilken indvirkning dette kan have på byggeprocessen, hvis vi understøttede Windows, Linux, Android, MacOS ...

JNI tilføjer ikke kun et kompleksitetslag til vores program. Det tilføjer også et dyrt lag af kommunikation mellem koden, der løber ind i JVM og vores oprindelige kode: vi skal konvertere de data, der udveksles på begge måder mellem Java og C ++ i en marshaling / unmarshaling-proces.

Nogle gange er der ikke engang en direkte konvertering mellem typerne, så vi bliver nødt til at skrive vores tilsvarende.

5. Konklusion

Kompilering af koden til en bestemt platform (normalt) gør det hurtigere end at køre bytecode.

Dette gør det nyttigt, når vi skal fremskynde en krævende proces. Også når vi ikke har andre alternativer, som når vi har brug for et bibliotek, der administrerer en enhed.

Dette har dog en pris, da vi bliver nødt til at opretholde yderligere kode for hver anden platform, vi understøtter.

Derfor er det normalt en god idé at Brug kun JNI i de tilfælde, hvor der ikke er noget Java-alternativ.

Som altid er koden til denne artikel tilgængelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found