Введение


Бывают моменты, когда в 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

Комментарии (7)


  1. staticlab
    11.04.2018 16:44

    А для чего у вас CMake, если вы компилируете "вручную"?


    1. Forwolk Автор
      11.04.2018 16:49

      Лично я использую Clion для удобства разработки на плюсах. А там без внесения изменений в CMakeList.txt не подхватываются зависимости. Конечно, можно и без этого.


  1. staticlab
    11.04.2018 16:52

    Теперь попробуйте создать из C++ Java-объект, вызвать в нём метод, а потом напишите ещё одну статью :)


  1. Akon32
    11.04.2018 17:30

    Не стыдно в 2018 про javah рассказывать?


  1. kovserg
    11.04.2018 23:56

    Еще есть одна засада придётся компилировать бинарники для разных платформ и выбирать что загружать при загрузке.
    {windows,linux,...}*{x86,x86_sse4,amd64,arm64avx,arm64avx512,arm,arm64,mips,mips64...}*{static,dynamic}*{cpu,gpu}


  1. daniillnull
    12.04.2018 09:57

    Вместо использования System.load, который требует полный путь до библиотеки, лучше использовать метод System.loadLibrary, который принимает название библиотеки. Она будет грузиться из java.library.path. (Его можно указать в параметрах JVM через -D)


  1. valery1707
    12.04.2018 10:14

    Вот есть библиотека JavaCpp позволяющая довольно просто обращатся к библиотекам на C++ без, на сколько я понимаю, внесения зависимостей на Java внутрь код библиотеки.