JNI взаимодействие Java с C/C++

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

Однако иногда без использования вспомогательных средств создать полноценную программу согласно техническим требования невозможно. Например, необходимо воспользоваться кодом, который обеспечивает низкоуровневый доступ к «железу» компьютера, на котором выполняется программа. В языке программирования Java, который по своей идеологии является многоплатформенным, средства низкоуровневого доступа к аппаратной части просто-напросто не предусмотрены. С другой стороны, к моменту появления языка Java в мире программирования уже существовали колоссальные «залежи» программ и библиотек, позволяющих решать практически любые задачи, начиная от математических вычислений и заканчивая управлением сложными системами. Естественно, не замечать это богатство было бы просто неразумно.

Разработчики Java включили в язык возможность обращения из приложений к программам, реализованным на других языках программирования с использованием так называемых native-методов. Подсистема Java, реализующая данную возможность, называется JNI (Java Native Interface – интерфейс обращения к native-методам). К native методам следует прибегать в том случае, когда необходимо воспользоваться сторонней dll библиотекой, чтобы ускорить критичный алгоритм за счет оптимизированного кода на C/С++ или ассемблере. Например, для обработки потокового медиа, сжатия, шифрования и т.п.

Зачастую native-методы просты: они не вызывают исключений, не создают новые объекты в heap'e, не обходят stack, не работают с handle'ами и не синхронизованы. Можно ли для них не делать лишних действий? В статье будет затронуты вопросы о недокументированных возможностях HotSpot JVM для ускоренного вызова простых JNI методов.

Процедуры, обеспечивающие связь native-метода с программой Java, зависят от применяемой операционной системы и языка программирования, на котором native-методы реализованы. В статье рассматривается связь языка Java с библиотеками DLL операционных систем семейства Microsoft Windows. Рассмотрим простой вариант использования JNI. Пример показывает насколько несложно использовать данный механизм.

Пример использования JNI в java приложениях

Допустим, необходимо создать dll библиотеку, методы которой получают и обрабатывают текстовые и целочисленные значения. Результат обработки возвращается также в текстовом и целочисленном виде. Для решения данной задачи создаем проект jnicall-desk, в который включаем обычный класс JNICall.java с описанием методов. Реализация методов будет выполнена в библиотеке dll. На следующем скриншоте приведена структура проекта. Класс JNICallTest.java будет использоваться для тестирования методов библиотеки dll.

Листинг класса JNICall.java

Класс JNICall.java включает три метода, при описании которых указываем модификаторы native. Для загрузки библиотеки dll (JNICall.dll) используется системный вызов loadLibrary(). Необходимо отметить, что наименование загружаемой библиотеки указывается без расширения «.dll».

package com.example;

public class JNICall {
    static {
        System.loadLibrary("JNICall");
    }
    public native int    doMultiply(int    val1, int    val2); 
    public native String doCombine (String str1, String str2); 
    public native String getMessage(String message); 
}

Класс располагается в пакете com.example, и на это следует обратить пристальное внимание. Это очень важный момент, поскольку наименование пакета будет использовано в наименованиях методов в коде С/С++. Далее перемещать данный класс в другой пакет не следует; иначе не будет загружаться динамическая библиотека.

В проекте классы .java компилируются в директорию bin, где «раскладываются» по пакетам (поддиректориям). После компиляции класса JNICall получаем в проекте файл com.example.JNICall.class. Для создания файла с заголовком для C/C++ обработаем класс утилитой javah, которая создаст файл «com_example_JNICall.h».


jnicall-desk> "C:\Program Files\Java\jdk1.8.0_131\bin"\javah -classpath bin com.example.JNICall
 

Параметр classpath ссылается на директорию bin (-classpath bin). Для класса JNICall приводится полное имя : [package].[Class.name].

Листинг com_example_JNICall.h

В результате выполнения команды javah в корневой директории проекта (где была выполнена команда) будет создан файл com_example_JNICall.h следующего вида:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_JNICall */

#ifndef _Included_com_example_JNICall
#define _Included_com_example_JNICall
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_JNICall
 * Method:    doMultiply
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_example_JNICall_doMultiply
  (JNIEnv *, jobject, jint, jint);

/*
 * Class:     com_example_JNICall
 * Method:    doCombine
 * Signature: (Ljava/lang/String;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_example_JNICall_doCombine
  (JNIEnv *, jobject, jstring, jstring);

/*
 * Class:     com_example_JNICall
 * Method:    getMessage
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_JNICall_getMessage
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

Следует обратить внимание на 2-ю строчку, в которой выполняется импорт файла <jni.h>. Месторасположение данного файла следует искать в JDK в директории include. На моем компьютере он располагается в директории
"C:\Program Files\Java\jdk1.8.0_131\include". Файл "jni.h" потребовал дополнительно "jni_md.h" из поддиректории win32
("C:\Program Files\Java\jdk1.8.0_131\include\win32").

Проект создания dll

Для создания библиотеки JNICall.dll был использован CodeBlocks, который «легко» устанавливается на компьютер. При создании проекта выбран тип "Dynamic Link Library", в который включен файл com_example_JNICall.h и добавлен файл com_example_JNICall.cpp. На следующем скриншоте представлен проект создания библиотеки из двух файлов.

Редактировать заголовок не следует; его необходимо включить в файл CPP (com_example_JNICall.cpp), который представлен ниже :

#include "com_example_JNICall.h"
#include <string.h>

JNIEXPORT jint JNICALL Java_com_example_JNICall_doMultiply (
                JNIEnv * jenv, jobject jobj, jint val1, jint val2)
{
    return val1 * val2;
}

JNIEXPORT jstring JNICALL Java_com_example_JNICall_doCombine(
            JNIEnv * jenv, jobject jobj, jstring str1, jstring str2)
{
    const char *string1 = jenv->GetStringUTFChars(str1, 0);
    const char *string2 = jenv->GetStringUTFChars(str2, 0);
    char string3[512] = "";
    strcat(string3, string1);
    strcat(string3, string2);

    jenv->ReleaseStringUTFChars(str1, string1);
    jenv->ReleaseStringUTFChars(str2, string2);

    jstring result = jenv->NewStringUTF(string3);
    return result;
}

JNIEXPORT jstring JNICALL Java_com_example_JNICall_getMessage (
                             JNIEnv * jenv, jobject jobj, jstring str)
{
    const char *string = jenv->GetStringUTFChars(str, 0);
    char string3[255] = "Get text : ";
    strcat(string3, string);
    jenv->ReleaseStringUTFChars(str, string);

    jstring result = jenv->NewStringUTF(string3);
    return result;
}

В коде класса com_example_JNICall.cpp следует обратить внимание на обработку строк. Целочисленные переменные обычно не так сложны. Текстовые же значения нужно «вытащить» из Unicode в обычный char.

Метод doMultiply возвращает значение произведения двух чисел. Метод doCombine - создает одну строку из двух строк. Метод getMessage к текстовой строке "Get text : " добавляет строку, полученную в качестве параметра.

Класс тестирования JNI

Обратите внимание, что в проекте jnicall-desk созданная библиотека располагается в поддиректории "dll". Чтобы JVM могла найти эту библиотеку следует внести исправления в строку загрузки класса JNICall.java :

static {
//  System.loadLibrary("JNICall");
    System.loadLibrary("dll/JNICall");
}

Это работает для standalone-приложения, но это НЕПРАВИЛЬНО. А если динамическую библиотеку необходимо будет использовать в WEB-приложении, то данный подход уже не сработает, библиотека не будет найдена и Вы увидите исключение java.lang.UnsatisfiedLinkError. ПРАВИЛЬНЫЙ подход - это использование "java.library.path", который работает как в standalone приложении, так и WEB-приложении. Пример использования JNI в сервлете WEB-приложения, включающего ajax-вызов библиотеки JQuery, можно увидеть здесь.

Коротко о java.library.path

«java.library.path» это, если можно так выразиться, аналог classpath, но только не для Java классов и *.jar файлов, а для нативных библиотек. Т.е. является свойством, которое указывает JVM, где искать нативные библиотеки. Свойство «java.library.path» устанавливается перед запуском java-приложения (читай JVM), через глобальные system properties, или как ключ -Dname=value. После этого оно становится read-only.

На просторах Интернета можно найти 2 примера как изменить данное свойство в run-time. Вы можете посмотреть первоисточник откуда получен следующий метод setLibraryPath :

public void setLibraryPath(String path) throws NoSuchFieldException,
                                               SecurityException,
                                               IllegalArgumentException,
                                               IllegalAccessException	
{
    System.setProperty("java.library.path", path);

    // set sys_paths to null
    Field sysPathsField = ClassLoader.class.getDeclaredField("sys_paths");
    sysPathsField.setAccessible(true);
    sysPathsField.set(null, null);
}

Данный метод был протестирован, он рабочий, но мне, откровенно говоря, не очень понравился, поскольку обнуляет поля. В примере JNICallTest.java для тестирования JNI был использован второй метод addLibraryPath.

Листинг класса JNICallTest.test

В классе выполняется проверка наличия библиотеки dll в поддиректории. Если библиотека найдена, то добавляется путь к dll в переменную "usr_paths" загрузчика класса ClssLoader. Остальное все тривиально и прозрачно, особых комментариев не требуется. Помните, что в системный метод загрузки библиотеки loadLibrary в классе JNICall необходимо передать только ее наименование, т.е. использовать код : System.loadLibrary("JNICall").

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

package com.example;

import java.io.File;
import java.lang.reflect.Field;
import java.util.Arrays;

public class JNICallTest
{
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public JNICallTest()
    {
        File file = new File("dll/JNICall.dll");
        if (file.exists()) {
            try {
                String path = file.getAbsolutePath();
                int idx = path.indexOf(File.separatorChar 
                                            + "JNICall.dll");
                path = path.substring(0, idx);
                addLibraryPath(path);

                JNICall jl = new JNICall();

                System.out.println (
                            "1. multiply (11, 15) = " 
                                + jl.doMultiply(11, 15));
                System.out.println (
                            "2. concat ('Hello', ' world!') = " 
                                + jl.doCombine("Hello", " world!"));
                System.out.println (
                            "3. " + jl.getMessage("java message"));
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        } else {
            System.out.println("DLL NOT EXISTS");
        }
    }
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public void addLibraryPath(String pathToAdd) throws Exception
    {
        Field usrPathsField;
        usrPathsField = ClassLoader.class.getDeclaredField("usr_paths");
        usrPathsField.setAccessible(true);

        // get array of paths
        final String[] paths = (String[])usrPathsField.get(null);

        // check if the path to add is already present
        for (String path : paths) {
            if(path.equals(pathToAdd)) {
                return;
            }
        }
        // add the new path
        String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
        newPaths[newPaths.length-1] = pathToAdd;
        usrPathsField.set(null, newPaths);
    }
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public static void main(String[] args)
    {
        new JNICallTest();
        System.exit(0);
    }
}

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

1. multiply (11, 15) = 165
2. concat ('Hello', ' world!') = Hello world!
3. Get text : java message
 

Исходный код примера можно скачать в конце страницы.

Особенности использования JNI

Вызов native метода не бесплатен. Порой, накладные расходы на JNI оказываются даже больше, чем выигрыш в производительности. Это связано с тем, что вызов native-метода включают в себя:
  • создание stack frame;
  • перекладывание аргументов в соответствии с ABI;
  • оборачивание ссылок в JNI хендлы (jobject);
  • передачу дополнительных аргументов JNIEnv* и jclass;
  • захват и освобождение монитора, если метод synchronized;
  • «ленивую» линковку нативной функции;
  • трассировку входа и выхода из метода;
  • перевод потока из состояния in_Java в in_native и обратно;
  • проверку необходимости safepoint;
  • обработку возможных исключений.

Но зачастую native методы просты: они не вызывают исключений, не создают новые объекты в heap'e, не обходят стек, не работают с хендлами и не синхронизованы. Можно ли для них не делать лишних действий?

Далее в статье будут рассмотрены недокументированные возможности HotSpot JVM для ускоренного вызова простых JNI методов.
Рассмотрим для примера простой native метод, получающий на вход массив byte[] и возвращающий сумму элементов. Есть несколько способов работы с массивом в JNI:

1. GetByteArrayRegion – копирует элементы Java массива в указанное место нативной памяти.

JNIEXPORT jint JNICALL
Java_bench_Natives_arrayRegionImpl(JNIEnv* env, jclass cls, jbyteArray array) {
    static jbyte buf[1048576];
    jint length = (*env)->GetArrayLength(env, array);
    (*env)->GetByteArrayRegion(env, array, 0, length, buf);
    return sum(buf, length);
}

2. GetByteArrayElements – то же самое, только JVM сама выделяет область памяти, куда будут скопированы элементы. По окончании работы с массивом необходимо вызвать ReleaseByteArrayElements.

JNIEXPORT jint JNICALL
Java_bench_Natives_arrayElementsImpl(JNIEnv* env, jclass cls, jbyteArray array) {
    jboolean isCopy;
    jint length = (*env)->GetArrayLength(env, array);
    jbyte* buf = (*env)->GetByteArrayElements(env, array, &isCopy);
    jint result = sum(buf, length);
    (*env)->ReleaseByteArrayElements(env, array, buf, JNI_ABORT);
    return result;
}
3. GetPrimitiveArrayCritical

Здесь можно задаться вопросом:"Зачем делать копию массива?". Но ведь работать с объектами в Java Heap напрямую из натива нельзя, так как они могут перемещаться сборщиком мусора прямо во время работы JNI метода. Однако есть функция GetPrimitiveArrayCritical, которая возвращает прямой адрес массива в heap'e, но при этом запрещает работу GC до вызова ReleasePrimitiveArrayCritical.

JNIEXPORT jint JNICALL
Java_bench_Natives_arrayElementsCriticalImpl(JNIEnv* env, jclass cls, jbyteArray array) {
    jboolean isCopy;
    jint length = (*env)->GetArrayLength(env, array);
    jbyte* buf = (jbyte*) (*env)->GetPrimitiveArrayCritical(env, array, &isCopy);
    jint result = sum(buf, length);
    (*env)->ReleasePrimitiveArrayCritical(env, array, buf, JNI_ABORT);
    return result;
}

Метод Critical Native

А вот и секретный инструмент. Внешне он похож на обычный JNI метод, только с приставкой JavaCritical_ вместо Java_. Среди аргументов отсутствуют JNIEnv* и jclass, а вместо jbyteArray передаются два аргумента: jint length – длина массива и jbyte* data – «сырой» указатель на элементы массива. Таким образом, Critical Native методу не нужно вызывать дорогие JNI функции GetArrayLength и GetByteArrayElements – можно сразу работать с массивом. На время выполнения такого метода GC будет отложен.

JNIEXPORT jint JNICALL
JavaCritical_bench_Natives_javaCriticalImpl(jint length, jbyte* buf) {
    return sum(buf, length);
}

Как можно увидеть, в реализации не осталось ничего лишнего. Но чтобы метод мог стать Critical Native, он должен удовлетворять строгим ограничениям:

  • метод должен быть static и не synchronized;
  • среди аргументов поддерживаются только примитивные типы и массивы примитивов;
  • Critical Native не может вызывать JNI функции, а, следовательно, аллоцировать Java объекты или вызывать исключения;
  • метод должен завершаться за короткое время (самое главное), поскольку на время выполнения он блокирует GC.

Critical Natives задумывался как приватный API HotSpot'a для JDK, чтобы ускорить вызов криптографических функций, реализованных в native методе.

Важная особенность: JavaCritical_ функции вызываются только из горячего (скомилированного) кода, поэтому помимо JavaCritical_ реализации у метода должна быть еще и «запасная» традиционная JNI реализация. Впрочем, для совместимости с другими JVM так даже лучше.

Давайте, измерим, какова же экономия при работе с массивами разной длины: 16, 256, 4KB, 64KB и 1MB. Естественно, с помощью JMH.

@State(Scope.Benchmark)
public class Natives
{
    @Param({"16", "256", "4096", "65536", "1048576"})
    int length;
    byte[] array;

    @Setup
    public void setup() {
        array = new byte[length];
    }

    @GenerateMicroBenchmark
    public int arrayRegion() {
        return arrayRegionImpl(array);
    }

    @GenerateMicroBenchmark
    public int arrayElements() {
        return arrayElementsImpl(array);
    }

    @GenerateMicroBenchmark
    public int arrayElementsCritical() {
        return arrayElementsCriticalImpl(array);
    }

    @GenerateMicroBenchmark
    public int javaCritical() {
        return javaCriticalImpl(array);
    }

    static native int arrayRegionImpl(byte[] array);
    static native int arrayElementsImpl(byte[] array);
    static native int arrayElementsCriticalImpl(byte[] array);
    static native int javaCriticalImpl(byte[] array);

    static {
        System.loadLibrary("natives");
    }
}

Результаты

Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode)

Benchmark                         (length)   Mode   Samples         Mean   Mean error    Units
b.Natives.arrayElements                 16  thrpt         5     7001,853       66,532   ops/ms
b.Natives.arrayElements                256  thrpt         5     4151,384       89,509   ops/ms
b.Natives.arrayElements               4096  thrpt         5      571,006        5,534   ops/ms
b.Natives.arrayElements              65536  thrpt         5       37,745        2,814   ops/ms
b.Natives.arrayElements            1048576  thrpt         5        1,462        0,017   ops/ms
b.Natives.arrayElementsCritical         16  thrpt         5    14467,389       70,073   ops/ms
b.Natives.arrayElementsCritical        256  thrpt         5     6088,534      218,885   ops/ms
b.Natives.arrayElementsCritical       4096  thrpt         5      677,528       12,340   ops/ms
b.Natives.arrayElementsCritical      65536  thrpt         5       44,484        0,914   ops/ms
b.Natives.arrayElementsCritical    1048576  thrpt         5        2,788        0,020   ops/ms
b.Natives.arrayRegion                   16  thrpt         5    19057,185      268,072   ops/ms
b.Natives.arrayRegion                  256  thrpt         5     6722,180       46,057   ops/ms
b.Natives.arrayRegion                 4096  thrpt         5      612,198        5,555   ops/ms
b.Natives.arrayRegion                65536  thrpt         5       37,488        0,981   ops/ms
b.Natives.arrayRegion              1048576  thrpt         5        2,054        0,071   ops/ms
b.Natives.javaCritical                  16  thrpt         5    60779,676      234,483   ops/ms
b.Natives.javaCritical                 256  thrpt         5     9531,828       67,106   ops/ms
b.Natives.javaCritical                4096  thrpt         5      707,566       13,330   ops/ms
b.Natives.javaCritical               65536  thrpt         5       44,653        0,927   ops/ms
b.Natives.javaCritical             1048576  thrpt         5        2,793        0,047   ops/ms

Оказывается, для маленьких массивов стоимость JNI вызова в разы превосходит время работы самого метода! Для массивов в сотни байт накладные расходы сравнимы с полезной работой. Ну, а для многокилобайтных массивов способ вызова не столь важен – всё время тратится собственно на обработку.

Critical Natives – приватное расширение JNI в HotSpot, появившееся с JDK 7. Реализовав JNI-подобную функцию по определенным правилам, можно значительно сократить накладные расходы на вызов native метода и обработку Java-массивов в нативном коде. Однако для долгоиграющих функций такое решение не подойдет, поскольку GC не сможет запуститься, пока исполняется Critical Native.

Источник информации об особенностях использования JNI можно увидеть здесь.

Скачать пример

Рассмотренный на странице пример, включающий проект Eclipse с использованием JNI, можно скачать здесь (37.2 Кб). Библиотека JNICall.dll создана для windows x64.

Пример использования JNI в сервлете WEB-приложения можно увидеть здесь.

  Рейтинг@Mail.ru