Перейти к содержимому

Jni java что это

  • автор:

JNI: объединяем C и Java

Java Native Interface (JNI) — стандартный механизм для запуска кода, который написан на языках С/С++ или Ассемблера, и скомпонован в виде динамических библиотек, позволяет не использовать статическое связывание. Это даёт возможность вызова функции С/С++ из программы на Java, и наоборот.

Если говорить кратко, JNI — механизм, связывающий Java и C/C++ в одно целое.

Как пользоваться: Java

Со стороны Java нам нужно описать класс, загружающий динамическую библиотеку и имеющий в себе описание функций, расположенных в библиотеке:

1
2
3
4
5
6
System.loadLibrary("Library"); // Будет загружать Library.o или Library.dll в зависимости от ОС
.
// Данные функции объявлены с модификатором native
// Это значит, что они расположены в динамической библиотеке
public static native void libraryFunc();
public static native int libraryFunc(int x);

Как пользоваться: С

Следующим этапом необходимо создать заголовочный файл библиотеки для дальнейшей работы с динамической библиотекой:

javac -g Main.java
javah -jni Main

После выполнения данных команд в терминале мы получим файл Main.h примерно такого содержания:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class Main */
#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Main
* Method: libraryFunc
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Main_libraryFunc
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif

Как видим, в нашем заголовочном файле уже описана функция Java_Main_libraryFunc , которая расположена в классе Main и называется libraryFunc . В динамической библиотеке функция принимает дополнительные аргументы — окружение и класс.
Переходим к файлу Main.c , в котором опишем реализацию функции Java_Main_libraryFunc :

1
2
3
4
5
6
7
#include
#include
#include "Main.h"
JNIEXPORT void JNICALL Java_Main_libraryFunc(JNIEnv *env, jobject obj) {
printf("Native code executed!\n");
}

Теперь этот небольшой модуль нужно скомпилировать в динамическую библиотеку:

gcc -std=c99 -Wl,--add-stdcall-alias -I"\include" -I"\include\win32" -shared -o Library.dll

Также нужно иметь ввиду, что разрядность библиотеки должна быть такой же, как и разрядность JVM.

Связываем вместе

После того, как библиотека скомпилирована, достаточно загрузить ее и вызвать нужную функцию:

System.loadLibrary("Library");
libraryFunc();

Заключение

JNI — отличное средство для более близкого взаимодействия с системой. Все, что нельзя сделать средствами чистой Java, можно сделать на С/С++ и вызвать из Java. Единственным вопросом остается производительность — в каких случаях стоит обратиться к нативному коду, а в каких достаточно воспользоваться стандартными средствами.

Ссылки

  • Java Native Interface
  • JNI — Tutorial

JNI Часть 1: Введение

Всем привет! Меня зовут Роман Аймалетдинов и я разрабатываю клиентское приложение Ситимобил. Решил написать небольшую серию из трех статей по JNI, так как технология используется редко, но иногда она бывает очень полезной. Несмотря на то, что я разрабатываю классические приложения под Android, иногда хочется посмотреть технологии рядом со своей специализацией.

Что такое JNI?

JNI — это интерфейс, позволяющий из Java вызывать нативные функции. Например, метод С++, который что-нибудь делает. Допустим, мы пишем большую программу на простом и любимом Java или Kotlin, и нужно реализовать задачу коммивояжера для нашего клиента. Или мы пишем генетический алгоритм, который ищет что-то в большом объёме данных, и так уж вышло, что у нас есть замечательная реализация на С++. Особенно часто я слышу про JNI в gamedev- и в automotive-проектах. Однажды я работал в таком, будучи ещё интерном, и, точно не помню, но в общих чертах на С++ было много низкоуровневого кода по обработке потока данных, получаемого со спутника. JNI позволяет вручную управлять памятью. Можно написать фрагмент кода на C/С++, и при необходимости дёргать нативный метод и получать из него результат вычисления.

И ещё по поводу производительности: мы все знаем, что С/С++ побыстрее будут, и тогда получается, что мы сможем оптимизировать наши алгоритмы на Java, переписав их на C++. (спойлер: не надо, скорее всего, будет работать медленнее). Некоторые авторы в интернете говорят про увеличение производительности, поэтому в своей третьей статье я приведу тесты JNI/NDK и мы сможем сверить производительность.

Также для Android есть инструмент NDK, который всё про то же — запуск нативного кода и увеличение производительности отдельных алгоритмов.

NDK — Native Development Kit (для Android).
JNI — Java Native Interface (общий интерфейс).

Шаги для запуска кода из JNI

Шаг 1. Создадим новый проект ex: JNI_aymaletdinov_roman.

Шаг 2. Создадим класс, у которого будут native-методы, и назовём его AwesomeLib.

public class AwesomeLib < static < System.loadLibrary("nativeLib"); >>

System.loadLibrary(«nativeLib»); — это статический метод, который загружает native-библиотеку из файловой системы в память и делает её экспортированные функции доступными для нашего Java-кода. То есть мы загружаем файл на С/С++, который будет выполнять сложные вычисления. Однако nativeLib мы ещё не создали, сделаем это позже.

Шаг 3. Следующим шагом нам необходимо объявить метод, помеченный ключевым словом native. Он не имеет реализации в Java, напишем её на C++.

public class AwesomeLib < static < System.loadLibrary("nativeLib"); >public native void helloHabr(); //

Шаг 4. Вызовем из Main нашу библиотеку, которая что-то делает в native-коде .

public class Main < public static void main(String[] args) < System.out.println("Hey! This is Java!"); AwesomeLib nativeLib = new AwesomeLib(); nativeLib.helloHabr(); >>

Шаг 5. На Java мы всё сделали! Теперь начинается что-то непривычное: нужно обязательно установить MinGW-w64, иначе не на чем будет компилировать нативный код. При установке выберите x86-64 и пропишите bin переменные среды path.

Шаг 6. Затем необходимо написать реализацию нашего нативного метода на C++. В этом языке объявление и реализация обычно хранятся в файлах .h и .cpp соответственно. Открываем консоль; переходим в папку, в которой будут лежать native-файлы; генерируем заголовок для натива командой javac -h . absolutePath/MyClass.java.

После выполнения команды будет создано два файла: .h и .class .

Декомпилированный .class мало интересен, а вот .h — что-то не из нашего JVM-мира. На самом деле всё очевидно: наш метод сгенерировался таким, каким он будет в С++. При этом имя функции генерируется автоматически с использованием полного имени пакета, класса и метода. Кроме того, мы получаем два параметра, передаваемых нашей функции:

  • указатель на текущий JNIEnv;
  • Java-объект (AwesomeLib), к которому привязан метод.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class nativelib_AwesomeLib */ #ifndef _Included_nativelib_AwesomeLib #define _Included_nativelib_AwesomeLib #ifdef __cplusplus extern "C" < #endif /* * Class: nativelib_AwesomeLib * Method: sayHello * Signature: ()V */ JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_helloHabr(JNIEnv *, jobject); #ifdef __cplusplus >#endif #endif

Шаг 7. Следующий шаг — создание .cpp . То есть мы сами создаём файл с расширением .cpp и копируем из .h контракт метода. Я назвал файл тем же именем, что и Java-класс: AwesomeLib.cpp .

#include "nativelib_AwesomeLib.h" #include JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_helloHabr(JNIEnv* env, jobject thisObject)

Шаг 8. Затем наш нативный код надо скомпилировать. Когда мы выполним в консоли команду g++ -c -I»C:\Program Files\Java\jdk-12.0.1\include» -I»C:\Program Files\Java\jdk-12.0.1\include\win32″ AwesomeLib.o.cpp -o AwesomeLib.o , появится ещё один файл: .o .

Смотрим на дерево проекта и видим новый файл:

Шаг 9. Осталось совсем немного. Теперь нужно сгенерировать .dll с названием нативной библиотеки, которое мы указали в статике в Java. В консоли выполняем команду: g++ -shared -o nativeLib.dll AwesomeLib.o -Wl,—add-stdcall-alias .

Смотрим, в IDE появился ещё один сгенерированный файл.

Шаг 10. Всё! Хочется запустить нашу программу с нативным кодом, но IDEA сейчас этого сделать не сможет. Она ничего не знает про С++, следовательно, для запуска воспользуемся консолью:
java -cp . -Djava.library.path=»[Абсолютный путь до папки, где лежит dll]» Main.java

Обратите внимание, что я перед запуском выполнил команду cd .. для того, чтобы быть в той же папке, где лежит запускаемый мною файл Main .

Как запустить проект из IDEA, а не через консоль?

Так как IDEA по умолчанию не знает ни о каких .dll , она не сможет запустить наш проект по нажатию на треугольник “Run”. Чтобы запустить проект, нужно прописать VM options в меню “Edit configurations”.

У нас появится поле VM options, и мы добавляем Djava.library.path full-width «>

Вот теперь точно всё! Можно запускать проект из IDEA и радоваться жизни.

Заключение

Краткий список шагов, чтобы было проще вспоминать тем, кто знает, читал и делал, но что-то подзабыл и нужна шпаргалка:

  • Установить MinGW-w64. При установке выбрать x86-64 и прописать bin переменные среды path.
  • Создать NativeLib.java с native-методами.
  • Перейти в консоли в папку (ex: src), в которую хотите сохранить сгенерированные файлы.
  • Выполнить javac -h . AwesomeLib.java .
  • Выполнить g++ -c -I»C:\\Program Files\\Java\\jdk-12.0.1\\include» -I»C:\\Program Files\\Java\\jdk-12.0.1\\include\\win32″ AwesomeLib.cpp -o AwesomeLib.o .
  • Выполнить g++ -shared -o nativeLib.dll AwesomeLib.o -Wl,—add-stdcall-alias .
  • Запустить программу: java -cp . -Djava.library.path=”Путь до папки с .dll» Main (или через студию).

Если вы внесли изменения в .cpp , то заголовок пересоздавать не нужно, но если вы изменили контракт метода, то придётся начинать с этапа javac -h . . .

В следующей части я расскажу про более сложные методы в JNI, про передачу List и вызов Java из C++.

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 /* 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-ю строчку, в которой выполняется импорт файла . Месторасположение данного файла следует искать в 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 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

  • создание 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)

Как можно увидеть, в реализации не осталось ничего лишнего. Но чтобы метод мог стать 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() 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-приложения можно увидеть здесь.

B: Java Native Interface (JNI)

Язык Java и его стандартные API самодостаточны для написания полноценного приложения. Но в некоторых случаях Вы должны использовать не-Java код, например, в случае вызова функций специфичных для операционной системы, доступа к специальным аппаратным устройствам, использовании уже существующего не-Java кода или создании критичных ко времени выполнения частей кода.

Для взаимодействия с не-Java кодом требуется специальная поддержка в компиляторе и Виртуальной Машине, и дополнительные средства отображения Java кода в не-Java код. Стандартным решением для вызова не-Java кода, который обеспечивает JavaSoft, называется ava Native Interface, который был введен в этом приложении. Это не глубокая трактовка, и в некоторых случаях вы должны принимать на себя изучение части знаний относительно концепции и техники.

JNI достаточно богатый программный интерфейс позволяющий выполнять системные вызовы из приложений на Java. Данная возможность была добавлена в Java 1.1, устанавливая определенную степень соответствия с их эквивалентами в Java 1.0, native method interface (NMI). NMI имеет спроектированные характеристики которые делают его неподходящими для адаптации на всех виртуальных машинах. По этой причине, будущие версии языка могут не поддерживать NMI, и они не будут здесь описаны.

В настоящий момент JNI разработана как интерфейс с собственными методами написанными только на С или С++. Используя JNI ваши собственные методы могут:

  • Создавать, проверять и обновлять Java объекты (включая массивы и типы String )
  • Вызывать Java методы
  • Ловить и выбрасывать исключения
  • Загружать классы и получать информацию о классах
  • Выполнять проверку типов во время исполнения

Таким образом, практически все, что вы можете делать с классами и объектами в Java вы можете выполнить с собственными методами.

Вызов собственных методов

Мы начнем с простого примера: Java программы, вызывающей собственные метод, который в свою очередь вызывает функцию printf( ) стандартной библиотеки С:

Первый шаг заключается в написании Java кода с описанием прототипа собственного метода и его аргументов:

//: appendixb:ShowMessage.java public class ShowMessage < private native void ShowMessage(String msg); static < System.loadLibrary("MsgImpl"); // Linux hack, если в вашей среде не установлен // путь к библиотеке: // System.load( // "/home/bruce/tij2/appendixb/MsgImpl.so"); > public static void main(String[] args) < ShowMessage app = new ShowMessage(); app.ShowMessage("Generated with JNI"); > > ///:~

Описание собственного метода следует за блоком static, который вызывает System.loadLibrary( ) (который вы можете вызывать в любое время, но приведенный стиль более приемлемый). System.loadLibrary( ) загружает DLL в память и связывает ее. DLL должна быть в каталоге системных библиотек. Расширение файла будет автоматически добавлено JVM в зависимости от типа операционной системы.

В приведенном выше коде вы можете также видеть вызов метода System.load( ) , который закоментирован. Путь, указанный здесь, это абсолютный путь, а не относительный с учетом переменной окружения. Использование переменной окружения, естественно, лучшее и более портативное решение, но если вы не можете закомментировать вызов loadLibrary( ) и раскомментировать эту строку, отрегулировав путь к вашему собственному директорию.

javah: генератор заголовочных файлов на С

Теперь скомпилируйте ваш исходный файл на Java и запустите javah с полученным файлом .class в качестве параметра, указав ключ —jni (это выполнится автоматически за вас с помощью makefile, присутствующим в исходном коде для книги ):

javah —jni ShowMessage

javah читает файл Java класса, и для каждого описания собственного метода генерирует прототип функции в заголовочном файле С или С++. Ниже приведен результат вызова javah для нашего случая (слегка измененный, чтобы уместиться в книгу):

/* НЕ РЕДАКТИРУЙТЕ ЭТОТ ФАЙЛ - он сгенерирован машиной */ #include /* Заголовок для класса ShowMessage */ #ifndef _Included_ShowMessage #define _Included_ShowMessage #ifdef __cplusplus extern "C" < #endif /* * Class: ShowMessage * Method: ShowMessage * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_ShowMessage_ShowMessage (JNIEnv *, jobject, jstring); #ifdef __cplusplus > #endif #endif

Как можно видеть с помощью препроцессорной директивы #ifdef __cplusplus данный файл может быть откомпилирован как С так и С++ компилятором. Первая директива #include включает jni.h, заголовочный файл, который кроме всего прочего, определяет типы, используемые далее. JNIEXPORT и JNICALL — это макросы который расширены чтобы соответствовать платформо-зависимым директивам. JNIEnv, jobject и jstring определение JNI типов данных, который скоро будут описаны .

Искажение имен и сигнатура функций

JNI использует преобразование имен (называемое name manglingискажением имен) собственных методов. Это важно, так как это является частью механизма, с помощью которого виртуальная машина компонует Java вызовы собственных методов. В основном все собственные методы начинаются со слова «Java», за которым слкдует имя класса в котором присутствует собственный вызов Java, следом идет имя Java метода. Символ подчеркивания используется как разделитель. Если собственный Java метод перекрывается, то к имени также добавляется сигнатура функции; вы можете видеть собственную сигнатуру в комментариях предшествующих прототипу. Дополнительную информацию об искажении имен и сигнатурах собственных методов можно найти в документации по JNI .

Реализация вашей DLL

В данном случае, все что вам нудно сделать — это написать файл с исходным код на C или C++ включающий заголовок сгенерированный утилитой javah и реализацию собственных методов, затем откомпилировать его и создать библиотеку динамической компоновки. Данная часть платформо — зависимая. Нижеприведенный код компонуется в файл называемый MsgImpl.dll для Windows или MsgImlp.so для UNIX/Linux (makefile включенный в список файлов с исходными текстами содержит соответствующие команды, он доступен на CD-ROM поставляемым вместе с данной книгой, либо его можно загрузить с сайта www.BruceEckel.com).

//: appendixb:MsgImpl.cpp //# Проверено с VC++ & BC++. Включенный путь //# должен быть изменен для нахождения JNI заголовков. Смотрите //# makefile для этой главы (в загруженном исходном коде) //# для примера. #include #include #include "ShowMessage.h" extern "C" JNIEXPORT void JNICALL Java_ShowMessage_ShowMessage(JNIEnv* env, jobject, jstring jMsg) < const char* msg=env->GetStringUTFChars(jMsg,0); printf("Thinking in Java, JNI: %s\n", msg); env->ReleaseStringUTFChars(jMsg, msg); > ///:~

Аргументы, передаваемые в собственные методы — это доступ к коду на Java. Во-первых, согласно JNIEnv, содержит все привязки которые позволяют вам выполнить обратные вызовы JVM. (Мы рассмотрим это в следующей разделе). Во-вторых, аргументы имеют разное толкование в зависимости от типа метода. Для не статических (static) методов, таких как приведенный выше пример, второй аргумент соответствует указателю “this” в С++ и похож на this в Java: он ссылается на объект вызвавший собственный метод. Для статических методов он ссылается на объект Class, в котором метод реализован .

Оставшиеся аргументы представляют собой объекты Java передаваемые в вызов собственного метода. Примитивы передаются аналогичным образом, по значению.

В следующем разделе мы рассмотрим данный код с точки зрения доступа и управления JVM из собственного метода.

Доступ к JNI функциям: аргументы JNIEnv

Под функциями JNI подразумеваются функции, которые взаимодействуют с JVM из собственных методов. Как вы могли видеть в приведенном выше примере, каждый собственный метод JNI получает специальный аргумент в качестве первого параметра: это и есть JNIEnv аргумент, который является указателем на специальную структура данных типа JNIEnv_. Один из элемент структуры данных JNI является указателем на массив генерируемый JVM. Массив состоит из указателей на JNI функции. JNI функции могут быть вызваны из собственного метода путем разыменования данных указателей (это проще чем кажется). Каждая JVM обеспечивает собственной реализацией JNI функций, но их адреса всегда остаются на определенном месте .

С помощью аргументов JNIEnv программе доступно большое количество функций. Эти функции могут быть сгруппированы в следующие категории:

  • Получение информации о версии
  • Выполнение операций с классами и объектами
  • Использование глобальных и локальных ссылок на Java объекты
  • Доступ к полям ссылки и статическим полям
  • Вызов ссылочных методов и статических методов
  • Выполнение операций со строками и массивами
  • Генерация и перехват Java исключений

Количество JNI функций достаточно большое и не может быть описано здесь. Вместо этого покажем логическое обоснование использования этих функций. Более детальная информация находиться в документации по JNI.

Если посмотреть на заголовочный файл jni.h можно видеть что внутри условий препроцессора #ifdef __cplusplus структура JNIEnv_ определена как класс когда компилируется С++ компилятором. Данный класс содержит несколько функций, которые позволяют вам получить доступ к JNI функциям через простой и знакомый синтаксис. В качестве иллюстрации приведем строку кода из рассмотренного примера :

env->ReleaseStringUTFChars(jMsg, msg);

также может быть вызвано из С следующим образом:

(*env)->ReleaseStringUTFChars(env, jMsg, msg);

Вы заметили, что строка на С гораздо сложнее (что естесственно) — вам необходимо двойное разыменование указателя на env. Вы также должны передать этот указатель в качестве первого параметра при вызове JNI функции. Примеры данного приложения используют С++ стиль .

Доступ к Java строкам

В качестве примера доступа к JNI функции рассмотрим код MsgImрl.cpp. Здесь аргумент env типа JNIEnv используется для доступа к типам String в Java. Строки в Java хранятся в формате Unicode, поэтому если вы хотите передать их в качестве параметра в функцию, которая Unicode не поддерживает (printf() например), необходимо вначале преобразовать строку в ASCII с помощью GetStringUTFChars(). Данная функция принимает String и преобразует в строку в формате UTF-8. (Для хранения ASCII достаточно 8 бит и 16 бит для Unicode. Если исходная строка 8-ми битовая ASCII, то результирующая строка будет также ASCII.)

GetStringUTFChars( ) одна из функций-членов JNIEnv. Для доступа к JNI функции мы используем типичный C++ синтаксис для вызова функции-члена несмотря на указатель. Можно использовать приведенную выше форму для доступа ко всем JNI функциям .

Передача и использование Java объектов

В предыдущем примере мы передавали String в собственный метод. Можно также передавать ваши собственные Java объекты в собственные методы. Внутри вашего собственного метода вы имеете доступ к полям и методам полученного объекта .

Для передачи объектов используйте обычный Java синтаксис когда описываете собственные методы. В следующем примере MyJavaClass имеет одно public поле и один public метод. В классе UseObject объявлен собственный метод, который принимает объекты класса MyJavaClass. Для отображения того, что собственный метод использует эти аргументы передадим поле public, вызовем собственный метод и, затем, распечатаем это поле .

//: appendixb:UseObjects.java class MyJavaClass < public int aValue; public void divByTwo() < aValue /= 2; >> public class UseObjects < private native void changeObject(MyJavaClass obj); static < System.loadLibrary("UseObjImpl"); // Linux hack, если в вашей среде не установлен // путь к библиотеке: // System.load( //"/home/bruce/tij2/appendixb/UseObjImpl.so"); > public static void main(String[] args) < UseObjects app = new UseObjects(); MyJavaClass anObj = new MyJavaClass(); anObj.aValue = 2; app.changeObject(anObj); System.out.println("Java: " + anObj.aValue); > > ///:~

После компиляции кода и использования javah можно реализовать собственные методы. В примере ниже, как только поле и ID метода получены они доступны чере JNI функции .

//: appendixb:UseObjImpl.cpp //# Проверено с VC++ & BC++. Включенный путь //# должен быть изменен для нахождения JNI заголовков. Смотрите //# makefile для этой главы (в загруженном исходном коде) //# для примера. #include extern "C" JNIEXPORT void JNICALL Java_UseObjects_changeObject( JNIEnv* env, jobject, jobject obj) < jclass cls = env->GetObjectClass(obj); jfieldID fid = env->GetFieldID( cls, "aValue", "I"); jmethodID mid = env->GetMethodID( cls, "divByTwo", "()V"); int value = env->GetIntField(obj, fid); printf("Native: %d\n", value); env->SetIntField(obj, fid, 6); env->CallVoidMethod(obj, mid); value = env->GetIntField(obj, fid); printf("Native: %d\n", value); > ///:~

Игнорируя эквиваелент «this», функция С++ получает jobject, который является собственной частью Java объекта переданного нами из Java кода. Мы просто прочитали значение aValue, напечатали его, изменили, вызвали метод объекта divByTwo() и напечатали значение параметра еще раз.

Для доступа к полю или методу Java первоначально необходимо получить их дескриптор, используя GetFieldID() для полей и GetMethodID() для методов. Данные функции принимают объект класса, строку содержащую название элементов и строку с информацией о классах: тип данных поля или информацию с описанием для метода (подробности описаны в документации по JNI). Данные функции возвращают дескриптор, который потом используется для доступа к элементам.Данный подход может казаться запутанным, но ваши собственные методы не знают о внутренней компоновке Java объектов. Вместо этого, они должны обращаться к полям и методам через индексы, возвращаемые JVM.Это позволяет различным JVM реализовать различные сруктуры внутренних объектов, не влияя на ваши собственные методы .

Если запустить Java программу видно, что объекты передаваемые со стороны Java используют ваши собственные методы. Но что же передается в действительности? Указатель или значение Java? И что делает сборщик мусора при вызове собственных методов?

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

Поскольку данные ссылки создаются и потом уничтожаются при каждом вызове функции, вы не можете сделать локальную копию вашего собственного метода в static переменную. Если вам нужна ссылка, которая используется в течении вызова функции вам необходимо определить глобальную ссылку. Глобальная ссылка не создается JVM, но программист может создать глобальную ссылку вызовом специальных функций JVM. После создания глобальной ссылки вы отвечаете за время жизни и самого объекта. Глобальная ссылка (и объект к которому она относиться) должны находиться в памяти до тех пор пока программист явно не освободит память соответствующей JNI функцией. Это аналогично использованию malloc() и free() в С.

JNI и исключения в Java

С помощью JNI, Java исключения могут быть сгенерированы, перехвачены, распечатаны или вызваны повторно аналогично тому, как это делается в Java. Но при этом для работы с исключениями необходимо использовать специальные функции. Ниже приведен список JNI функций для обработки исключений :

  • Throw( )
    Выбрасывает существующий объект исключения. Используется в собственном объекте для повторного выбрасывания исключения.
  • ThrowNew( )
    Создает новый объект исключения и выбрасывает его.
  • ExceptionOccurred( )
    Определяет, было ли исключение уже выброшено, но еще не очищено.
  • ExceptionDescribe( )
    Печатает исключение и содержимое стека.
  • ExceptionClear( )
    Очищает рассматриваемое исключение.
  • FatalError( )
    Вызывает фатальную ошибку. Возврата нет.

Среди перечисленных вы не можете игнорировать ExceptionOccured( ) и ExceptionCleared( ). Большинство функций JNI способны генерировать исключения, кроме try блока у вас нет других возможностей отследить исключения, поэтому необходимо вызывать ExceptionOccured( ) после каждого вызова функции JNI для перехвата возможного исключения. При обнаружении исключения можно его перехватить и обработать (и, вероятно, сгенерировать повторно). Вы должны быть уверены однако, что исключение очищено. Это можно сделать в вашей функции вызовом ExceptionClear( ) или какой-либо другой функцией, если исключение вызвано повторно, но это должно быть сделано .

Вы должны быть уверены, что исключение очищено, потому что в противном случае вызов функции JNI будет непредсказуемым, пока исключение обрабатывается. Существует несколько функций JNI, которые можно вызывать во время обработки исключения, несомненно, все они являются функциями обработки исключения .

JNI и нити процесса

Поскольку Java поддерживает нити процессов, несколько нитей могут конкурировать в вызовах собственных методов. (Собственный метод может быть временно остановлен в середине выполнения в момент, когда другая нить процесса пытается обратиться к нему.) Поэтому вся ответственность за вызов собственного метода из нити лежит на программисте. В основном, имеется две возможости: описать собственный метод как synchronized или реализовать какую-либо другую стратегию внутри собственного метода, чтобы быть уверенным в правильном, конкурирующем изменении данных .

Кроме того, никогда нельзя передавать указатель на JNIEnv через нити, поскольку внутренняя структура, на которую они указывают, выделена на одно-нитевой основе и имеет смысл только в данной нити .

Использование существующего кода

Наиболее легкий метод реализовать собственные методы JNI — начать с написания прототипов собственных методов в Java классе, компиляции данного класса и запуске полученного .class файла используя javah. Но что делать если уже имеется большой код который хотелось бы вызывать из Java? Переименование всех вызовов функций в нашей DLL для соответствия именованиям JNI не самый реальный путь. Наиболее приемлемое решение заключается в написании оболочки для вызова функций оригинальной DLL. В этом случае Java код вызывает функции из новой DLL которая в свою очередь вызывает функции из оригинальной DLL. Данный путь не так уж бессмыслен, в большинстве случаев вам все равно придется сделать это, так как вам необходимо вызывать функции JNI в описании объектов до того как они будут использованы .

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

https://alkogolizm.vyvod-iz-zapoya-v-stacionare-samara11.ru/