Java >> Java tutorial >  >> Tag >> native

Guide til JNI (Java Native Interface)

1. Introduktion

Som vi ved, er en af ​​de største styrker ved Java dens portabilitet – hvilket betyder, at når vi først skriver og kompilerer kode, er resultatet af denne proces platform-uafhængig bytekode.

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 vil fungere så problemfrit, som vi kunne forvente.

Men nogle gange skal vi faktisk bruge kode, der er indbygget kompileret til en bestemt arkitektur .

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

  • Behovet for at håndtere noget hardware
  • Ydeevneforbedring for en meget krævende proces
  • Et eksisterende bibliotek, som vi ønsker at 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 hedder Java Native Interface. I denne artikel vil vi se, hvordan det er at skrive noget kode med det.

2. Sådan fungerer det

2.1. Native metoder:JVM opfylder kompileret kode

Java leverer native søgeord, der bruges til at angive, at metodeimplementeringen vil blive leveret af en indbygget kode.

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

  • Statiske biblioteker – alle bibliotekets binære filer vil blive inkluderet som en del af vores eksekverbare under sammenkædningsprocessen. Derfor har vi ikke brug for libs længere, men det vil øge størrelsen på vores eksekverbare fil.
  • Delte libs – den endelige eksekverbare har kun referencer til libs, ikke selve koden. Det kræver, at det miljø, vi kører vores eksekverbare i, har adgang til alle filerne i de libs, der bruges af vores program.

Det sidste er det, der giver mening for JNI, da vi ikke kan blande bytekode og indbygget kompileret kode i den samme binære fil.

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

Den native søgeord forvandler vores metode til en slags abstrakt metode:

private native void aNativeMethod();

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

En tabel med henvisninger i hukommelsen til implementeringen af ​​alle vores oprindelige 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 højde for. Vi forklarer dem yderligere senere i denne artikel

  • Java-kode – vores klasser. De vil omfatte mindst én native metode.
  • Native Code – den faktiske logik i vores native metoder, normalt kodet i C eller C++.
  • JNI-header-fil – denne header-fil til C/C++ (include/jni.h ind i JDK-biblioteket) inkluderer 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 en hvilken som helst anden 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" søgeord – som vi allerede har dækket, skal enhver metode, der er markeret som native, implementeres i en native, delt lib.
  • System.loadLibrary(String libname) – en statisk metode, der indlæser et delt bibliotek fra filsystemet til hukommelsen og gør dets eksporterede funktioner tilgængelige for vores Java-kode.

C/C++-elementer (mange af dem defineret i 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 , sikrer det, 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) ved at tilføje tråde til den, ødelægge den osv...

3. Hej verden JNI

Dernæst lad os se på, hvordan JNI fungerer i praksis.

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

Vi kan bruge enhver anden compiler efter vores præference, men her er, hvordan du installerer G++ på Ubuntu, Windows og MacOS:

  • Ubuntu Linux – kør kommandoen “sudo apt-get install build-essential” i en terminal
  • Windows – Installer MinGW
  • MacOS – kør kommandoen “g++” i en terminal, og hvis den endnu ikke er til stede, vil den installere den.

3.1. Oprettelse af Java-klassen

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

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

package com.baeldung.jni;

public class HelloWorldJNI {

    static {
        System.loadLibrary("native");
    }
    
    public static void main(String[] args) {
        new HelloWorldJNI().sayHello();
    }

    // Declare a native method sayHello() that receives no arguments and returns void
    private native void sayHello();
}

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

Alternativt kunne vi i dette trivielle program 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 lave implementeringen af ​​vores native metode i C++.

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

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

javac -h . HelloWorldJNI.java

Dette vil generere en com_baeldung_jni_HelloWorldJNI.h fil med alle de oprindelige metoder, der er inkluderet i klassen, sendt som en parameter, i dette tilfælde kun én:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);

Som vi kan se, genereres funktionsnavnet automatisk ved hjælp af det fuldt kvalificerede pakke-, klasse- og metodenavn.

Også noget interessant, som vi kan bemærke, er, at vi får to parametre videregivet til vores funktion; en pegepind til den aktuelle JNIEnv; og også Java-objektet, som metoden er knyttet til, instansen af ​​vores HelloWorldJNI klasse.

Nu skal vi oprette en ny .cpp fil til implementering af sayHello fungere. Det er her, vi udfører handlinger, der udskriver "Hello World" til konsollen.

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 void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    std::cout << "Hello from C++ !!" << std::endl;
}

3.3. Kompilere og linke

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

Vi skal bygge vores delte bibliotek ud fra C++-koden og køre det!

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

Ubuntu-version:

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

Når vi har koden kompileret til vores platform i filen com_baeldung_jni_HelloWorldJNI.o , skal vi inkludere det i et nyt fælles bibliotek. Hvad vi end beslutter os for at navngive det, er argumentet, der overføres til metoden System.loadLibrary .

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

G++-linkeren linker derefter C++-objektfilerne til vores brokoblede bibliotek.

Ubuntu-version:

g++ -shared -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.

Men vi skal tilføje den fulde sti til den mappe, der indeholder det bibliotek, vi lige har genereret. På denne måde vil Java vide, hvor de skal lede efter vores oprindelige libs:

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

Konsoludgang:

Hello from C++ !!

4. Brug 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 ExampleParametersJNI med to native metoder, der bruger parametre og returnerer af forskellige typer:

private native long sumIntegers(int first, int second);
    
private native String sayHelloToMe(String name, boolean isFemale);

Og 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++: The numbers received are : " << first << " and " << second << std::endl;
    return (long)first + (long)second;
}
JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sayHelloToMe 
  (JNIEnv* env, jobject thisObject, jstring name, jboolean isFemale) {
    const char* nameCharPointer = env->GetStringUTFChars(name, NULL);
    std::string title;
    if(isFemale) {
        title = "Ms. ";
    }
    else {
        title = "Mr. ";
    }

    std::string fullName = title + nameCharPointer;
    return env->NewStringUTF(fullName.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 sende Java Strings ind i vores C++-kode og gå tilbage uden at bekymre dig om implementeringen.

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

For at teste vores kode er vi nødt til at gentage alle kompileringstrinene fra den tidligere HelloWorld eksempel.

4.2. Brug af objekter og kalder Java-metoder fra indbygget 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 UserData som vi vil bruge til at gemme nogle brugeroplysninger:

package com.baeldung.jni;

public class UserData {
    
    public String name;
    public double balance;
    
    public String getUserInfo() {
        return "[name]=" + name + ", [balance]=" + balance;
    }
}

Derefter opretter vi en anden Java-klasse kaldet ExampleObjectsJNI med nogle indbyggede metoder, hvormed vi administrerer objekter af typen Brugerdata :

...
public native UserData createUser(String name, double balance);
    
public native String printUserData(UserData user);

En gang til, lad os oprette .h header og derefter C++-implementeringen af ​​vores native metoder på en ny .cpp fil:

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {
  
    // Create the object of the class UserData
    jclass userDataClass = env->FindClass("com/baeldung/jni/UserData");
    jobject newUserData = env->AllocObject(userDataClass);
	
    // Get the UserData fields to be set
    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);
    
    return newUserData;
}

JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData
  (JNIEnv *env, jobject thisObject, jobject userData) {
  	
    // Find the id of the Java method to be called
    jclass userDataClass=env->GetObjectClass(userData);
    jmethodID methodId=env->GetMethodID(userDataClass, "getUserInfo", "()Ljava/lang/String;");

    jstring result = (jstring)env->CallObjectMethod(userData, methodId);
    return result;
}

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

Normalt skal vi blot angive 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 er endda ved at oprette en forekomst af klassen com.baeldung.jni.UserData i vores oprindelige kode. Når vi har instansen, kan vi manipulere alle dens egenskaber og metoder på en måde, der ligner Java-refleksion.

Vi kan kontrollere alle andre metoder for JNIEnv i Oracles officielle dokumentation.

4. Ulemper ved at bruge JNI

JNI brobygning har sine faldgruber.

Den største ulempe er afhængigheden af ​​den underliggende platform; vi mister stort set "skriv én gang, løb hvor som helst" funktion af Java. Det betyder, at vi bliver nødt til at bygge en ny lib for hver ny kombination af platform og arkitektur, vi ønsker at understøtte. Forestil dig, hvilken indvirkning dette kunne have på byggeprocessen, hvis vi understøttede Windows, Linux, Android, MacOS...

JNI tilføjer ikke kun et lag af kompleksitet til vores program. Det tilføjer også et kostbart kommunikationslag mellem koden, der kører 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 typer, så vi bliver nødt til at skrive vores tilsvarende.

5. Konklusion

At kompilere koden til en bestemt platform (normalt) gør det hurtigere end at køre bytekode.

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

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

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

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


Java tag