Guide til JNI (Java Native Interface)

1. Introduksjon

Som vi vet er en av hovedstyrkene til Java bærbarheten - noe som betyr at når vi skriver og kompilerer kode, er resultatet av denne prosessen plattformuavhengig bytekode.

Enkelt sagt, dette kan kjøre på en hvilken som helst maskin eller enhet som kan kjøre en Java Virtual Machine, og den vil fungere så sømløst som vi kunne forvente.

Imidlertid noen ganger vi trenger faktisk å bruke kode som er naturlig kompilert for en bestemt arkitektur.

Det kan være noen grunner til at du trenger å bruke innfødt kode:

  • Behovet for å håndtere noe maskinvare
  • Ytelsesforbedring for en veldig krevende prosess
  • Et eksisterende bibliotek som vi vil bruke på nytt i stedet for å skrive det om på Java.

For å oppnå dette introduserer JDK en bro mellom bytekoden som kjører i vår JVM og den opprinnelige koden (vanligvis skrevet i C eller C ++).

Verktøyet kalles Java Native Interface. I denne artikkelen får vi se hvordan det er å skrive litt kode med den.

2. Hvordan det fungerer

2.1. Innfødte metoder: JVM oppfyller kompilert kode

Java tilbyr innfødt nøkkelord som brukes til å indikere at implementeringen av metoden blir levert av en innfødt kode.

Normalt, når vi lager et eget kjørbart program, kan vi velge å bruke statiske eller delte biblioteker:

  • Statiske biblioteker - alle bibliotekbinarier blir inkludert som en del av vår kjørbare fil under koblingsprosessen. Dermed trenger vi ikke libs lenger, men det vil øke størrelsen på den kjørbare filen vår.
  • Delt libs - den endelige kjørbare filen har bare referanser til libs, ikke selve koden. Det krever at miljøet der vi kjører vår kjørbare fil har tilgang til alle filene til libs som brukes av programmet vårt.

Sistnevnte er det som er fornuftig for JNI, ettersom vi ikke kan blande bytekode og naturlig kompilert kode i samme binære fil.

Derfor vil vår delte lib holde den opprinnelige koden separat innenfor sin .so / .dll / .dylib filen (avhengig av hvilket operativsystem vi bruker) i stedet for å være en del av klassene våre.

De innfødt nøkkelord forvandler metoden vår til en slags abstrakt metode:

privat innfødt ugyldig aNativeMethod ();

Med hovedforskjellen som i stedet for å bli implementert av en annen Java-klasse, vil den implementeres i et atskilt eget delt bibliotek.

En tabell med pekere i minnet til implementeringen av alle våre innfødte metoder vil bli konstruert slik at de kan ringes fra vår Java-kode.

2.2. Komponenter som trengs

Her er en kort beskrivelse av nøkkelkomponentene vi må ta hensyn til. Vi forklarer dem nærmere senere i denne artikkelen

  • Java Code - våre klasser. De vil inneholde minst en innfødt metode.
  • Native Code - den faktiske logikken til våre opprinnelige metoder, vanligvis kodet i C eller C ++.
  • JNI header file - denne headerfilen for C / C ++ (inkluderer / jni.h inn i JDK-katalogen) inneholder alle definisjoner av JNI-elementer som vi kan bruke i våre opprinnelige programmer.
  • C / C ++ kompilator - vi kan velge mellom GCC, Clang, Visual Studio eller andre vi liker så langt det er i stand til å generere et eget delt bibliotek for plattformen vår.

2.3. JNI Elements in Code (Java And C / C ++)

Java-elementer:

  • "Innfødt" nøkkelord - som vi allerede har dekket, må enhver metode som er merket som innfødt implementeres i et eget, delt lib.
  • System.loadLibrary (String libname) - en statisk metode som laster et delt bibliotek fra filsystemet inn i minnet og gjør de eksporterte funksjonene tilgjengelige for vår Java-kode.

C / C ++ - elementer (mange av dem definert i jni.h)

  • JNIEXPORT- markerer funksjonen i delt lib som eksporterbar, slik at den blir inkludert i funksjonstabellen, og dermed kan JNI finne den
  • JNICALL - kombinert med JNIEXPORT, det sikrer at metodene våre er tilgjengelige for JNI-rammeverket
  • JNIEnv - en struktur som inneholder metoder som vi kan bruke vår opprinnelige kode for å få tilgang til Java-elementer
  • JavaVM - en struktur som lar oss manipulere en kjørende JVM (eller til og med starte en ny) og legge til tråder til den, ødelegge den osv ...

3. Hei verden JNI

Neste, la oss se på hvordan JNI fungerer i praksis.

I denne opplæringen bruker vi C ++ som morsmål og G ++ som kompilator og linker.

Vi kan bruke hvilken som helst annen kompilator som vi foretrekker, men hvordan installerer du G ++ på Ubuntu, Windows og MacOS:

  • Ubuntu Linux - kjør kommando “Sudo apt-get install build-essential” i en terminal
  • Windows - Installer MinGW
  • MacOS - kjør kommando “G ++” i en terminal, og hvis den ennå ikke er tilstede, vil den installere den.

3.1. Opprette Java-klassen

La oss begynne å lage vårt første JNI-program ved å implementere en klassisk "Hello World".

Til å begynne med oppretter vi følgende Java-klasse som inkluderer den opprinnelige metoden som skal utføre arbeidet:

pakke com.baeldung.jni; offentlig klasse HelloWorldJNI {statisk {System.loadLibrary ("innfødt"); } public static void main (String [] args) {new HelloWorldJNI (). sayHello (); } // Erklære en innfødt metode sayHello () som ikke mottar argumenter og returnerer ugyldige private native void sayHello (); }

Som vi kan se, vi laster det delte biblioteket i en statisk blokk. Dette sikrer at den vil være klar når vi trenger det og fra hvor vi trenger det.

Alternativt, i dette trivielle programmet, kan vi i stedet laste biblioteket rett før vi kaller vår opprinnelige metode fordi vi ikke bruker det opprinnelige biblioteket andre steder.

3.2. Implementering av en metode i C ++

Nå må vi lage implementeringen av vår opprinnelige metode i C ++.

Innen C ++ lagres definisjonen og implementeringen vanligvis i .h og .cpp filer henholdsvis.

Først, for å lage definisjonen av metoden, må vi bruke -h flagget til Java-kompilatoren:

javac -h. HelloWorldJNI.java

Dette vil generere en com_baeldung_jni_HelloWorldJNI.h fil med alle de innfødte metodene som er inkludert i klassen som er sendt som parameter, i dette tilfellet bare en:

JNIEXPORT ugyldig JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject); 

Som vi kan se, genereres funksjonsnavnet automatisk ved hjelp av den fullstendige pakken, klassen og metodenavnet.

Også noe interessant vi kan merke er at vi får to parametere overført til vår funksjon; en peker til strømmen JNIEnv; og også Java-objektet som metoden er knyttet til, forekomsten av vår HelloWorldJNI klasse.

Nå må vi lage et nytt .cpp fil for implementering av si hei funksjon. Det er her vi vil utføre handlinger som skriver ut “Hello World” til konsollen.

Vi vil gi navnet vårt .cpp fil med samme navn som .h-filen som inneholder overskriften, og legg til denne koden for å implementere den opprinnelige funksjonen:

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

3.3. Kompilering og lenking

På dette punktet har vi alle delene vi trenger på plass og har en forbindelse mellom dem.

Vi må bygge vårt delte bibliotek fra C ++ - koden og kjøre det!

For å gjøre det, må vi bruke G ++ kompilator, ikke glemme å inkludere JNI-overskriftene fra Java JDK-installasjonen vår.

Ubuntu-versjon:

g ++ -c -fPIC -I $ {JAVA_HOME} / inkluderer -I $ {JAVA_HOME} / inkluderer / linux com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Windows-versjon:

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

MacOS-versjon;

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 laget koden for plattformen vår i filen com_baeldung_jni_HelloWorldJNI.o, må vi inkludere det i et nytt delt bibliotek. Uansett hva vi bestemmer oss for å navngi, er argumentet overført til metoden System.loadLibrary.

Vi kalte vår ”native”, og vi laster den inn når du kjører Java-koden.

G ++ -linkeren kobler deretter C ++ -objektfilene til vårt brobygde bibliotek.

Ubuntu-versjon:

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

Windows-versjon:

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

MacOS-versjon:

g ++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc

Og det er det!

Vi kan nå kjøre programmet vårt fra kommandolinjen.

Derimot, vi må legge til hele banen til katalogen som inneholder biblioteket vi nettopp har generert. På denne måten vil Java vite hvor de skal se etter våre innfødte libs:

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

Konsollutgang:

Hei fra C ++ !!

4. Bruke avanserte JNI-funksjoner

Å si hei er hyggelig, men ikke veldig nyttig. Vanligvis vil vi utveksle data mellom Java og C ++ - kode og administrere disse dataene i programmet vårt.

4.1. Legge til parametere til våre innfødte metoder

Vi vil legge til noen parametere i våre opprinnelige metoder. La oss lage en ny klasse som heter EksempelParametereJNI med to innfødte metoder som bruker parametere og avkastning av forskjellige typer:

private innfødte lange sumIntegers (int første, int andre); privat innfødt String sayHelloToMe (strengnavn, boolsk erKvinne);

Gjenta deretter fremgangsmåten for å opprette en ny .h-fil med “javac -h” som vi gjorde før.

Lag nå den korresponderende .cpp-filen med implementeringen av den nye C ++ -metoden:

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

Vi har brukt pekeren * env av typen JNIEnv for å få tilgang til metodene som tilbys av JNI-miljøforekomsten.

JNIEnv tillater oss, i dette tilfellet, å passere Java Strenger inn i C ++ - koden og gå ut uten å bekymre deg for implementeringen.

Vi kan sjekke ekvivalensen av Java-typer og C JNI-typer i offisiell Oracle-dokumentasjon.

For å teste koden vår, må vi gjenta alle kompileringstrinnene i forrige Hei Verden eksempel.

4.2. Bruke objekter og ringe Java-metoder fra opprinnelig kode

I dette siste eksemplet skal vi se hvordan vi kan manipulere Java-objekter til vår opprinnelige C ++ - kode.

Vi begynner å lage en ny klasse Brukerdata som vi bruker til å lagre brukerinformasjon:

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

Deretter oppretter vi en annen Java-klasse som heter EksempelObjectsJNI med noen innfødte metoder som vi administrerer objekter av typen Brukerdata:

... offentlige innfødte UserData createUser (strengnavn, dobbel balanse); offentlig innfødt String printUserData (UserData-bruker); 

En gang til, la oss lage .h topptekst og deretter C ++ implementering av våre opprinnelige metoder på en ny .cpp fil:

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser (JNIEnv * env, jobject thisObject, jstring name, jdouble balance) {// Opprett objektet til klassen UserData jclass userDataClass = env-> FindClass ("com / userData /" jobject newUserData = env-> AllocObject (userDataClass); // Få UserData-feltene som skal settes 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) {// Finn ID-en til Java-metoden som skal kalles jclass userDataClass = env-> GetObjectClass (userData); jmethodID methodId = env-> GetMethodID (userDataClass, "getUserInfo", "() Ljava / lang / String;"); jstring result = (jstring) env-> CallObjectMethod (userData, methodId); returresultat; } 

Igjen bruker vi JNIEnv * env pekeren for å få tilgang til nødvendige klasser, objekter, felt og metoder fra den kjørende JVM.

Normalt trenger vi bare å oppgi det fulle klassenavnet for å få tilgang til en Java-klasse, eller riktig metodenavn og signatur for å få tilgang til en objektmetode.

Vi lager til og med en forekomst av klassen com.baeldung.jni.UserData i vår opprinnelige kode. Når vi har forekomsten, kan vi manipulere alle dens egenskaper og metoder på en måte som ligner på Java-refleksjon.

Vi kan sjekke alle andre metoder for JNIEnv inn i den offisielle Oracle-dokumentasjonen.

4. Ulemper ved å bruke JNI

JNI-bro har sine fallgruver.

Den viktigste ulempen er avhengigheten av den underliggende plattformen; vi mister egentlig "skriv en gang, løp hvor som helst" funksjon av Java. Dette betyr at vi må bygge en ny lib for hver nye kombinasjon av plattform og arkitektur vi vil støtte. Tenk deg hvilken innvirkning dette kan ha på byggeprosessen hvis vi støttet Windows, Linux, Android, MacOS ...

JNI legger ikke bare til et kompleksitetslag i programmet vårt. Det gir også et kostbart lag med kommunikasjon mellom koden som går inn i JVM og vår opprinnelige kode: vi må konvertere dataene som utveksles på begge måter mellom Java og C ++ i en marshaling / unmarshaling-prosess.

Noen ganger er det ikke engang en direkte konvertering mellom typene, så vi må skrive tilsvarende.

5. Konklusjon

Å kompilere koden for en bestemt plattform (vanligvis) gjør den raskere enn å kjøre bytecode.

Dette gjør det nyttig når vi trenger å øke hastigheten på en krevende prosess. Også når vi ikke har andre alternativer, for eksempel når vi trenger å bruke et bibliotek som administrerer en enhet.

Dette har imidlertid en pris da vi må opprettholde tilleggskode for hver forskjellige plattform vi støtter.

Derfor er det vanligvis en god ide å bruk bare JNI i tilfeller der det ikke er noe Java-alternativ.

Som alltid er koden for denne artikkelen tilgjengelig på GitHub.


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