Взаимодействия с С/С++, jni

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

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

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

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

Таким образом, к native методам следует прибегать в том случае, когда необходимо воспользоваться сторонней DLL библиотекой. В других случаях, чтобы ускорить критичный алгоритм за счет оптимизированного кода на C или ассемблере. Например, для обработки потокового медиа, для сжатия, шифрования и т.п.

В этой статье речь пойдет о внутренней реализации и вопросах практического применения JNI.

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

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

Обращение к native-методам

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

Рассмотрим простой вариант использования JNI. Пример показывает насколько несложно использовать этот механизм.

Создаем обычный java класс. Но тот метод, который входит в библиотеку dll, описываем как native. Кроме этого используем системный вызов loadLibrary(), который загружает нашу dll.

public class JniTest {
    static { 
        System.loadLibrary("JniTest");
    } 
    // следует обратить внимание на слово native метода
    public native int showString(String message);
}

После компиляции нашего примера получим файл JniTest.class

javac JniTest.java

Для получения файла с заголовком для Cи обработаем наш класс утилитой javah. Эта утилита создаст .h файл. Писать файл следует без расширения.

javah -classpath . JniTest

Получаем файл JniTest.h следующего вида:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniTest */
 
#ifndef _Included_JniTest
#define _Included_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class:     JniTest
* Method:    showString
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_JniTest_showString
  (JNIEnv *, jobject, jstring);
 
#ifdef __cplusplus
}
#endif
#endif

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

#include <jni.h>
#include "JniTest.h"
 
JNIEXPORT jint JNICALL Java_JniTest_showString(JNIEnv * jenv, 
                                               jobject jobj,
                                               jstring message)
{
  const char *string = jenv->GetStringUTFChars(message, 0);
  printf("%s\n",string);
  jenv->ReleaseStringUTFChars(message, string);
 
  return 0;
}

В нем следует обратить внимание на обработку строк. Остальные переменные обычно не так сложны. А тут видим, что надо извлечь строку из Unicode в обычный char.

Теперь создадим dll для нашего проекта. Лучше было бы, чтобы в системе уже был прописан путь до компилятора. JAVA_HOME – это переменная среды, т.е. путь до директории с установленной java.

cl -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 -LD JniTest.cpp \
   -FeJniTest.dll

cl – это компилятро С++ из Visual C++. Можно использовать любой другой компилятор. Тут важно собрать наши исходники на Си для получения DLL. Результатом компиляции должна быть JniTest.dll. Кстати, для Unix-систем должен быть файл вида libJniTest.so. Такая особенность; нужен префикс lib.

Теперь создаем тестовый пример.

public class ShowJniTest
{
    public static void main(String[] args)
    {
        JniTest jt = new JniTest();
        jt.showString("Hello, world!");
        jt.showString("JNI is great!");
    }
}

Компилируем

javac -classpath . ShowJniTest.java

и запускаем

java -classpath . ShowJniTest

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

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

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

Недокументированные возможности HotSpot JVM

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

1. GetByteArrayRegion

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

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, которая возвращает прямой адрес массива в хипе, но при этом запрещает работу 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)         Mean   Mean error
b.Natives.arrayElements                 16     7001,853       66,532
b.Natives.arrayElements                256     4151,384       89,509
b.Natives.arrayElements               4096      571,006        5,534
b.Natives.arrayElements              65536       37,745        2,814
b.Natives.arrayElements            1048576        1,462        0,017
b.Natives.arrayElementsCritical         16    14467,389       70,073
b.Natives.arrayElementsCritical        256     6088,534      218,885
b.Natives.arrayElementsCritical       4096      677,528       12,340
b.Natives.arrayElementsCritical      65536       44,484        0,914
b.Natives.arrayElementsCritical    1048576        2,788        0,020
b.Natives.arrayRegion                   16    19057,185      268,072
b.Natives.arrayRegion                  256     6722,180       46,057
b.Natives.arrayRegion                 4096      612,198        5,555
b.Natives.arrayRegion                65536       37,488        0,981
b.Natives.arrayRegion              1048576        2,054        0,071
b.Natives.javaCritical                  16    60779,676      234,483
b.Natives.javaCritical                 256     9531,828       67,106
b.Natives.javaCritical                4096      707,566       13,330
b.Natives.javaCritical               65536       44,653        0,927
b.Natives.javaCritical             1048576        2,793        0,047

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

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

Первоисточник статьи http://habrahabr.ru/post/222997

  Рейтинг@Mail.ru