суббота, 16 декабря 2017 г.

JNI для самых маленьких

Эта статья является простым – в стиле “шаг за шагом” – описанием использования JNI.
Минимум теории – максимум практики.

Предполагается, что читатель знаком с синтаксисом языка программирования Java, может самостоятельно установить jdk и – если это необходимо – интегрированную среду разработки и знает основы C/C++.


Используемые инструменты:
  1. jdk1.6
  2. Среда разработки на Java – IntelliJ Idea 10 Community Edition
  3. Компилятор для C – MIN GW
  4. Среда разработки для C/C++ - Code::Block 10

Операционная система: Windows XP Professional SP3

Принятые соглашения:
  1. JDK_HOME – папка инсталляции jdk;
  2. JVM – виртуальная java машина
Введение.

JNI (Java Native Interface) представляет собой способ обеспечить доступ коду, написанному на Java, к низкоуровневым возможностям, предоставляемым операционной системой.

Как правило, необходимость в этом возникает в следующих случаях:
  1. Существует работоспособный и проверенный код, написанный на С/C++, переписывать который на Java нерентабельно;
  2. Необходимую функциональность невозможно или сложно реализовать средствами Java;
  3. Существует возможность улучшения быстродействия кода при переписывании его на C/C++.

Так или иначе, если вы чувствуете необходимость использовать платформенно-ориентированные средства - эта статья для вас.


Шаг первый: Написать java-класс, содержащий native-методы.

В нашей статье мы напишем класс, который, используя JNI, печатает в консоль.
Исходный код класса:
package com.blogspot.toolkas;

public class Console {
    public static native void print(String string);
}
Как видим, компилятор не требует реализации метода, предваряемого ключевым словом “native” (скажем больше – нельзя скомпилировать класс, нативные методы которого имеют тело).

Если попытаться воспользоваться данным классом, например:
package com.blogspot.toolkas;

public class TestConsole {
    public static void main(String[] args) {
        Console.print("TEST");
    }
}

То мы получим следующую ошибку:
    Exception in thread "main" java.lang.UnsatisfiedLinkError:
    com.blogspot.toolkas.Console.print(Ljava/lang/String;)V
        at com.blogspot.toolkas.Console.print(Native Method)
        at com.blogspot.toolkas.TestConsole.main(TestConsole.java:5)
которая сообщает нам, что виртуальная машина Java не смогла обнаружить подходящей реализации метода Console.print.

Что ж, поможем ей в этом=)

Шаг второй: реализовать native-метод платформенно ориентированным способом.

Для реализации метода Console.print будем использовать язык Си.

а) Используя утилиту javah (которая находится в папке { JDK_HOME }/bin), сгенерируем заголовочный файл из скомпилированного класса ru.olympico.io.Console:

javah -classpath (класспас) -d (папка) com.blogspot.toolkas.Console

Не забываем, что для корректной работы утилиты javah в класспасе должен находиться скомпилированный класс Console (то есть после внесения изменений в исходный код – сначала компилируем – потом перегенерируем заголовочный файл).

Так или иначе, если утилита отработала корректно, на выходе мы должны получить что-то подобное:

    /* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>
/* Header for class ru_olympico_io_Console */

#ifndef _Included_com_blogspot_toolkas_io_Console
#define _Included_com_blogspot_toolkas_Console
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_blogspot_toolkas_Console
* Method: print
* Signature: (Ljava/lang/String;)V
*/

JNIEXPORT void JNICALL Java_com_blogspot_toolkas_Console_print
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}

#endif
#endif
Разберемся немного в сгенерированном файле. Первое что мы видим – это включение заголовочного файла jni.h.

Этот файл можно найти в папке {JDK_HOME}\include. Данный заголовочный файл содержит большое количество функций структур и макросов, которые позволят нам взаимодействовать с JVM из нативного кода.

Дальше идут знакомые каждому, кто писал код на C или C++ - так называемые стражи включения (_Included_com_blogspot_toolkas_Console), которые гарантируют, что тело заголовочного файла не будет включено в исходный код программы дважды и более раз.

И наконец прототип функции, реализацию которой мы и должны написать:

JNIEXPORT void JNICALL Java_com_blogspot_toolkas_Console_print
(JNIEnv *, jclass, jstring);

Отметим следующие интересные моменты:
  1. Чтобы обеспечить уникальность функции и однозначность связывания с методом, написанным на Java, генератор формирует имя функции, используя полный путь класса (включая пакет) и имя метода (случай перегруженных функций можете исследовать сами) ;
  2. Аргументы функции соответствуют аргументам метода (за исключением первых двух), поэтому можно спокойно их использовать;
  3. Указатель JNIEnv* представляет собой аргумент, обеспечивающий доступ к API JVM;
  4. jclass – это структура, которая представляет собой класс, содержащий вызываемый метод;
  5. существует определенное соответствие между типом передаваемых аргументов в Java и в C:
Язык Java Язык C

boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
String jstring


Эти и другие типы вы можете найти в jni.h.

Итак, перейдем к цели нашей статьи – реализуем метод print.
Напишем тело функции Java_com_blogspot_toolkas_Console_print в файле Console.c. Сгенерированный заголовочный файл назовем Console.h.

Console.c будет выглядеть примерно так:

#include <stdio.h>
#include "Console.h"

JNIEXPORT void JNICALL Java_ru_olympico_io_Console_print(JNIEnv* env, jclass clazz, jstring string) {
 const char* val = (*env)->GetStringUTFChars(env, string, NULL);
 printf(val);
 fflush(stdout);
 (*env)->ReleaseStringUTFChars(env, string, val);
}
Строчка
const char* val = (*env)->GetStringUTFChars(env, string, NULL);

конвертирует jstring в массив char-символов, для того, чтобы мы могли их распечатать.

printf(val); - осуществляет непосредственно печать массива символов в консоль.
fflush(stdout); - очищает буфер вывода в консоль.

И наконец
(*env)->ReleaseStringUTFChars(env, string, val);

Сообщает JVM, что платформенно-ориентированному коду больше не требуется доступ к массиву val;

Шаг третий: компиляция

Теперь, необходимо скомпилировать dll и сообщить JVM о ее существовании.
Компилятор MinGW под Windows делает это следующим образом:

gcc -I {JDK_HOME}/include -I {JDK_HOME}/include/win32 -shared -Wl,--add-stdcall-alias -o Console.dll Console.c

Шаг четвертый: запуск


Теперь необходимо сообщить JVM имя библиотеки, которую необходимо загрузить. Это делается вызовом System.loadLibrary("Console"):

package com.blogspot.toolkas;

public class TestConsole {
public static void main(String[] args) {
 System.loadLibrary("Console");
 Console.print("TEST");
 }
}
Чтобы облегчить поиск нашей библиотеки – самый простой способ – скопировать ее в папку {JDK_HOME}/bin.

Если все сделано правильно, то запуск класса TestConsole пройдет успешно и на консоль будет выведена строка TEST.

На этом я хочу закончить наше краткое введение в мир JNI.
Удачных исследований!

Комментариев нет:

Отправить комментарий