Vodič za JNI (Java Native Interface)

1. Uvod

Kao što znamo, jedna od glavnih snaga Jave je njena prenosivost - što znači da kada jednom napišemo i kompajliramo kod, rezultat ovog postupka je bajt kod neovisan o platformi.

Jednostavno rečeno, ovo se može izvoditi na bilo kojem stroju ili uređaju koji mogu pokretati Java virtualni stroj i radit će neometano koliko smo mogli očekivati.

Međutim, ponekad zapravo trebamo koristiti kod koji je izvorno kompiliran za određenu arhitekturu.

Mogli bi postojati neki razlozi za potrebu upotrebe izvornog koda:

  • Potreba za rukovanjem nekim hardverom
  • Poboljšanje performansi za vrlo zahtjevan proces
  • Postojeća biblioteka koju želimo ponovno upotrijebiti umjesto da je prepišemo u Javi.

Da bi to postigao, JDK uvodi most između bajt-koda koji se izvodi u našem JVM-u i izvornog koda (obično napisano na C ili C ++).

Alat se naziva Java Native Interface. U ovom ćemo članku vidjeti kako je napisati neki kôd s njim.

2. Kako to djeluje

2.1. Izvorne metode: JVM ispunjava kompilirani kod

Java nudi domorodac ključna riječ koja se koristi da naznači da će implementaciju metode osigurati izvorni kôd.

Uobičajeno, kada izrađujemo izvorni izvršni program, možemo odabrati upotrebu statičkih ili zajedničkih datoteka:

  • Statički libs - svi binarni programi knjižnice bit će uključeni kao dio naše izvršne datoteke tijekom postupka povezivanja. Stoga nam više neće trebati libs, ali povećat će veličinu naše izvršne datoteke.
  • Dijeljeni udovi - konačni izvršni program sadrži samo reference na uši, a ne i sam kôd. Potrebno je da okruženje u kojem pokrećemo našu izvršnu datoteku ima pristup svim datotekama datoteka koje koristi naš program.

Ovo potonje je ono što za JNI ima smisla jer ne možemo miješati bajt kod i izvorno prevedeni kôd u istu binarnu datoteku.

Stoga će naš zajednički lib zadržati izvorni kôd odvojeno unutar svog .so / .dll / .dylib datoteku (ovisno o tome koji operativni sustav koristimo) umjesto da bude dio naše klase.

The domorodac ključna riječ transformira našu metodu u neku vrstu apstraktne metode:

privatna izvorna void aNativeMethod ();

S glavnom razlikom što umjesto da ga implementira druga Java klasa, bit će implementiran u odvojenu izvornu zajedničku knjižnicu.

Izradit će se tablica s pokazivačima u memoriji na implementaciju svih naših izvornih metoda kako bi ih se moglo pozvati iz našeg Java koda.

2.2. Potrebne komponente

Evo kratkog opisa ključnih komponenata koje moramo uzeti u obzir. Objasnit ćemo ih dalje u ovom članku

  • Java Code - naše satove. Sadržat će barem jedan domorodac metoda.
  • Izvorni kôd - stvarna logika naših izvornih metoda, obično kodiranih u C ili C ++.
  • JNI datoteka zaglavlja - ova datoteka zaglavlja za C / C ++ (uključuju / jni.h u JDK direktorij) uključuje sve definicije JNI elemenata koje možemo koristiti u našim izvornim programima.
  • C / C ++ Compiler - možemo birati između GCC-a, Clang-a, Visual Studija ili bilo kojeg drugog koji nam se sviđa, ukoliko je u mogućnosti generirati izvornu zajedničku knjižnicu za našu platformu.

2.3. JNI elementi u kodu (Java i C / C ++)

Java elementi:

  • "Native" ključna riječ - kao što smo već obradili, bilo koja metoda označena kao izvorna mora biti implementirana u nativni zajednički lib.
  • System.loadLibrary (niz libname) - statička metoda koja zajedničku knjižnicu učitava iz datotečnog sustava u memoriju i čini njezine izvezene funkcije dostupnima za naš Java kôd.

C / C ++ elementi (mnogi od njih definirani u jni.h)

  • JNIEXPORT - označava funkciju u dijeljenoj lib kao izvoznu, tako da će biti uključena u tablicu funkcija, pa je tako JNI može pronaći
  • JNICALL - u kombinaciji s JNIEXPORT, osigurava dostupnost naših metoda za JNI okvir
  • JNIEnv - struktura koja sadrži metode pomoću kojih možemo koristiti svoj izvorni kod za pristup Java elementima
  • JavaVM - struktura koja nam omogućuje manipuliranje pokrenutim JVM-om (ili čak pokretanje novog) dodavanjem niti u njega, uništavanjem itd. ...

3. Pozdrav svijete JNI

Sljedeći, pogledajmo kako JNI djeluje u praksi.

U ovom uputstvu koristit ćemo C ++ kao materinji jezik, a G ++ kao prevoditelj i povezivač.

Možemo koristiti bilo koji drugi kompajler koji želimo, ali evo kako instalirati G ++ na Ubuntu, Windows i MacOS:

  • Ubuntu Linux - naredba za pokretanje "Sudo apt-get install build-bitno" u terminalu
  • Windows - Instalirajte MinGW
  • MacOS - naredba za pokretanje "G ++" u terminalu i ako još nije prisutan, instalirat će ga.

3.1. Izrada Java klase

Počnimo stvarati svoj prvi JNI program primjenom klasičnog "Hello World".

Za početak kreiramo sljedeću Java klasu koja uključuje izvornu metodu koja će izvesti posao:

paket com.baeldung.jni; javna klasa HelloWorldJNI {static {System.loadLibrary ("native"); } public static void main (String [] args) {new HelloWorldJNI (). sayHello (); } // Deklariraj nativnu metodu sayHello () koja ne prima argumente i vraća void privatnu nativnu void sayHello (); }

Kao što vidimo, učitavamo zajedničku knjižnicu u statički blok. To osigurava da će biti spremno kad nam zatreba i odakle god nam treba.

Alternativno, u ovom trivijalnom programu mogli bismo umjesto toga učitati knjižnicu neposredno prije pozivanja naše izvorne metode jer nigdje drugdje ne koristimo matičnu knjižnicu.

3.2. Implementacija metode u C ++

Sada moramo stvoriti implementaciju naše izvorne metode u C ++.

Unutar C ++ definicija i implementacija obično se pohranjuju u .h i .cpp datoteke.

Prvi, da bismo stvorili definiciju metode, moramo koristiti -h zastavica Java kompajlera:

javac -h. HelloWorldJNI.java

Ovo će generirati com_baeldung_jni_HelloWorldJNI.h datoteka sa svim izvornim metodama uključenim u klasu koja se prosljeđuje kao parametar, u ovom slučaju, samo jedna:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject); 

Kao što vidimo, ime funkcije automatski se generira pomoću potpuno kvalificiranog naziva paketa, klase i metode.

Također, nešto zanimljivo što možemo primijetiti jest da u našu funkciju prosljeđujemo dva parametra; pokazivač na struju JNIEnv; a također i objekt Java na koji je metoda pridružena, instanca našeg HelloWorldJNI razred.

Sada moramo stvoriti novi .cpp datoteka za provedbu reci zdravo funkcija. Ovdje ćemo izvršiti radnje koje ispisuju "Hello World" na konzolu.

Nazvat ćemo naše .cpp datoteku s istim imenom kao .h koja sadrži zaglavlje i dodajte ovaj kôd za implementaciju izvorne funkcije:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv * env, jobject thisObject) {std :: cout << "Pozdrav iz C ++ !!" << std :: endl; } 

3.3. Sastavljanje i povezivanje

U ovom trenutku imamo sve potrebne dijelove i imamo vezu između njih.

Moramo izgraditi našu zajedničku biblioteku od C ++ koda i pokrenuti je!

Da bismo to učinili, moramo koristiti G ++ kompajler, ne zaboravljajući uključiti JNI zaglavlja iz naše Java JDK instalacije.

Ubuntu verzija:

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

Verzija sustava Windows:

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

Verzija MacOS-a;

g ++ -c -fPIC -I $ {JAVA_HOME} / uključuju -I $ {JAVA_HOME} / uključuju / darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Jednom kad u datoteku spremimo kod za našu platformu com_baeldung_jni_HelloWorldJNI.o, moramo ga uključiti u novu zajedničku knjižnicu. Što god odlučili imenovati, to je argument koji se prenosi u metodu System.loadLibrary.

Svoje smo nazvali "izvorni" i učitat ćemo ih prilikom pokretanja našeg Java koda.

Povezivač G ++ zatim povezuje datoteke objekta C ++ u našu premošćenu knjižnicu.

Ubuntu verzija:

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

Verzija sustava Windows:

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

Verzija MacOS-a:

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

I to je to!

Sada možemo pokrenuti naš program iz naredbenog retka.

Međutim, moramo dodati puni put u direktorij koji sadrži biblioteku koju smo upravo generirali. Na ovaj će način Java znati gdje treba potražiti naše izvorne libove:

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

Izlaz konzole:

Pozdrav sa C ++ !!

4. Korištenje naprednih značajki JNI

Pozdravi je lijepo, ali ne baš korisno. Obično bismo željeli razmjenjivati ​​podatke između Java i C ++ koda i upravljati tim podacima u našem programu.

4.1. Dodavanje parametara našim izvornim metodama

Dodati ćemo neke parametre našim izvornim metodama. Stvorimo novu klasu pod nazivom ExampleParametersJNI s dvije nativne metode pomoću parametara i povrata različitih vrsta:

private native long sumIntegers (int prvo, int drugo); private native String sayHelloToMe (Ime niza, logička vrijednost jeFemale);

A zatim ponovite postupak za stvaranje nove .h datoteke s “javac -h” kao što smo to radili prije.

Sada stvorite odgovarajuću .cpp datoteku s implementacijom nove metode C ++:

... JNIEXPORT jlong ​​JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers (JNIEnv * env, jobject thisObject, jint first, jint second) {std :: cout << "C ++: Primljeni brojevi su:" << prvi << "i" << drugi NewStringUTF (puno ime.c_str ()); } ...

Koristili smo pokazivač * env tipa JNIEnv za pristup metodama koje pruža instanca okoline JNI.

JNIEnv omogućuje nam, u ovom slučaju, da prođemo Javu Žice u naš C ++ kôd i vratite se bez brige o implementaciji.

Možemo provjeriti ekvivalentnost tipova Java i C JNI tipova u službenoj dokumentaciji tvrtke Oracle.

Da bismo testirali naš kôd, moramo ponoviti sve korake kompilacije iz prethodnog Pozdrav svijete primjer.

4.2. Korištenje objekata i pozivanje Java metoda iz izvornog koda

U ovom posljednjem primjeru vidjet ćemo kako možemo manipulirati Java objektima u svoj izvorni C ++ kôd.

Počet ćemo s izradom nove klase Korisnički podaci koje ćemo koristiti za pohranu nekih korisničkih podataka:

paket com.baeldung.jni; javna klasa UserData {naziv javnog niza; dvostruka javna bilanca; javni String getUserInfo () {return "[ime] =" + ime + ", [saldo] =" + stanje; }}

Zatim ćemo stvoriti još jednu Java klasu pod nazivom ExampleObjectsJNI s nekim izvornim metodama pomoću kojih ćemo upravljati objektima tipa Korisnički podaci:

... javni izvorni UserData createUser (naziv niza, dvostruko stanje); javni izvorni String printUserData (korisnik UserData); 

Još jednom, stvorimo .h zaglavlje, a zatim C ++ implementacija naših izvornih metoda na novom .cpp datoteka:

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser (JNIEnv * env, jobject thisObject, jstring name, jdouble saldo) {// Stvaranje objekta klase UserData jclass userDataClass = env-> FindClass ("com / baceldS (Datoteka / bata / Korisnik / bata / ba / Korisničko ime / ba / Korisničko ime (jta / bata / Korisničko ime / Korisničko ime jobject newUserData = env-> AllocObject (userDataClass); // Nabavite da se postave polja UserData jfieldID nameField = env-> GetFieldID (userDataClass, "name", "Ljava / lang / String;"); jfieldID balanceField = env-> GetFieldID (userDataClass, "saldo", "D"); env-> SetObjectField (newUserData, nameField, name); env-> SetDoubleField (newUserData, balanceField, balance); vrati newUserData; } JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData (JNIEnv * env, jobject thisObject, jobject userData) {// Pronađi id Java metode koja će se zvati jclass userDataClass = env-> GetObjectataclass; jmethodID methodId = env-> GetMethodID (userDataClass, "getUserInfo", "() Ljava / lang / String;"); jstring rezultat = (jstring) env-> CallObjectMethod (userData, methodId); povratni rezultat; } 

Opet koristimo JNIEnv * env pokazivač za pristup potrebnim klasama, objektima, poljima i metodama iz pokrenutog JVM-a.

Obično samo trebamo navesti puni naziv klase za pristup Java klasi ili ispravno ime metode i potpis za pristup objektnoj metodi.

Čak stvaramo primjerak klase com.baeldung.jni.UserData u našem izvornom kodu. Jednom kad imamo instancu, možemo manipulirati svim njezinim svojstvima i metodama na način sličan Java refleksiji.

Možemo provjeriti sve ostale metode JNIEnv u Oracleovu službenu dokumentaciju.

4. Nedostaci korištenja JNI

JNI premošćivanje ima svoje zamke.

Glavni nedostatak je ovisnost o osnovnoj platformi; u osnovi gubimo "napiši jednom, trči bilo gdje" značajka Jave. To znači da ćemo morati izraditi novi lib za svaku novu kombinaciju platforme i arhitekture koju želimo podržati. Zamislite kakav bi ovo utjecaj mogao imati na postupak izrade ako podržavamo Windows, Linux, Android, MacOS ...

JNI ne samo da dodaje složenost našem programu. Također dodaje skupi sloj komunikacije između koda koji se izvodi u JVM i našeg izvornog koda: moramo pretvoriti podatke razmijenjene na oba načina između Jave i C ++ u procesu marširanja / nemarširanja.

Ponekad čak nema niti izravne konverzije između tipova pa ćemo morati napisati svoj ekvivalent.

5. Zaključak

Sastavljanje koda za određenu platformu (obično) čini ga bržim od pokretanja bajt-koda.

To ga čini korisnim kada moramo ubrzati zahtjevan postupak. Također, kada nemamo druge alternative, na primjer kada trebamo koristiti knjižnicu koja upravlja uređajem.

Međutim, to ima svoju cijenu jer ćemo morati održavati dodatni kôd za svaku različitu platformu koju podržavamo.

Zato je obično dobra ideja koristite JNI samo u slučajevima kada ne postoji Java alternativa.

Kao i uvijek kôd za ovaj članak dostupan je na GitHub-u.