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

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

1. Вступление

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

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

Однако иногдаwe do actually need to use code that’s natively-compiled for a specific architecture.

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

  • Необходимость справиться с некоторым оборудованием

  • Улучшение производительности для очень сложного процесса

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

To achieve this, the JDK introduces a bridge between the bytecode running in our JVM and the native code (обычно пишется на C или C ++).

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

2. Как это устроено

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

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

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

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

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

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

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

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

private native void aNativeMethod();

С той основной разницей, чтоinstead of being implemented by another Java class, it will be implemented in a separated native shared library.

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

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

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

  • Java Code - наши занятия. Они будут включать как минимум один методnative.

  • Нативный код - актуальная логика наших нативных методов, обычно написанная на C или C ++.

  • Файл заголовка JNI - этот файл заголовка для C / C ++ (include/jni.h в каталоге JDK) включает все определения элементов JNI, которые мы можем использовать в наших собственных программах.

  • C/C++ Compiler – we can choose between GCC, Clang, Visual Studio, or any other we like as far as it’s able to generate a native shared library for our platform.

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

Элементы Java:

  • "Native" ключевое слово - как мы уже говорили, любой метод, помеченный как собственный, должен быть реализован в собственной общей библиотеке.

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

C/C++ elements (many of them defined within jni.h)

  • JNIEXPORT - помечает функцию в разделяемой библиотеке как экспортируемую, чтобы она была включена в таблицу функций, и, таким образом, JNI может найти ее

  • JNICALL - в сочетании сJNIEXPORT гарантирует, что наши методы доступны для инфраструктуры JNI.

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

  • JavaVM - структура, которая позволяет нам манипулировать работающей JVM (или даже запускать новую), добавляя к ней потоки, уничтожая ее и т. Д.

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

Далееlet’s look at how JNI works in practice.

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

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

  • Ubuntu Linux - запустите команду“sudo apt-get install build-essential” в терминале

  • Окна -Install MinGW

  • MacOS - запустите команду“g++” в терминале, и если ее еще нет, она установит.

3.1. Создание класса Java

Давайте начнем создавать нашу первую программу JNI с реализации классического «Hello World».

Для начала мы создадим следующий класс Java, который включает нативный метод, который будет выполнять эту работу:

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

Как видим,we load the shared library in a static block. Это гарантирует, что он будет готов, когда нам это нужно и оттуда, где нам это нужно.

В качестве альтернативы в этой тривиальной программе мы могли бы вместо этого загрузить библиотеку непосредственно перед вызовом нашего собственного метода, потому что мы нигде больше не используем родную библиотеку.

3.2. Реализация метода в C ++

Теперь нам нужно создать реализацию нашего нативного метода на C ++.

В C ++ определение и реализация обычно хранятся в файлах.h и.cpp соответственно.

Во-первых,to create the definition of the method, we have to use the -h flag of the Java compiler:

javac -h . HelloWorldJNI.java

Это сгенерирует файлcom_example_jni_HelloWorldJNI.h со всеми встроенными методами, включенными в класс, переданный в качестве параметра, в данном случае только один:

JNIEXPORT void JNICALL Java_com_example_jni_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);

Как мы видим, имя функции автоматически генерируется с использованием полного имени пакета, класса и метода.

Кроме того, мы можем заметить кое-что интересное: в нашу функцию передаются два параметра; указатель на текущийJNIEnv;, а также на объект Java, к которому прикреплен метод, экземпляр нашего классаHelloWorldJNI.

Теперь нам нужно создать новый файл.cpp для реализации функцииsayHello. This is where we’ll perform actions that print “Hello World” to console.

Назовем наш файл.cpp тем же именем, что и файл .h, содержащий заголовок, и добавим этот код для реализации нативной функции:

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

3.3. Компиляция и связывание

На данный момент у нас есть все необходимые детали и есть связь между ними.

Нам нужно собрать нашу общую библиотеку из кода C ++ и запустить ее!

Для этого мы должны использовать компилятор G ++,not forgetting to include the JNI headers from our Java JDK installation.

Версия Ubuntu:

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

Версия для Windows:

g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

Версия для MacOS;

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

Когда у нас есть код, скомпилированный для нашей платформы в файлcom_example_jni_HelloWorldJNI.o, мы должны включить его в новую общую библиотеку. Whatever we decide to name it is the argument passed into the method System.loadLibrary.

Мы назвали наш «родным», и мы будем загружать его при запуске нашего Java-кода.

Затем компоновщик G связывает объектные файлы C с нашей библиотекой, объединенной мостом.

Версия Ubuntu:

g++ -shared -fPIC -o libnative.so com_example_jni_HelloWorldJNI.o -lc

Версия для Windows:

g++ -shared -o native.dll com_example_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias

Версия для MacOS:

g++ -dynamiclib -o libnative.dylib com_example_jni_HelloWorldJNI.o -lc

Вот и все!

Теперь мы можем запустить нашу программу из командной строки.

Однакоwe need to add the full path to the directory containing the library we’ve just generated. Таким образом, Java будет знать, где искать наши собственные библиотеки:

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

Консольный вывод:

Hello from C++ !!

4. Использование расширенных функций JNI

Поздороваться приятно, но не очень полезно. Usually, we would like to exchange data between Java and C++ code and manage this data in our program.с

4.1. Добавление параметров в наши собственные методы

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

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_example_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_example_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 позволяет нам в этом случае передать JavaStrings в наш код C ++ и вернуться назад, не беспокоясь о реализации.

Мы можем проверить эквивалентность типов Java и типов C JNI вOracle official documentation.

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

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

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

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

package com.example.jni;

public class UserData {

    public String name;
    public double balance;

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

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

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

public native String printUserData(UserData user);

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

JNIEXPORT jobject JNICALL Java_com_example_jni_ExampleObjectsJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {

    // Create the object of the class UserData
    jclass userDataClass = env->FindClass("com/example/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_example_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.example.jni.UserData в нашем собственном коде. Once we have the instance, we can manipulate all its properties and methods in a way similar to Java reflection.

Мы можем проверить все другие методыJNIEnv вOracle official documentation.

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

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

Основным недостатком является зависимость от базовой платформы; we essentially lose the “write once, run anywhere” функция Java. Это означает, что нам придется создавать новую библиотеку для каждой новой комбинации платформы и архитектуры, которую мы хотим поддерживать. Представьте, какое влияние это может оказать на процесс сборки, если мы будем поддерживать Windows, Linux, Android, MacOS ...

JNI не только добавляет слой сложности к нашей программе. It also adds a costly layer of communication между кодом, выполняемым в JVM, и нашим собственным кодом: нам нужно преобразовать данные, которыми обмениваются в обоих направлениях между Java и C ++ в процессе маршалинга / демаршалинга.

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

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

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

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

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

Вот почему обычно рекомендуетсяonly use JNI in the cases where there’s no Java alternative.

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