Введение
Бывают моменты, когда в Java некоторые действия выполняются за пределами обычных Java-классов. Например, необходимо исполнить код, написанный на C/C++ или другом каком-нибудь языке.
В данной статье рассмотрим данный вопрос с практической точки зрения, а именно напишем простой пример взаимодействия кода Java с кодом C++, используя JNI. Статья не содержит чего-то сверхестественного, это скорее памятка для тех, кто с этим не работал.
Для наших целей существует возможность динамической загрузки нативных библиотек, вызываемая методом
System.load(), о чем более подробно можно прочитать здесь.Постановка задачи
Пусть нам необходимо реализовать класс, содержащий в себе нативный метод, выводящий на экран “Hello world”.
JNIHelloWorld.java
package ru.forwolk.test; public class JNIHelloWorld { native void printHelloWorld(); }
Генерация заголовков
Сгенерируем заголовки данного класса для C++.
Сначала создадим папку в корне проекта, где будем собирать бинарники:
mkdir bin
Затем, скомпилируем наш класс в данную директорию
javac -d bin/ src/ru/forwolk/test/JNIHelloWorld.java
В папке bin у нас появился class-файл. Вернее, в bin/ru/forwolk/test/. Переидем в папку bin и сгенерируем заголовки.
cd bin/ javah ru.forwolk.test.JNIHelloWorld
Как видно, в нашей папке bin появился файл ru_forwolk_test_JNIHelloWorld.h. Для простоты переименуем его в JNIHelloWorld.h
mv ru_forwolk_test_JNIHelloWorld.h JNIHelloWorld.h
Открыв его, видим следующую картину:
JNIHelloWorld.h /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class ru_forwolk_test_JNIHelloWorld */ #ifndef _Included_ru_forwolk_test_JNIHelloWorld #define _Included_ru_forwolk_test_JNIHelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: ru_forwolk_test_JNIHelloWorld * Method: printHelloWorld * Signature: ()V */ JNIEXPORT void JNICALL Java_ru_forwolk_test_JNIHelloWorld_printHelloWorld (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
Реализация на C++
Создадим файл с исходниками JNIHelloWorld.cpp. Я для этой цели создал проект в Clion, в который вставил необходимый файл. Реализуем наш метод.
JNIHelloWorld.cpp #include <iostream> #include "JNIHelloWorld.h" JNIEXPORT void JNICALL Java_ru_forwolk_test_JNIHelloWorld_printHelloWorld (JNIEnv *, jobject) { std::cout << "Hello world!"; }
Чтобы в Clion все работало корректно, необходимо в файл CMakeLists.txt добавить библиотеки Java:
// Вместо $JAVA_HOME -- путь до Java include_directories($JAVA_HOME/include) include_directories($JAVA_HOME/include/linux) link_directories($JAVA_HOME/include) link_directories($JAVA_HOME/include/linux)
Далее компилируем
g++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -fPIC JNIHelloWorld.cpp -shared -o helloworld.so -Wl,-soname -Wl,--no-whole-archive
Загрузка в Java
В корневой папке проекта появился файл helloworld.so. Переместим его в папку bin/ проекта Java.
Теперь необходимо загрузить нашу библиотеку. Хорошей практикой будет статическая загрузка библиотеки прямо в классе. Добавим загрузку прямо в класс JNIHelloWorld
static { // $PROJECT_ROOT -- абсолютный путь до библиотеки System.load("$PROJECT_ROOT/bin/helloworld.so"); }
Теперь мы можем полноценно использовать данный класс. Давайте проверим.
public static void main(String[] args) { JNIHelloWorld p = new JNIHelloWorld(); p.printHelloWorld(); }
На выходе получаем
Hello world! Process finished with exit source 0
Передача параметров
А что делать, если нам надо не только выполнять какой-то код, а также передавать параметры и получать ответ? Рассмотрим еще один метод, выполняющий умножение двух чисел. Добавим в класс JNIHelloWorld метод
native int multiply(int a, int b);
Выполняем те же самые действия, описанные выше по генерации заголовков. Как видим, сгенерировалось следующее
/* * Class: ru_forwolk_test_JNIHelloWorld * Method: multiply * Signature: (II)I */ JNIEXPORT jint JNICALL Java_ru_forwolk_test_JNIHelloWorld_multiply (JNIEnv *, jobject, jint, jint);
Реализуем метод в JNIHelloWorld.cpp
JNIEXPORT jint JNICALL Java_ru_forwolk_test_JNIHelloWorld_multiply (JNIEnv *, jobject, jint a, jint b) { return a * b; }
Опять же произведем описанные выше действия по подтягиванию библиотеки, добавим строку в main по выводу результата произведения двух чисел и запустим
public static void main(String[] args) { JNIHelloWorld p = new JNIHelloWorld(); System.out.println(p.multiply(2, 2)); p.printHelloWorld(); }
Что получаем в консоли
4 Hello world! Process finished with exit source 0
Заключение
Мы с вами рассмотрели возможность Java использовать код, написанный на C/C++. Это можно применять в различных целях, например, для увеличения скорости исполнения кода, для защиты кода от прямого вмешательства и для прочих целей. Очень надеюсь, что данная статья поможет вам разобраться в основах JNI.
Весь код я выложил в открытый доступ. В директории cpp разместил класс C++ без лишних файлов проекта Clion.
Дополнительная литература
Также для большего кругозора по данной теме рекомендую обратить внимание на следующие статьи:
JNI, загрузка нативных библиотек. Меняем java.library.path на лету
Дорог ли native метод? «Секретное» расширение JNI
