Gids voor JNI (Java Native Interface)

1. Inleiding

Zoals we weten, is een van de belangrijkste sterke punten van Java de draagbaarheid - wat betekent dat zodra we code schrijven en compileren, het resultaat van dit proces platformonafhankelijke bytecode is.

Simpel gezegd, dit kan worden uitgevoerd op elke machine of elk apparaat dat een Java Virtual Machine kan uitvoeren, en het werkt zo naadloos als we konden verwachten.

Soms echter we hebben eigenlijk code nodig die native is gecompileerd voor een specifieke architectuur.

Er kunnen een aantal redenen zijn waarom u native code moet gebruiken:

  • De noodzaak om met wat hardware om te gaan
  • Prestatieverbetering voor een zeer veeleisend proces
  • Een bestaande bibliotheek die we willen hergebruiken in plaats van deze in Java te herschrijven.

Om dit te bereiken, introduceert de JDK een brug tussen de bytecode die in onze JVM draait en de native code (meestal geschreven in C of C ++).

De tool heet Java Native Interface. In dit artikel zullen we zien hoe het is om er wat code mee te schrijven.

2. Hoe het werkt

2.1. Native methoden: de JVM voldoet aan de gecompileerde code

Java biedt het native trefwoord dat wordt gebruikt om aan te geven dat de implementatie van de methode wordt geleverd door een native code.

Normaal gesproken kunnen we bij het maken van een native uitvoerbaar programma ervoor kiezen om statische of gedeelde libs te gebruiken:

  • Statische bibliotheken - alle binaire bestanden van de bibliotheek worden tijdens het koppelingsproces opgenomen als onderdeel van ons uitvoerbare bestand. We hebben de libs dus niet meer nodig, maar het zal de grootte van ons uitvoerbare bestand vergroten.
  • Gedeelde libs - het uiteindelijke uitvoerbare bestand heeft alleen verwijzingen naar de libs, niet de code zelf. Het vereist dat de omgeving waarin we ons uitvoerbare bestand draaien toegang heeft tot alle bestanden van de libs die door ons programma worden gebruikt.

Dit laatste is wat logisch is voor JNI, omdat we bytecode en native gecompileerde code niet kunnen combineren in hetzelfde binaire bestand.

Daarom zal onze gedeelde bibliotheek de native code afzonderlijk binnen zijn .so / .dll / .dylib bestand (afhankelijk van welk besturingssysteem we gebruiken) in plaats van deel uit te maken van onze lessen.

De native trefwoord transformeert onze methode in een soort abstracte methode:

private native void aNativeMethod ();

Met als belangrijkste verschil dat in plaats van geïmplementeerd te worden door een andere Java-klasse, wordt het geïmplementeerd in een aparte native gedeelde bibliotheek.

Er zal een tabel worden gemaakt met verwijzingen in het geheugen naar de implementatie van al onze native methoden, zodat ze kunnen worden aangeroepen vanuit onze Java-code.

2.2. Componenten die nodig zijn

Hier is een korte beschrijving van de belangrijkste componenten waarmee we rekening moeten houden. We leggen ze later in dit artikel verder uit

  • Java-code - onze klassen. Ze zullen er minstens één bevatten native methode.
  • Native Code - de feitelijke logica van onze native methoden, meestal gecodeerd in C of C ++.
  • JNI header-bestand - dit header-bestand voor C / C ++ (include / jni.h in de JDK-directory) bevat alle definities van JNI-elementen die we kunnen gebruiken in onze oorspronkelijke programma's.
  • C / C ++ Compiler - we kunnen kiezen tussen GCC, Clang, Visual Studio of elke andere die we leuk vinden, voor zover het in staat is om een ​​native gedeelde bibliotheek voor ons platform te genereren.

2.3. JNI-elementen in code (Java en C / C ++)

Java-elementen:

  • "Native" trefwoord - zoals we al hebben besproken, moet elke methode die als native is gemarkeerd, worden geïmplementeerd in een native, gedeelde bibliotheek.
  • System.loadLibrary (String libname) - een statische methode die een gedeelde bibliotheek van het bestandssysteem in het geheugen laadt en de geëxporteerde functies beschikbaar stelt voor onze Java-code.

C / C ++ - elementen (veel ervan zijn gedefinieerd in jni.h)

  • JNIEXPORT - markeert de functie in de gedeelde bibliotheek als exporteerbaar, zodat deze wordt opgenomen in de functietabel, zodat JNI deze kan vinden
  • JNICALL - gecombineerd met JNIEXPORT, zorgt het ervoor dat onze methoden beschikbaar zijn voor het JNI-framework
  • JNIEnv - een structuur met methoden waarmee we onze native code kunnen gebruiken om toegang te krijgen tot Java-elementen
  • JavaVM - een structuur waarmee we een actieve JVM kunnen manipuleren (of zelfs een nieuwe kunnen starten) door er threads aan toe te voegen, deze te vernietigen, enz ...

3. Hallo wereld JNI

De volgende, laten we eens kijken hoe JNI in de praktijk werkt.

In deze tutorial gebruiken we C ++ als moedertaal en G ++ als compiler en linker.

We kunnen elke andere compiler van onze voorkeur gebruiken, maar hier is hoe je G ++ op Ubuntu, Windows en MacOS installeert:

  • Ubuntu Linux - voer het commando uit "Sudo apt-get install build-essential" in een terminal
  • Windows - Installeer MinGW
  • MacOS - voer het commando uit "G ++" in een terminal en als het nog niet aanwezig is, zal het het installeren.

3.1. De Java-klasse maken

Laten we beginnen met het maken van ons eerste JNI-programma door een klassieke "Hello World" te implementeren.

Om te beginnen maken we de volgende Java-klasse met de native methode die het werk zal uitvoeren:

pakket com.baeldung.jni; openbare klasse HelloWorldJNI {statische {System.loadLibrary ("native"); } public static void main (String [] args) {new HelloWorldJNI (). sayHello (); } // Declareer een native methode sayHello () die geen argumenten ontvangt en void private native void sayHello () retourneert; }

Zoals we kunnen zien, we laden de gedeelde bibliotheek in een statisch blok. Dit zorgt ervoor dat het klaar is wanneer we het nodig hebben en waar we het ook nodig hebben.

Als alternatief kunnen we in dit triviale programma in plaats daarvan de bibliotheek laden net voordat we onze native methode aanroepen, omdat we de native bibliotheek nergens anders gebruiken.

3.2. Een methode implementeren in C ++

Nu moeten we de implementatie van onze native methode in C ++ maken.

Binnen C ++ worden de definitie en de implementatie meestal opgeslagen in .h en .cpp bestanden.

Eerste, om de definitie van de methode te creëren, moeten we de -h vlag van de Java-compiler:

javac -h. HelloWorldJNI.java

Dit genereert een com_baeldung_jni_HelloWorldJNI.h bestand met alle native methoden in de klasse die als parameter zijn doorgegeven, in dit geval slechts één:

JNIEXPORT ongeldig JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject); 

Zoals we kunnen zien, wordt de functienaam automatisch gegenereerd met behulp van de volledig gekwalificeerde pakket-, klasse- en methode-naam.

Wat ook interessant is, is dat we twee parameters aan onze functie krijgen doorgegeven; een verwijzing naar de huidige JNIEnv; en ook het Java-object waaraan de methode is gekoppeld, de instantie van onze HalloWereldJNI klasse.

Nu moeten we een nieuwe maken .cpp bestand voor de implementatie van het zeg hallo functie. Dit is waar we acties uitvoeren die "Hallo wereld" naar de console afdrukken.

We noemen onze .cpp bestand met dezelfde naam als het .h-bestand met de header en voeg deze code toe om de native functie te implementeren:

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

3.3. Compileren en koppelen

Op dit punt hebben we alle onderdelen die we nodig hebben op hun plaats en hebben we een verbinding ertussen.

We moeten onze gedeelde bibliotheek bouwen vanuit de C ++ -code en deze uitvoeren!

Om dit te doen, moeten we de G ++ -compiler gebruiken, niet te vergeten de JNI-headers van onze Java JDK-installatie op te nemen.

Ubuntu-versie:

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

Windows-versie:

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

MacOS-versie;

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

Zodra we de code voor ons platform in het bestand hebben gecompileerd com_baeldung_jni_HelloWorldJNI.o, we moeten het opnemen in een nieuwe gedeelde bibliotheek. Wat we ook besluiten het te noemen, het is het argument dat in de methode wordt doorgegeven System.loadLibrary.

We hebben de onze "native" genoemd en we zullen deze laden als we onze Java-code uitvoeren.

De G ++ -linker koppelt vervolgens de C ++ -objectbestanden aan onze overbrugde bibliotheek.

Ubuntu-versie:

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

Windows-versie:

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

MacOS-versie:

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

En dat is het!

We kunnen ons programma nu vanaf de opdrachtregel uitvoeren.

Echter, we moeten het volledige pad toevoegen aan de map met de bibliotheek die we zojuist hebben gegenereerd. Op deze manier weet Java waar we naar onze native libs moeten zoeken:

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

Console-uitgang:

Hallo van C ++ !!

4. Geavanceerde JNI-functies gebruiken

Hallo zeggen is leuk, maar niet erg handig. Meestal willen we gegevens uitwisselen tussen Java- en C ++ -code en deze gegevens in ons programma beheren.

4.1. Parameters toevoegen aan onze oorspronkelijke methoden

We zullen enkele parameters toevoegen aan onze native methoden. Laten we een nieuwe klas maken met de naam Voorbeeld Parameters JNI met twee native methoden met behulp van parameters en verschillende soorten retouren:

private native long sumIntegers (int eerste, int tweede); private native String sayHelloToMe (String naam, boolean isFemale);

En herhaal dan de procedure om een ​​nieuw .h-bestand te maken met "javac -h" zoals we eerder deden.

Maak nu het bijbehorende .cpp-bestand met de implementatie van de nieuwe C ++ - methode:

... JNIEXPORT jlong ​​JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers (JNIEnv * env, jobject thisObject, jint first, jint second) {std :: cout << "C ++: De ontvangen cijfers zijn:" << first << "en" << second NewStringUTF (fullName.c_str ()); } ...

We hebben de aanwijzer gebruikt * env van het type JNIEnv om toegang te krijgen tot de methoden die worden geboden door de JNI-omgevingsinstantie.

JNIEnv stelt ons in dit geval in staat Java te passeren Snaren in onze C ++ code en ga terug zonder je zorgen te maken over de implementatie.

We kunnen de gelijkwaardigheid van Java-typen en C JNI-typen controleren in de officiële documentatie van Oracle.

Om onze code te testen, moeten we alle compilatiestappen van de vorige herhalen Hallo Wereld voorbeeld.

4.2. Objecten gebruiken en Java-methoden aanroepen vanuit native code

In dit laatste voorbeeld gaan we zien hoe we Java-objecten kunnen manipuleren in onze native C ++ -code.

We beginnen met het maken van een nieuwe klas Gebruikersgegevens die we zullen gebruiken om wat gebruikersinformatie op te slaan:

pakket com.baeldung.jni; openbare klasse UserData {openbare tekenreeksnaam; publiek dubbel saldo; public String getUserInfo () {return "[name] =" + naam + ", [saldo] =" + saldo; }}

Vervolgens maken we nog een Java-klasse met de naam Voorbeeld ObjectenJNI met enkele native methoden waarmee we objecten van het type zullen beheren Gebruikersgegevens:

... publieke native UserData createUser (String naam, dubbele balans); openbare native String printUserData (UserData-gebruiker); 

Laten we nog een keer het .h header en vervolgens de C ++ -implementatie van onze native methoden op een nieuw .cpp het dossier:

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser (JNIEnv * env, jobject thisObject, jstring name, jdouble balance) {// Maak het object van de klasse UserData jclass userDataClass = env-> FindCeldata " jobject newUserData = env-> AllocObject (userDataClass); // Haal de UserData-velden op die moeten worden ingesteld jfieldID nameField = env-> GetFieldID (userDataClass, "name", "Ljava / lang / String;"); jfieldID balanceField = env-> GetFieldID (userDataClass, "balans", "D"); env-> SetObjectField (newUserData, nameField, naam); env-> SetDoubleField (newUserData, balanceField, balans); retourneer newUserData; } JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData (JNIEnv * env, jobject thisObject, jobject userData) {// Zoek de id van de Java-methode die jclass userDataClass = env-> GetObjectDataClass moet worden genoemd; jmethodID methodId = env-> GetMethodID (userDataClass, "getUserInfo", "() Ljava / lang / String;"); jstring resultaat = (jstring) env-> CallObjectMethod (userData, methodId); resultaat teruggeven; } 

Nogmaals, we gebruiken de JNIEnv * env pointer om toegang te krijgen tot de benodigde klassen, objecten, velden en methoden van de actieve JVM.

Normaal gesproken hoeven we alleen de volledige klassenaam op te geven om toegang te krijgen tot een Java-klasse, of de juiste naam en handtekening van de methode om toegang te krijgen tot een objectmethode.

We maken zelfs een instantie van de klasse com.baeldung.jni.UserData in onze native code. Zodra we de instantie hebben, kunnen we al zijn eigenschappen en methoden manipuleren op een manier die vergelijkbaar is met Java-reflectie.

We kunnen alle andere methoden van JNIEnv in de officiële documentatie van Oracle.

4. Nadelen van het gebruik van JNI

JNI-overbrugging heeft zijn valkuilen.

Het belangrijkste nadeel is de afhankelijkheid van het onderliggende platform; we verliezen in wezen het 'één keer schrijven, overal uitvoeren' kenmerk van Java. Dit betekent dat we een nieuwe bibliotheek moeten bouwen voor elke nieuwe combinatie van platform en architectuur die we willen ondersteunen. Stel je de impact voor die dit zou kunnen hebben op het bouwproces als we Windows, Linux, Android, MacOS ...

JNI voegt niet alleen een laagje complexiteit toe aan ons programma. Het voegt ook een kostbare communicatielaag toe tussen de code die in de JVM loopt en onze native code: we moeten de gegevens die op beide manieren worden uitgewisseld tussen Java en C ++ in een marshaling / unmarshaling-proces converteren.

Soms is er niet eens een directe conversie tussen typen, dus we zullen ons equivalent moeten schrijven.

5. Conclusie

Het compileren van de code voor een specifiek platform maakt het (meestal) sneller dan het uitvoeren van bytecode.

Dit maakt het handig wanneer we een veeleisend proces moeten versnellen. Ook als we geen andere alternatieven hebben, zoals wanneer we een bibliotheek moeten gebruiken die een apparaat beheert.

Dit heeft echter een prijs, aangezien we aanvullende code moeten bijhouden voor elk afzonderlijk platform dat we ondersteunen.

Daarom is het meestal een goed idee om Gebruik JNI alleen in de gevallen waarin er geen Java-alternatief is.

Zoals altijd is de code voor dit artikel beschikbaar op GitHub.