Panduan untuk JNI (Java Native Interface)

1. Pengenalan

Seperti yang kita ketahui, salah satu kekuatan utama Java adalah mudah alihnya - yang bermaksud bahawa setelah kita menulis dan menyusun kod, hasil dari proses ini adalah bytecode bebas platform.

Ringkasnya, ini dapat dijalankan pada mesin atau perangkat apa pun yang mampu menjalankan Java Virtual Machine, dan ia akan berfungsi dengan lancar seperti yang kita harapkan.

Walau bagaimanapun, kadang-kadang kita sebenarnya perlu menggunakan kod yang disusun secara asli untuk seni bina tertentu .

Mungkin ada beberapa sebab untuk menggunakan kod asli:

  • Keperluan untuk mengendalikan beberapa perkakasan
  • Peningkatan prestasi untuk proses yang sangat menuntut
  • Perpustakaan yang ada yang ingin kita gunakan semula dan bukannya menulisnya semula di Java.

Untuk mencapainya, JDK memperkenalkan jambatan antara kod bytec yang berjalan di JVM kami dan kod asli (biasanya ditulis dalam C atau C ++).

Alat ini dipanggil Java Native Interface. Dalam artikel ini, kita akan melihat bagaimana menulis beberapa kod dengannya.

2. Bagaimana Ia Berfungsi

2.1. Kaedah Asli: JVM Memenuhi Kod yang Disusun

Java menyediakan kata kunci asli yang digunakan untuk menunjukkan bahawa pelaksanaan kaedah akan diberikan oleh kod asli.

Biasanya, ketika membuat program asli yang dapat dilaksanakan, kita dapat memilih untuk menggunakan lib statis atau bersama:

  • Lib statik - semua binari perpustakaan akan disertakan sebagai sebahagian daripada pelaksanaan kami semasa proses penghubung. Oleh itu, kami tidak memerlukan lib lagi, tetapi akan meningkatkan saiz fail yang boleh dilaksanakan kami.
  • Lib yang dikongsi - yang boleh dilaksanakan akhir hanya mempunyai rujukan ke lib, bukan kod itu sendiri. Ia memerlukan bahawa persekitaran di mana kita menjalankan eksekusi kita dapat mengakses semua fail lib yang digunakan oleh program kita.

Yang terakhir adalah yang masuk akal bagi JNI kerana kita tidak dapat mencampurkan kod bytec dan kod yang disusun secara asli ke dalam fail binari yang sama.

Oleh itu, lib bersama kami akan menyimpan kod asli secara berasingan dalam fail .so / .dll / .dylib (bergantung pada Sistem Operasi yang kami gunakan) dan bukannya menjadi sebahagian daripada kelas kami.

Kata kunci asli mengubah kaedah kami menjadi kaedah abstrak:

private native void aNativeMethod();

Dengan perbezaan utama bahawa bukannya dilaksanakan oleh kelas Java yang lain, ia akan dilaksanakan di perpustakaan bersama asli yang terpisah .

Jadual dengan petunjuk untuk mengimplementasikan semua kaedah asli kita akan dibina sehingga dapat dipanggil dari kod Java kita.

2.2. Komponen Diperlukan

Berikut adalah penerangan ringkas mengenai komponen utama yang perlu kita ambil kira. Kami akan menerangkannya lebih lanjut dalam artikel ini

  • Kod Java - kelas kami. Mereka akan memasukkan sekurang-kurangnya satu kaedah asli .
  • Native Code - logik sebenar kaedah asli kita, biasanya dikodkan dalam C atau C ++.
  • Fail tajuk JNI - fail tajuk ini untuk C / C ++ ( sertakan / jni.h ke dalam direktori JDK) merangkumi semua definisi elemen JNI yang mungkin kami gunakan ke dalam program asli kami.
  • Pengkompilasi C / C ++ - kita dapat memilih antara GCC, Clang, Visual Studio, atau yang lain yang kita suka sejauh mana ia dapat menghasilkan perpustakaan bersama asli untuk platform kita.

2.3. Elemen JNI dalam Kod (Java Dan C / C ++)

Elemen Java:

  • Kata kunci "asli" - seperti yang telah kita bahas, kaedah apa pun yang ditandai sebagai asli mesti dilaksanakan dalam lib yang dikongsi asli.
  • System.loadLibrary (String libname) - kaedah statik yang memuatkan pustaka bersama dari sistem fail ke dalam memori dan menjadikan fungsi yang dieksport tersedia untuk kod Java kami.

Elemen C / C ++ (kebanyakannya ditentukan dalam jni.h )

  • JNIEXPORT- menandakan fungsi ke lib bersama sebagai eksport sehingga akan dimasukkan ke dalam jadual fungsi, dan dengan itu JNI dapat menemuinya
  • JNICALL - digabungkan dengan JNIEXPORT , memastikan kaedah kami tersedia untuk rangka kerja JNI
  • JNIEnv - struktur yang mengandungi kaedah yang dapat kita gunakan kod asli kita untuk mengakses elemen Java
  • JavaVM - struktur yang membolehkan kita memanipulasi JVM yang sedang berjalan (atau bahkan memulakan yang baru) menambahkan utas ke dalamnya, memusnahkannya, dan lain-lain ...

3. Hello World JNI

Seterusnya, mari kita lihat bagaimana JNI berfungsi dalam praktik.

Dalam tutorial ini, kami akan menggunakan C ++ sebagai bahasa ibunda dan G ++ sebagai penyusun dan penghubung.

Kita boleh menggunakan penyusun pilihan lain, tetapi inilah cara memasang G ++ di Ubuntu, Windows dan MacOS:

  • Ubuntu Linux - jalankan perintah "sudo apt-get install build-essential" di terminal
  • Windows - Pasang MinGW
  • Perintah MacOS - run "g ++" di terminal dan jika belum ada, ia akan memasangnya.

3.1. Membuat Kelas Java

Mari mulakan program JNI pertama kami dengan menerapkan "Hello World" klasik.

Untuk memulakan, kami membuat kelas Java berikut yang merangkumi kaedah asli yang akan melakukan kerja:

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(); }

Seperti yang kita lihat, kita memuatkan pustaka bersama dalam blok statik . Ini memastikan bahawa ia akan siap ketika kita memerlukannya dan dari mana sahaja kita memerlukannya.

Sebagai alternatif, dalam program sepele ini, kita boleh memuatkan perpustakaan sebelum memanggil kaedah asli kita kerana kita tidak menggunakan perpustakaan asli di tempat lain.

3.2. Melaksanakan Kaedah dalam C ++

Sekarang, kita perlu membuat pelaksanaan kaedah asli kita di C ++.

Dalam C ++ definisi dan pelaksanaannya biasanya disimpan dalam fail .h dan .cpp masing-masing.

First, to create the definition of the method, we have to use the -h flag of the Java compiler:

javac -h . HelloWorldJNI.java

This will generate a com_baeldung_jni_HelloWorldJNI.h file with all the native methods included in the class passed as a parameter, in this case, only one:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject); 

As we can see, the function name is automatically generated using the fully qualified package, class and method name.

Also, something interesting that we can notice is that we're getting two parameters passed to our function; a pointer to the current JNIEnv; and also the Java object that the method is attached to, the instance of our HelloWorldJNI class.

Now, we have to create a new .cpp file for the implementation of the sayHello function. This is where we'll perform actions that print “Hello World” to console.

We'll name our .cpp file with the same name as the .h one containing the header and add this code to implement the native function:

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

3.3. Compiling And Linking

At this point, we have all parts we need in place and have a connection between them.

We need to build our shared library from the C++ code and run it!

To do so, we have to use G++ compiler, not forgetting to include the JNI headers from our 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

Once we have the code compiled for our platform into the file com_baeldung_jni_HelloWorldJNI.o, we have to include it in a new shared library. Whatever we decide to name it is the argument passed into the method System.loadLibrary.

We named ours “native”, and we'll load it when running our Java code.

The G++ linker then links the C++ object files into our bridged library.

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

And that's it!

We can now run our program from the command line.

However, we need to add the full path to the directory containing the library we've just generated. This way Java will know where to look for our native libs:

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

Console output:

Hello from C++ !!

4. Using Advanced JNI Features

Saying hello is nice but not very useful. Usually, we would like to exchange data between Java and C++ code and manage this data in our program.

4.1. Adding Parameters To Our Native Methods

We'll add some parameters to our native methods. Let's create a new class called ExampleParametersJNI with two native methods using parameters and returns of different types:

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

And then, repeat the procedure to create a new .h file with “javac -h” as we did before.

Now create the corresponding .cpp file with the implementation of the new C++ method:

... 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 
    
     NewStringUTF(fullName.c_str()); } ...
    

We've used the pointer *env of type JNIEnv to access the methods provided by the JNI environment instance.

JNIEnv allows us, in this case, to pass Java Strings into our C++ code and back out without worrying about the implementation.

We can check the equivalence of Java types and C JNI types into Oracle official documentation.

To test our code, we've to repeat all the compilation steps of the previous HelloWorld example.

4.2. Using Objects and Calling Java Methods From Native Code

In this last example, we're going to see how we can manipulate Java objects into our native C++ code.

We'll start creating a new class UserData that we'll use to store some user info:

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

Then, we'll create another Java class called ExampleObjectsJNI with some native methods with which we'll manage objects of type UserData:

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

One more time, let's create the .h header and then the C++ implementation of our native methods on a new .cpp file:

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; } 

Again, we're using the JNIEnv *env pointer to access the needed classes, objects, fields and methods from the running JVM.

Normally, we just need to provide the full class name to access a Java class, or the correct method name and signature to access an object method.

We're even creating an instance of the class com.baeldung.jni.UserData in our native code. Once we have the instance, we can manipulate all its properties and methods in a way similar to Java reflection.

We can check all other methods of JNIEnv into the Oracle official documentation.

4. Disadvantages Of Using JNI

JNI bridging does have its pitfalls.

The main downside being the dependency on the underlying platform; we essentially lose the “write once, run anywhere” feature of Java. This means that we'll have to build a new lib for each new combination of platform and architecture we want to support. Imagine the impact that this could have on the build process if we supported Windows, Linux, Android, MacOS…

JNI not only adds a layer of complexity to our program. It also adds a costly layer of communication between the code running into the JVM and our native code: we need to convert the data exchanged in both ways between Java and C++ in a marshaling/unmarshaling process.

Sometimes there isn't even a direct conversion between types so we'll have to write our equivalent.

5. Conclusion

Compiling the code for a specific platform (usually) makes it faster than running bytecode.

This makes it useful when we need to speed up a demanding process. Also, when we don't have other alternatives such as when we need to use a library that manages a device.

However, this comes at a price as we'll have to maintain additional code for each different platform we support.

Itulah sebabnya biasanya adalah idea yang baik untuk hanya menggunakan JNI sekiranya tidak ada alternatif Java .

Seperti biasa kod untuk artikel ini terdapat di GitHub.