Panduan untuk JNI (Java Native Interface)

1. Perkenalan

Seperti yang kita ketahui, salah satu kekuatan utama Java adalah portabilitasnya - artinya setelah kita menulis dan mengompilasi kode, hasil dari proses ini adalah bytecode yang tidak bergantung platform.

Sederhananya, ini dapat berjalan di mesin atau perangkat apa pun yang mampu menjalankan Mesin Virtual Java, dan ini akan bekerja semulus yang kita harapkan.

Namun, terkadang kita benar-benar perlu menggunakan kode yang dikompilasi secara native untuk arsitektur tertentu .

Mungkin ada beberapa alasan untuk perlu menggunakan kode asli:

  • Kebutuhan untuk menangani beberapa perangkat keras
  • Peningkatan kinerja untuk proses yang sangat menuntut
  • Pustaka yang ada yang ingin kami gunakan kembali alih-alih menulis ulang di Java.

Untuk mencapai ini, JDK memperkenalkan jembatan antara bytecode yang berjalan di JVM kami dan kode native (biasanya ditulis dalam C atau C ++).

Alat tersebut disebut Java Native Interface. Di artikel ini, kita akan melihat bagaimana cara menulis beberapa kode dengannya.

2. Bagaimana Ini Bekerja

2.1. Metode Asli: JVM Memenuhi Kode yang Dikompilasi

Java menyediakan kata kunci native yang digunakan untuk menunjukkan bahwa implementasi metode akan disediakan oleh kode native.

Biasanya, saat membuat program executable asli, kita dapat memilih untuk menggunakan libs statis atau bersama:

  • Pustaka statis - semua biner pustaka akan dimasukkan sebagai bagian dari eksekusi kami selama proses penautan. Jadi, kami tidak membutuhkan libs lagi, tetapi itu akan meningkatkan ukuran file yang dapat dieksekusi.
  • Berbagi libs - eksekusi akhir hanya memiliki referensi ke libs, bukan kode itu sendiri. Ini mensyaratkan bahwa lingkungan tempat kami menjalankan eksekusi kami memiliki akses ke semua file lib yang digunakan oleh program kami.

Yang terakhir inilah yang masuk akal untuk JNI karena kami tidak dapat mencampur bytecode dan kode yang dikompilasi secara native ke dalam file biner yang sama.

Oleh karena itu, lib bersama kami akan menyimpan kode asli secara terpisah di dalam file .so / .dll / .dylib (bergantung pada Sistem Operasi mana yang kami gunakan) alih-alih menjadi bagian dari kelas kami.

Kata kunci native mengubah metode kami menjadi semacam metode abstrak:

private native void aNativeMethod();

Dengan perbedaan utama bahwa alih - alih diimplementasikan oleh kelas Java lain, ini akan diimplementasikan di perpustakaan bersama bawaan yang terpisah .

Sebuah tabel dengan pointer dalam memori untuk mengimplementasikan semua metode native kita akan dibangun sehingga bisa dipanggil dari kode Java kita.

2.2. Komponen yang Dibutuhkan

Berikut penjelasan singkat tentang komponen utama yang perlu kita perhitungkan. Kami akan menjelaskannya lebih lanjut nanti di artikel ini

  • Kode Java - kelas kami. Mereka akan menyertakan setidaknya satu metode asli .
  • Kode Asli - logika sebenarnya dari metode asli kami, biasanya dikodekan dalam C atau C ++.
  • File header JNI - file header ini untuk C / C ++ ( sertakan / jni.h ke dalam direktori JDK) menyertakan semua definisi elemen JNI yang dapat kita gunakan ke dalam program asli kita.
  • C / C ++ Compiler - kita dapat memilih antara GCC, Clang, Visual Studio, atau apa pun yang kita suka sejauh itu dapat menghasilkan pustaka bersama asli untuk platform kita.

2.3. Elemen JNI dalam Kode (Java dan C / C ++)

Elemen Java:

  • Kata kunci “native” - seperti yang telah kita bahas, metode apa pun yang ditandai sebagai native harus diimplementasikan dalam native, lib bersama.
  • System.loadLibrary (String libname) - metode statis yang memuat pustaka bersama dari sistem file ke dalam memori dan membuat fungsi yang diekspor tersedia untuk kode Java kita.

Elemen C / C ++ (banyak di antaranya didefinisikan dalam jni.h )

  • JNIEXPORT- menandai fungsi ke lib bersama sebagai dapat diekspor sehingga akan dimasukkan dalam tabel fungsi, dan dengan demikian JNI dapat menemukannya
  • JNICALL - digabungkan dengan JNIEXPORT , memastikan bahwa metode kami tersedia untuk kerangka kerja JNI
  • JNIEnv - struktur yang berisi metode yang kita dapat menggunakan kode asli kita untuk mengakses elemen Java
  • JavaVM - struktur yang memungkinkan kita memanipulasi JVM yang sedang berjalan (atau bahkan memulai yang baru) menambahkan utas ke dalamnya, menghancurkannya, dll…

3. Halo Dunia JNI

Selanjutnya, mari kita lihat bagaimana JNI bekerja dalam praktiknya.

Dalam tutorial ini, kita akan menggunakan C ++ sebagai bahasa native dan G ++ sebagai compiler dan linker.

Kami dapat menggunakan kompiler lain yang kami sukai, tetapi berikut ini cara menginstal G ++ di Ubuntu, Windows, dan MacOS:

  • Ubuntu Linux - jalankan perintah "sudo apt-get install build-essential" di terminal
  • Windows - Instal MinGW
  • MacOS - jalankan perintah "g ++" di terminal dan jika belum ada, itu akan menginstalnya.

3.1. Membuat Kelas Java

Mari kita mulai membuat program JNI pertama kita dengan menerapkan "Hello World" klasik.

Untuk memulai, kami membuat kelas Java berikut yang menyertakan metode asli yang akan melakukan pekerjaan:

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 bisa kita lihat, kita memuat pustaka bersama dalam blok statis . Ini memastikan bahwa itu akan siap saat kita membutuhkannya dan dari mana pun kita membutuhkannya.

Alternatifnya, dalam program sepele ini, kita bisa memuat perpustakaan sebelum memanggil metode asli kita karena kita tidak menggunakan perpustakaan asli di tempat lain.

3.2. Menerapkan Metode di C ++

Sekarang, kita perlu membuat implementasi metode asli kita di C ++.

Dalam C ++ definisi dan implementasi biasanya disimpan di file .h dan .cpp .

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 mengapa biasanya ide yang bagus untuk hanya menggunakan JNI jika tidak ada alternatif Java .

Seperti biasa, kode untuk artikel ini tersedia di GitHub.