1. Въведение
Както знаем, една от основните силни страни на Java е нейната преносимост - което означава, че след като напишем и компилираме код, резултатът от този процес е независим от платформата байт код.
Най-просто казано, това може да работи на всяка машина или устройство, способно да работи с виртуална машина Java и ще работи толкова безпроблемно, колкото бихме могли да очакваме.
Понякога обаче всъщност се налага да използваме код, който е компилиран по естествен път за конкретна архитектура .
Може да има някои причини за необходимостта да се използва роден код:
- Необходимостта да се борави с някакъв хардуер
- Подобряване на производителността за много взискателен процес
- Съществуваща библиотека, която искаме да използваме повторно, вместо да я пренаписваме в Java.
За да постигне това, JDK въвежда мост между байт кода, работещ в нашата JVM, и родния код (обикновено написан на C или C ++).
Инструментът се нарича Java Native Interface. В тази статия ще видим как е да напишем някакъв код с нея.
2. Как работи
2.1. Родни методи: JVM отговаря на компилирания код
Java предоставя родната ключова дума, която се използва, за да покаже, че изпълнението на метода ще бъде осигурено от роден код.
Обикновено, когато правим родна изпълнима програма, можем да изберем да използваме статични или споделени библиотеки:
- Статични библиотеки - всички двоични файлове на библиотеката ще бъдат включени като част от изпълнимия файл по време на процеса на свързване. По този начин вече няма да имаме нужда от библиотеките, но това ще увеличи размера на нашия изпълним файл.
- Споделени библиотеки - крайният изпълним файл има препратки само към библиотеките, а не към самия код. Това изисква средата, в която изпълняваме нашия изпълним файл, да има достъп до всички файлове на библиотеките, използвани от нашата програма.
Последното е онова, което има смисъл за JNI, тъй като не можем да смесваме байт код и компилиран код в същия двоичен файл.
Следователно, нашият споделен lib ще запази родния код отделно в неговия .so / .dll / .dylib файл (в зависимост от това коя операционна система използваме), вместо да бъде част от нашите класове.
В родния ключова дума трансформира нашия метод в нещо абстрактно метод:
private native void aNativeMethod();
С основната разлика, че вместо да бъде внедрен от друг клас Java, той ще бъде реализиран в отделна родна споделена библиотека .
Ще бъде изградена таблица с указатели в паметта за изпълнението на всички наши собствени методи, за да могат да бъдат извикани от нашия Java код.
2.2. Необходими компоненти
Ето кратко описание на ключовите компоненти, които трябва да вземем предвид. Ще ги обясним допълнително по-късно в тази статия
- Java Code - нашите класове. Те ще включват поне един естествен метод.
- Native Code - действителната логика на нашите собствени методи, обикновено кодирани в C или C ++.
- JNI заглавен файл - този заглавен файл за C / C ++ ( включва / jni.h в директорията JDK) включва всички дефиниции на JNI елементи, които можем да използваме в нашите собствени програми.
- C / C ++ Compiler - можем да избираме между GCC, Clang, Visual Studio или друг, който ни харесва, доколкото е в състояние да генерира собствена споделена библиотека за нашата платформа.
2.3. JNI елементи в кода (Java и C / C ++)
Java елементи:
- „Native“ ключова дума - както вече разгледахме, всеки метод, означен като native, трябва да бъде реализиран в роден споделен lib.
- System.loadLibrary (String libname) - статичен метод, който зарежда споделена библиотека от файловата система в паметта и прави експортираните й функции достъпни за нашия Java код.
C / C ++ елементи (много от тях са дефинирани в jni.h )
- JNIEXPORT - маркира функцията в споделената библиотека като експортируема, така че тя ще бъде включена във функционалната таблица и по този начин JNI може да я намери
- JNICALL - в комбинация с JNIEXPORT , той гарантира, че нашите методи са достъпни за рамката JNI
- JNIEnv - структура, съдържаща методи, които можем да използваме нашия собствен код за достъп до Java елементи
- JavaVM - структура, която ни позволява да манипулираме работещ JVM (или дори да стартираме нов), добавяйки нишки към него, унищожавайки го и т.н. ...
3. Hello World JNI
След това нека да разгледаме как JNI работи на практика.
В този урок ще използваме C ++ като роден език и G ++ като компилатор и линкер.
Можем да използваме всеки друг компилатор по наше предпочитание, но ето как да инсталираме G ++ на Ubuntu, Windows и MacOS:
- Ubuntu Linux - стартирайте командата “sudo apt-get install build-essential” в терминал
- Windows - Инсталирайте MinGW
- MacOS - стартирайте команда „g ++“ в терминал и ако все още не е налице, ще я инсталира.
3.1. Създаване на Java клас
Нека да започнем да създаваме първата си програма JNI чрез прилагане на класически „Hello World“.
За начало създаваме следния клас Java, който включва родния метод, който ще изпълнява работата:
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(); }
Както виждаме, зареждаме споделената библиотека в статичен блок . Това гарантира, че тя ще бъде готова, когато имаме нужда от нея и от където и да ни е необходима.
Като алтернатива, в тази тривиална програма бихме могли вместо това да заредим библиотеката точно преди да извикаме нашия роден метод, защото не използваме родната библиотека никъде другаде.
3.2. Внедряване на метод в C ++
Сега трябва да създадем внедряването на нашия собствен метод в C ++.
В рамките на C ++ дефиницията и изпълнението обикновено се съхраняват съответно в .h и .cpp файлове.
Първо, за да създадем дефиницията на метода, трябва да използваме флага -h на Java компилатора :
javac -h . HelloWorldJNI.java
Това ще генерира файл com_baeldung_jni_HelloWorldJNI.h с всички естествени методи, включени в класа, предаден като параметър, в този случай само един:
JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject);
Както виждаме, името на функцията се генерира автоматично, използвайки напълно квалифицираното име на клас, клас и метод.
Също така, нещо интересно, което можем да забележим, е, че получаваме два параметъра, предадени на нашата функция; указател към текущия JNIEnv; а също и обекта Java, към който е прикрепен методът, екземпляра на нашия клас HelloWorldJNI .
Сега трябва да създадем нов .cpp файл за изпълнение на функцията sayHello . Тук ще извършим действия, които отпечатват “Hello World” за конзола.
Ще назовем нашия .cpp файл със същото име като този .h, съдържащ заглавката, и ще добавим този код, за да реализираме собствената функция:
JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv* env, jobject thisObject) { std::cout << "Hello from C++ !!" << std::endl; }
3.3. Компилиране и свързване
На този етап разполагаме с всички необходими части и имаме връзка между тях.
Трябва да изградим нашата споделена библиотека от C ++ кода и да я стартираме!
За целта трябва да използваме компилатор G ++, като не забравяме да включим и JNI заглавията от нашата Java JDK инсталация .
Версия на Ubuntu:
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
Версия на Windows:
g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
Версия на MacOS;
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
След като сме компилирали кода за нашата платформа във файла com_baeldung_jni_HelloWorldJNI.o , трябва да го включим в нова споделена библиотека. Каквото и да решим да назовем, това е аргументът, предаден в метода System.loadLibrary .
Нарекохме нашите „родни“ и ще ги заредим, когато изпълняваме нашия Java код.
След това G ++ линкерът свързва C ++ обектните файлове в нашата мостова библиотека.
Версия на Ubuntu:
g++ -shared -fPIC -o libnative.so com_baeldung_jni_HelloWorldJNI.o -lc
Версия на Windows:
g++ -shared -o native.dll com_baeldung_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias
Версия на MacOS:
g++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc
И това е!
Вече можем да стартираме нашата програма от командния ред.
Трябва обаче да добавим пълния път към директорията, съдържаща току-що генерираната библиотека. По този начин Java ще знае къде да търси нашите родни библиотеки:
java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.baeldung.jni.HelloWorldJNI
Конзолен изход:
Hello from C++ !!
4. Използване на разширени функции на JNI
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.
Ето защо обикновено е добра идея да използвате JNI само в случаите, когато няма алтернатива на Java .
Както винаги кодът за тази статия е достъпен в GitHub.