Рубрики
Без рубрики

Руководство по JNI (собственный интерфейс Java)

Понимание JNI и способность связывать байт-код Java с собственным C++.

Автор оригинала: baeldung.

1. введение

Как мы знаем, одной из главных сильных сторон Java является ее переносимость – это означает, что как только мы пишем и компилируем код, результатом этого процесса является независимый от платформы байт-код.

Проще говоря, это может работать на любой машине или устройстве, способном запускать виртуальную машину Java, и это будет работать так же легко, как мы могли ожидать.

Однако иногда нам действительно нужно использовать код, который изначально скомпилирован для определенной архитектуры .

Могут быть некоторые причины для необходимости использования собственного кода:

  • Необходимость обработки некоторого оборудования
  • Повышение производительности для очень требовательного процесса
  • Существующая библиотека, которую мы хотим использовать повторно, а не переписывать ее на Java.

Для достижения этой цели JDK вводит мост между байт-кодом, выполняемым в нашей JVM, и собственным кодом (обычно написанным на C или C++).

Инструмент называется Java Native Interface. В этой статье мы рассмотрим, как с его помощью написать некоторый код.

2. Как Это Работает

2.1. Собственные Методы: JVM Соответствует Скомпилированному Коду

Java предоставляет ключевое слово native , которое используется для указания на то, что реализация метода будет обеспечена собственным кодом.

Обычно при создании собственной исполняемой программы мы можем использовать статические или общие библиотеки:

  • Статические библиотеки – все двоичные файлы библиотеки будут включены в состав нашего исполняемого файла во время процесса связывания. Таким образом, нам больше не понадобятся библиотеки, но это увеличит размер нашего исполняемого файла.
  • Общие библиотеки – конечный исполняемый файл содержит только ссылки на библиотеки, а не на сам код. Это требует, чтобы среда, в которой мы запускаем наш исполняемый файл, имела доступ ко всем файлам библиотек, используемых нашей программой.

Последнее имеет смысл для JNI, поскольку мы не можем смешивать байт-код и изначально скомпилированный код в один и тот же двоичный файл.

Таким образом, наша общая библиотека будет хранить машинный код отдельно в своем файле .so/.dll/.dylib (в зависимости от используемой операционной системы) вместо того, чтобы быть частью наших классов.

То родной ключевое слово превращает наш метод в своего рода абстрактный метод:

private native void aNativeMethod();

С той основной разницей, что вместо того, чтобы быть реализованным другим классом Java, он будет реализован в отдельной собственной общей библиотеке .

Таблица с указателями в памяти на реализацию всех наших собственных методов будет построена таким образом, чтобы их можно было вызывать из нашего кода Java.

2.2. Необходимые компоненты

Вот краткое описание ключевых компонентов, которые мы должны принять во внимание. Мы объясним их подробнее позже в этой статье

  • Java-код – наши классы. Они будут включать в себя по крайней мере один метод native .
  • Машинный код – фактическая логика наших собственных методов, обычно закодированных на C или C++.
  • Файл заголовка JNI – этот файл заголовка для C/C++ ( include/jni.h в каталог JDK) включает в себя все определения элементов JNI, которые мы можем использовать в наших собственных программах.
  • Компилятор C/C++ – мы можем выбирать между GCC, Clang, Visual Studio или любым другим, который нам нравится, поскольку он способен создавать собственную общую библиотеку для нашей платформы.

2.3. Элементы JNI в Коде (Java И C/C++)

Элементы Java:

  • ключевое слово “native” – как мы уже говорили, любой метод, помеченный как native, должен быть реализован в собственной общей библиотеке.
  • System.LoadLibrary(String libname) – статический метод, который загружает общую библиотеку из файловой системы в память и делает ее экспортированные функции доступными для нашего кода Java.

Элементы C/C++ (многие из них определены в jni.h )

  • JNIEXPORT – помечает функцию в общей библиотеке как экспортируемую, чтобы она была включена в таблицу функций, и, таким образом, JNI может найти ее
  • JNICALL – в сочетании с JNIEXPORT это гарантирует , что наши методы доступны для фреймворка JNI
  • JNIEnv – структура, содержащая методы, которые мы можем использовать наш собственный код для доступа к элементам Java
  • JavaVM – структура, которая позволяет нам манипулировать запущенной JVM (или даже запускать новую), добавляя в нее потоки, уничтожая ее и т. Д…

3. Привет, Мир JNI

Далее, давайте посмотрим, как JNI работает на практике.

В этом уроке мы будем использовать C++ в качестве родного языка и G++ в качестве компилятора и компоновщика.

Мы можем использовать любой другой компилятор по вашему выбору, но вот как установить G++ на Ubuntu, Windows и Mac OS:

  • 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

Здороваться приятно, но не очень полезно. Обычно мы хотели бы обмениваться данными между кодом Java и C++ и управлять этими данными в нашей программе.

4.1. Добавление Параметров В Наши Собственные Методы

Мы добавим некоторые параметры в наши собственные методы. Давайте создадим новый класс с именем Example Parameters UNIT с двумя собственными методами, использующими параметры и возвращаемые значения разных типов:

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

А затем повторите процедуру, чтобы создать новый файл .h с “javac-h”, как мы делали раньше.

Теперь создайте соответствующий файл .cpp с реализацией нового метода C++:

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

Мы использовали указатель *env типа JNIEnv для доступа к методам, предоставляемым экземпляром среды JNI.

JNIEnv позволяет нам в этом случае передавать Java Строки в наш код C++ и обратно, не беспокоясь о реализации.

Мы можем проверить эквивалентность типов Java и типов C JNI в Официальная документация Oracle.

Чтобы протестировать наш код, мы должны повторить все шаги компиляции предыдущего примера Hello World .

4.2. Использование Объектов и вызов Методов Java Из Собственного Кода

В этом последнем примере мы увидим, как мы можем манипулировать объектами Java в нашем собственном коде C++.

Мы начнем создавать новый класс Пользовательские данные , который мы будем использовать для хранения некоторой информации о пользователе:

package com.baeldung.jni;

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

Затем мы создадим другой класс Java с именем Example Objects JNI с некоторыми собственными методами, с помощью которых мы будем управлять объектами типа UserData :

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

Еще раз, давайте создадим заголовок .h , а затем реализацию наших собственных методов на C++ в новом файле .cpp :

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

Опять же, мы используем указатель JNIEnv *env для доступа к необходимым классам, объектам, полям и методам из запущенной JVM.

Обычно нам просто нужно указать полное имя класса для доступа к классу Java или правильное имя метода и подпись для доступа к объектному методу.

Мы даже создаем экземпляр класса com.baeldung.jni.Пользовательские данные в нашем собственном коде. Как только у нас есть экземпляр, мы можем манипулировать всеми его свойствами и методами аналогично отражению Java.

Мы можем проверить все другие методы JNIEnv в официальной документации Oracle .

4. Недостатки Использования JNI

Мост JNI действительно имеет свои подводные камни.

Основным недостатком является зависимость от базовой платформы; мы, по сути, теряем функцию “написать один раз, запустить в любом месте” Java. Это означает, что нам придется создавать новую библиотеку для каждой новой комбинации платформы и архитектуры, которую мы хотим поддерживать. Представьте себе, какое влияние это могло бы оказать на процесс сборки, если бы мы поддерживали Windows, Linux, Android, macOS…

JNI не только добавляет уровень сложности нашей программе. Это также добавляет дорогостоящий уровень связи между кодом, запущенным в JVM, и нашим собственным кодом: нам нужно преобразовать данные, которыми обмениваются в обоих направлениях между Java и C++, в процесс маршалинга/немаршалинга.

Иногда нет даже прямого преобразования между типами, поэтому нам придется написать наш эквивалент.

5. Заключение

Компиляция кода для определенной платформы (обычно) делает его быстрее, чем запуск байт-кода.

Это делает его полезным, когда нам нужно ускорить сложный процесс. Кроме того, когда у нас нет других альтернатив, например, когда нам нужно использовать библиотеку, которая управляет устройством.

Однако за это приходится платить, так как нам придется поддерживать дополнительный код для каждой поддерживаемой нами платформы.

Вот почему обычно рекомендуется использовать JNI только в тех случаях, когда нет альтернативы Java .

Как всегда, код для этой статьи доступен на GitHub .