410013796724260
• Webmoney
R335386147728
Z369087728698
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. На моем компьютере он располагается в директории Проект создания 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 : " добавляет строку, полученную в качестве параметра. Примечание : в Windows создаваемая библиотека будет называться JNICall.dll; в Linux она должна иметь наименование libJNICall.so, т.е. обязательно начинаться с префикса 'lib'. Несмотря на то, что в Linux наименование библиотеки .so имеет префикс, при загрузке в LoadLibrary префикс не используется. Класс тестирования 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-метода включают в себя:
Но зачастую native методы просты: они не вызывают исключений, не создают новые объекты в heap'e, не обходят стек, не работают с хендлами и не синхронизованы. Можно ли для них не делать лишних действий? Далее в статье будут рассмотрены недокументированные возможности HotSpot JVM для ускоренного вызова простых 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; } Здесь можно задаться вопросом:"Зачем делать копию массива?". Но ведь работать с объектами в 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, он должен удовлетворять строгим ограничениям:
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-приложения можно увидеть здесь. |