Sprievodca po JNI (natívne rozhranie Java)

1. Úvod

Ako vieme, jednou z hlavných silných stránok Javy je jej prenosnosť - to znamená, že akonáhle napíšeme a skompilujeme kód, výsledkom tohto procesu je bajtkód nezávislý na platforme.

Jednoducho povedané, toto môže bežať na akomkoľvek stroji alebo zariadení schopnom spustiť Java Virtual Machine a bude fungovať tak bezproblémovo, ako by sme čakali.

Avšak niekedy skutočne potrebujeme použiť kód, ktorý je natívne kompilovaný pre konkrétnu architektúru.

Môžu existovať dôvody, prečo je potrebné použiť natívny kód:

  • Potreba zvládnuť nejaký hardvér
  • Zlepšenie výkonu pre veľmi náročný proces
  • Existujúca knižnica, ktorú chceme znova použiť namiesto prepisovania v jazyku Java.

Aby sme to dosiahli, zavádza JDK most medzi bajtkódom bežiacim v našom JVM a natívnym kódom (zvyčajne písané v jazyku C alebo C ++).

Tento nástroj sa nazýva Java Native Interface. V tomto článku uvidíme, ako to bude s ním napísať nejaký kód.

2. Ako to funguje

2.1. Natívne metódy: JVM spĺňa kompilovaný kód

Java poskytuje domorodec kľúčové slovo, ktoré sa používa na označenie toho, že implementáciu metódy zabezpečí natívny kód.

Normálne pri vytváraní natívneho spustiteľného programu môžeme zvoliť použitie statických alebo zdieľaných libs:

  • Statické knižnice - všetky binárne súbory z knižnice budú počas procesu prepájania súčasťou nášho spustiteľného súboru. Preto už nebudeme potrebovať libs, ale zväčší sa to veľkosť nášho spustiteľného súboru.
  • Shared libs - konečný spustiteľný súbor má iba odkazy na libs, nie na samotný kód. Vyžaduje to, aby prostredie, v ktorom spúšťame náš spustiteľný súbor, malo prístup ku všetkým súborom knižníc lib, ktoré používa náš program.

To druhé dáva zmysel pre JNI, pretože nemôžeme kombinovať bytecode a natívne kompilovaný kód do toho istého binárneho súboru.

Preto naše zdieľané lib bude natívny kód uchovávať oddelene v rámci neho .so / .dll / .dylib namiesto toho, aby sme boli súčasťou našich tried, v závislosti od toho, aký operačný systém používame.

The domorodec kľúčové slovo transformuje našu metódu na akúsi abstraktnú metódu:

private native void aNativeMethod ();

S hlavným rozdielom, že namiesto toho, aby bola implementovaná inou triedou Java, bude implementovaná v oddelenej natívnej zdieľanej knižnici.

Bude zostavená tabuľka s ukazovateľmi v pamäti na implementáciu všetkých našich natívnych metód, aby ich bolo možné volať z nášho kódu Java.

2.2. Potrebné súčasti

Tu je stručný popis kľúčových komponentov, ktoré musíme brať do úvahy. Ďalej ich vysvetlíme ďalej v tomto článku

  • Java Code - naše triedy. Budú obsahovať najmenej jednu domorodec metóda.
  • Natívny kód - skutočná logika našich natívnych metód, zvyčajne kódovaná v jazyku C alebo C ++.
  • Hlavičkový súbor JNI - tento hlavičkový súbor pre C / C ++ (zahrnúť / jni.h do adresára JDK) obsahuje všetky definície prvkov JNI, ktoré môžeme použiť v našich natívnych programoch.
  • Kompilátor C / C ++ - môžeme si vybrať medzi GCC, Clang, Visual Studio alebo akýmkoľvek iným, ktorý sa nám páči, pokiaľ je schopný vygenerovať natívnu zdieľanú knižnicu pre našu platformu.

2.3. Prvky JNI v kóde (Java a C / C ++)

Prvky Java:

  • „Natívne“ kľúčové slovo - ako sme už uviedli, každá metóda označená ako natívna musí byť implementovaná v natívnom, zdieľanom lib.
  • System.loadLibrary (reťazec libname) - statická metóda, ktorá načíta zdieľanú knižnicu zo súborového systému do pamäte a sprístupní jej exportované funkcie pre náš kód Java.

Prvky C / C ++ (veľa z nich je definovaných v rámci jni.h)

  • JNIEXPORT - označí funkciu do zdieľaného lib ako exportovateľnú, takže bude zahrnutá do tabuľky funkcií, a tak ju JNI môže nájsť
  • JNICALL - v kombinácii s JNIEXPORT, zaisťuje dostupnosť našich metód pre rámec JNI
  • JNIEnv - štruktúra obsahujúca metódy, pomocou ktorých môžeme použiť náš natívny kód na prístup k prvkom Java
  • JavaVM - štruktúra, ktorá nám umožňuje manipulovať s bežiacim JVM (alebo dokonca začať nový), pridávať do neho vlákna, ničiť ich atď ...

3. Ahoj Svet JNI

Ďalšie, poďme sa pozrieť na to, ako JNI funguje v praxi.

V tomto tutoriále budeme používať C ++ ako materinský jazyk a G ++ ako kompilátor a linker.

Môžeme použiť akýkoľvek iný kompilátor podľa našich preferencií, ale tu je postup, ako nainštalovať G ++ na Ubuntu, Windows a MacOS:

  • Ubuntu Linux - príkaz spustiť „Sudo apt-get install build-essential“ v termináli
  • Windows - Nainštalujte MinGW
  • MacOS - príkaz spustiť „G ++“ v termináli a ak ešte nie je k dispozícii, nainštaluje si ho.

3.1. Vytváranie triedy Java

Začnime vytvárať náš prvý program JNI implementáciou klasického „Hello World“.

Na začiatok vytvoríme nasledujúcu triedu Java, ktorá obsahuje natívnu metódu, ktorá bude prácu vykonávať:

balíček com.baeldung.jni; verejná trieda HelloWorldJNI {static {System.loadLibrary ("native"); } public static void main (String [] args) {new HelloWorldJNI (). sayHello (); } // Deklaruje natívnu metódu sayHello (), ktorá neprijíma žiadne argumenty a vracia void private native void sayHello (); }

Ako vidíme, načítame zdieľanú knižnicu do statického bloku. To zaisťuje, že bude pripravený, kedykoľvek to budeme potrebovať a odkiaľkoľvek to budeme potrebovať.

Alternatívne by sme v tomto triviálnom programe mohli namiesto toho načítať knižnicu tesne pred zavolaním našej natívnej metódy, pretože natívnu knižnicu nepoužívame nikde inde.

3.2. Implementácia metódy v C ++

Teraz musíme vytvoriť implementáciu našej natívnej metódy v C ++.

V C ++ sú definícia a implementácia zvyčajne uložené v .h a .cpp súbory resp.

Najprv, na vytvorenie definície metódy musíme použiť -h vlajka kompilátora Java:

javac -h. HelloWorldJNI.java

Toto vygeneruje a com_baeldung_jni_HelloWorldJNI.h súbor so všetkými natívnymi metódami zahrnutými v triede odovzdaný ako parameter, v tomto prípade iba jeden:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject); 

Ako vidíme, názov funkcie sa automaticky generuje pomocou plne kvalifikovaného názvu balíka, triedy a metódy.

Tiež si môžeme všimnúť niečo zaujímavé, že do našej funkcie dostávame dva parametre; ukazovateľ na prúd JNIEnv; a tiež objekt Java, ku ktorému je metóda pripojená, inštancia nášho HelloWorldJNI trieda.

Teraz musíme vytvoriť nový .cpp spis na implementáciu povedz ahoj funkcie. Toto je miesto, kde vykonáme akcie, ktoré do konzoly vytlačia text „Hello World“.

My si dáme meno .cpp súbor s rovnakým názvom ako ten, ktorý obsahuje hlavičku a pridajte tento kód na implementáciu natívnej funkcie:

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

3.3. Zostavovanie a prepojenie

V tomto okamihu máme všetky časti, ktoré potrebujeme, pripravené a máme medzi nimi spojenie.

Musíme vytvoriť našu zdieľanú knižnicu z kódu C ++ a spustiť ju!

Aby sme to mohli urobiť, musíme použiť kompilátor G ++, nezabudnite zahrnúť hlavičky JNI z našej inštalácie Java JDK.

Verzia Ubuntu:

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

Verzia pre Windows:

g ++ -c -I% JAVA_HOME% \ zahrnúť -I% JAVA_HOME% \ zahrnúť \ win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Verzia pre MacOS;

g ++ -c -fPIC -I $ {JAVA_HOME} / zahrnúť -I $ {JAVA_HOME} / zahrnúť / darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Keď máme kód pre našu platformu skompilovaný do súboru com_baeldung_jni_HelloWorldJNI.o, musíme ju zahrnúť do novej zdieľanej knižnice. Nech sa už rozhodneme pomenovať čokoľvek, argument odovzdaný do metódy System.loadLibrary.

Náš sme pomenovali „natívny“ a načítame ho pri spustení nášho Java kódu.

Linker G ++ potom prepojí súbory objektov C ++ do našej premostenej knižnice.

Verzia Ubuntu:

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

Verzia pre Windows:

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

Verzia pre MacOS:

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

A je to!

Náš program teraz môžeme spustiť z príkazového riadku.

Avšak musíme pridať celú cestu k adresáru obsahujúcemu knižnicu, ktorú sme práve vygenerovali. Takto bude Java vedieť, kde hľadať naše natívne knižnice libs:

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

Výstup konzoly:

Ahojte z C ++ !!

4. Používanie pokročilých funkcií JNI

Pozdrav je pekný, ale nie veľmi užitočný. Zvyčajne by sme si chceli vymeniť údaje medzi kódom Java a C ++ a tieto údaje spravovať v našom programe.

4.1. Pridanie parametrov k našim natívnym metódam

K našim natívnym metódam pridáme niektoré parametre. Vytvorme novú triedu s názvom ExampleParametersJNI s dvoma natívnymi metódami používajúcimi parametre a návratnosť rôznych typov:

private native long sumIntegers (int first, int second); súkromný natívny reťazec sayHelloToMe (názov reťazca, boolean je žena);

A potom opakujte postup na vytvorenie nového súboru .h s „javac -h“, ako sme to urobili predtým.

Teraz vytvorte zodpovedajúci súbor .cpp s implementáciou novej metódy C ++:

... JNIEXPORT jlong ​​JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers (JNIEnv * env, jobject thisObject, jint prvý, jint druhý) {std :: cout << "C ++: Prijaté čísla sú:" << prvý << "a" << druhý " NewStringUTF (fullName.c_str ()); } ...

Použili sme ukazovateľ * env typu JNIEnv na prístup k metódam poskytovaným inštanciou prostredia JNI.

JNIEnv umožňuje v tomto prípade prechádzať Javu Struny do nášho kódu C ++ a bez obáv z implementácie vycúvajte.

Môžeme skontrolovať ekvivalenciu typov Java a typov C JNI do oficiálnej dokumentácie Oracle.

Na otestovanie nášho kódu musíme zopakovať všetky kroky kompilácie predchádzajúceho HelloWorld príklad.

4.2. Používanie objektov a volanie metód Java z natívneho kódu

V tomto poslednom príklade sa pozrieme na to, ako môžeme manipulovať s objektmi Java do nášho natívneho kódu C ++.

Začneme vytvárať novú triedu Použivateľské dáta ktoré použijeme na uloženie niektorých informácií o používateľovi:

balíček com.baeldung.jni; public class UserData {public String name; verejná dvojitá bilancia; public String getUserInfo () {return "[name] =" + meno + ", [zostatok] =" + zostatok; }}

Potom vytvoríme ďalšiu triedu Java s názvom ExampleObjectsJNI s niektorými natívnymi metódami, pomocou ktorých budeme spravovať objekty typu Použivateľské dáta:

... verejné natívne UserData createUser (názov reťazca, dvojitá rovnováha); verejný natívny reťazec printUserData (užívateľ UserData); 

Ešte raz vytvorme .h hlavičku a potom implementáciu našich natívnych metód v C ++ na novú .cpp spis:

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser (JNIEnv * env, jobject thisObject, jstring name, jdouble balance) {// Vytvorte objekt triedy UserData jclass userDataClass = env-> FindClass ("jv") "" jobject newUserData = env-> AllocObject (userDataClass); // Získajte nastavenia polí UserData 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); vrátiť newUserData; } JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData (JNIEnv * env, jobject thisObject, jobject userData) {// Nájdite ID metódy Java, ktorá sa bude volať jclass userDataClass = env-> GetObjectClass) jmethodID methodId = env-> GetMethodID (userDataClass, "getUserInfo", "() Ljava / lang / String;"); jstring result = (jstring) env-> CallObjectMethod (userData, methodId); návratový výsledok; } 

Opäť používame JNIEnv * env ukazovateľ na prístup k potrebným triedam, objektom, poliam a metódam zo spusteného JVM.

Za normálnych okolností stačí zadať úplný názov triedy pre prístup k triede Java alebo správny názov metódy a podpis pre prístup k metóde objektu.

Dokonca vytvárame inštanciu triedy com.baeldung.jni.UserData v našom natívnom kóde. Keď už máme inštanciu, môžeme manipulovať so všetkými jej vlastnosťami a metódami podobne ako v Java reflexii.

Môžeme skontrolovať všetky ostatné metódy JNIEnv do oficiálnej dokumentácie Oracle.

4. Nevýhody používania JNI

Premostenie JNI má svoje úskalia.

Hlavnou nevýhodou je závislosť od základnej platformy; v podstate stratíme „napíš raz, utekaj kamkoľvek“ vlastnosť Java. To znamená, že budeme musieť vytvoriť nové lib pre každú novú kombináciu platformy a architektúry, ktorú chceme podporovať. Predstavte si, aký to môže mať dopad na proces vytvárania, keby sme podporovali Windows, Linux, Android, MacOS ...

JNI nášmu programu dodáva nielen vrstvu zložitosti. Pridáva tiež nákladnú vrstvu komunikácie medzi kódom bežiacim do JVM a našim natívnym kódom: musíme prevádzať údaje vymieňané oboma spôsobmi medzi jazykmi Java a C ++ v procese zaraďovania / zaraďovania.

Niekedy dokonca neexistuje priama konverzia medzi typmi, takže budeme musieť napísať náš ekvivalent.

5. Záver

Kompilácia kódu pre konkrétnu platformu (zvyčajne) ho robí rýchlejším ako spustenie bytecode.

To je užitočné, keď potrebujeme urýchliť náročný proces. Tiež, keď nemáme iné alternatívy, napríklad keď potrebujeme použiť knižnicu, ktorá spravuje zariadenie.

Toto však má svoju cenu, pretože budeme musieť udržiavať ďalší kód pre každú inú platformu, ktorú podporujeme.

Preto je zvyčajne dobrý nápad JNI používajte iba v prípadoch, keď neexistuje alternatíva Java.

Kód tohto článku je ako vždy k dispozícii na GitHub.


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