JNI: взаимодействие Java с другими языками
Язык программирования Java, несмотря на имеющие место недостатки, является мощным и, главное, в большинстве случаев самодостаточным языком программирования. Под самодостаточностью я понимаю возможность написания программ, решающих какую-то конкретную задачу без привлечения других языков программирования.
Однако я не зря написал, что самодостаточность языка Java проявляется именно в большинстве случаев. Иногда без привлечения вспомогательных средств написать программу полностью невозможно. Например, необходимо воспользоваться кодом, который обеспечивает низкоуровневый доступ к «железу» того компьютера, на котором выполняется программа. В языке программирования Java, который по своей идеологии является многоплатформенным, средства низкоуровневого доступа к аппаратной части просто-напросто не предусмотрены.
С другой стороны, к моменту появления языка Java в мире программирования уже существовали колоссальные «залежи» программ и библиотек, позволяющих решать практически любые задачи, начиная от математических вычислений и заканчивая управлением сложными системами. Естественно, не замечать это богатство было бы просто неразумно.
Разработчики Java, рассуждая, вероятно, подобным образом, включили в язык возможность обращения из Java-программ к программам, реализованным на других языках программирования, так называемым native-методам. Подсистема Java, реализующая эту возможность, называется JNI (Java Native Interface – интерфейс языка Java, позволяющий обращение к native-методам).
В этой статье я не буду касаться внутренней реализации и остановлюсь на вопросах практического применения JNI.
Обращение к native-методам из языка Java
Процедуры, обеспечивающие связь native-метода с программой на Java, зависят от применяемой операционной системы и языка программирования, на котором native-методы реализованы. Рассмотреть все или хотя бы наиболее распространенные языки операционные системы в рамках журнальной статьи не представляется возможным. Поэтому я остановлюсь на связи языка Java с библиотеками DLL операционных систем семейства Microsoft Windows.
Представим, что в программе на языке Java есть метод с именем nativeMethod(), который де-факто является native-методом и реализован в какой-то динамической библиотеке. Для того, чтобы указать, что метод nativeMethod() является native-методом, при его объявлении необходимо использовать ключевое слово native. Например, это объявление может выглядеть следующим образом:
public native int nativeMethod();
Естественно, что для использования native-метода необходимо загрузить ту библиотеку, в состав которой он входит. Для этого программа на Java может воспользоваться методами load и loadLibrary(), входящими в состав класса System.
Это – все, что необходимо сделать в Java-программе для обеспечения вызова native-метода. Приведем пример объявления и вызова native-метода. При этом допустим, что файл называется Example.java и что native-метод должен быть реализован в библиотеке с именем Article.dll:
public class Example
{
public static void main( String args[] )
{
Example ex = new Example();
System.out.println( "Before native method call." );
int result = ex.nativeMethod();
System.out.println( "After native method call." );
System.out.println( "Result = " + result );
}
public native int nativeMethod();
static
{
System.load( "f:/Article/Example/Article.dll" );
}
}
Откомпилировав этот файл и, соответственно, содержащийся в нем класс, мы получим файл Example.class. Однако, несмотря на то, что класс откомпилировался без ошибки, он пока неработоспособен, так как до сих пор не установлена связь с native-методом. Для того, чтобы установить эту связь, придется проделать некоторую работу.
Подготовка native-метода к вызову из языка Java
Во-первых, при компиляции native-метода компилятор языка Java производит некоторые вспомогательные действия, результатом которых, в частности, является то, что при вызове native-метода к списку аргументов последнего добавляются некоторые величины, существование которых игнорировать нельзя. Для того чтобы определить, как будет выглядеть интерфейс вызова native-метода, следует воспользоваться утилитой javah (вероятно, сокращение от Java’s Header), которая входит в состав JDK. Все аргументы этой утилиты описаны в ее документации. Нам сейчас важно понять, что эта утилита обрабатывает ранее откомпилированный java-класс, который находится в файле, имя которого совпадает с именем класса.
Аргументом при вызове утилиты должно быть имя класса, в котором описан native-метод. Отыскав в этом классе вызов native-метода, утилита определяет, с каким аргументами будет вызван native-метод, после чего формирует файл заголовка (.h-файл), который должен быть включен в С-файл, в котором будет находиться реализация метода nativeMethod().
Итак, применительно к нашему примеру утилиту javah необходимо вызвать следующим образом:javah Example
Результатом работы утилиты будет файл Example.h, который приведен ниже:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class Example */
#ifndef _Included_Example
#define _Included_Example
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Example
* Method: nativeMethod
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_Example_nativeMethod (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Что мы можем узнать, взглянув на этот файл? Во-первых, то, что в состав программы на С будет включен заголовочный файл jni.h, в котором, кстати, содержатся все необходимые для работы JNI описания.
Во-вторых, мы увидим, что тот метод, который в классе Example был описан как
public native int nativeMethod();
в программе на C должен быть описан несколько по-другому, а именно как
JNIEXPORT jint JNICALL Java_Example_nativeMethod (JNIEnv *, jobject);
Определение JNIEXPORT в файле jni_md.h, который вызывается из jni.h, описано следующим образом:
#define JNIEXPORT __declspec(dllexport)
Описание вполне понятно и я не стану тратить время и внимание читателя на объяснение очевидных вещей. В том же файле определение JNICALL описано так:
#define JNICALL __stdcall
После этого становится понятным, что все эти «страшные» описания являются просто обозначениями, используемыми при вызове обычной экспортируемой функции.
Что же касается описания «jint», то в файлах jni.h и jni_md.hопределены несколько простых типов, которые могут быть использованы при написании native-методов. Я приведу их в таблице 1.
Определение | Тип |
typedef unsigned char | Boolean |
typedef unsigned short | Jchar |
typedef short | Jshort |
typedef float | Jfloat |
typedef double | Jdouble |
typedef long | Jint |
typedef __int64 | Jlong |
typedef signed char | Jbyte |
Таблица 1.
Вспомним также, что программа на С для вызова из программы на Java должна содержать не функцию nativeMethod(), а функцию Java_Example_nativeMethod(). Вероятно, новое имя функции показывает, что она принадлежит классу Example, который, в свою очередь, является классом Java. Очень важно и то, что у этой функции должно быть два аргумента. Первым аргументом функции является указатель на «среду» JNI. Описание этой «среды» находится в файле jni.h, оно достаточно объемно, поэтому, к сожалению, нет возможности привести его здесь. Второй аргумент – это аналог указателя «this» в C, то есть подобие указателя на объект, из которого произведен вызов native-метода. Об этих аргументах мы поговорим немного позже. Но уже сейчас нам известно достаточно для того, чтобы написать native-метод и вызвать его из класса Java. Ниже приведен исходный текст программы Article.cpp, в которой определен native-метод, предназначенный для вызова из программы на языке Java:
#include
#include
#include
#include "Example.h"
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
JNIEXPORT jint JNICALL Java_Example_nativeMethod (JNIEnv * je,
jobject jo)
{
printf( "!!! In the native method !!!\n" );
return 32;
}
Результатом компиляции этого файла будет файл Article.dll. А результатом работы класса Example, осуществляющего вызов метода из библиотеки Article.dll, будет следующий вывод:
Before native method call.
!!! In the native method !!!
After native method call.
Result = 32
Приведенный выше результат работы программы показывает, что, с одной стороны, отработала функция (native-метод), находящаяся в библиотеке, написанной на C (именно она выдает сообщение «!!! In the native method !!!»). С другой стороны, класс на языке Java получил результат выполнения native-метода и отобразил его.
Однако все, о чем говорилось выше, касается только для тех native-методов, которые программист может написать сам. А как же быть в случае, когда native-методы находятся в библиотеке, к исходным текстам которой программист доступа не имеет? В таком случае процесс обращения к native-методу состоит из двух этапов. Во-первых, необходимо создать «промежуточную» библиотеку на языке C/C++, в которой будут реализованы методы с именами и аргументами, подготовленными к вызову из программы на языке Java. Во-вторых, из этой промежуточной библиотеки должны вызываться методы и функции, находящиеся в уже готовой библиотеке. Так как «промежуточная» библиотека написана на языке С/С++, то ничто не мешает обратиться к функциям, находящимся в другой библиотеке, обычным образом, а результаты работы передать в программу на языке Java.
Вызов методов класса Java из native-кода
Итак, как показано выше, вызов native-методов и получение результата их выполнения из программы на языке Java возможно. Возникает вопрос: а что представляют собой передаваемые native-методу аргументы? Что такое «среда» JNI? Что представляет собой объект Java с точки зрения практического использования?
Фактически «среда» JNI представляет собой массив указателей на методы, используя которые можно осуществлять доступ к полям класса Java и осуществлять вызов методов класса Java (как класса, из которого вызывается native-метод, так и любого Java-класса, подробнее см. в конце статьи). Передаваемый же native-методу объект позволяет осуществить привязку методов JNI непосредственно к конкретному объекту и классу объектов, с которыми может работать native-метод. Продемонстрируем это на практике.
Технология получения native-методом значения поля, принадлежащего классу Java, состоит в следующем. Во-первых, необходимо получить указатель на класс, к которому относится объект, осуществивший вызов native-метода. Это можно сделать при помощи метода GetObjectClass(), входящего в состав «среды» JNI. В том случае, если native-метод реализован на языке C++, этому методу передается только полученный native-методом объект (второй аргумент при вызове native-метода). В случае написания native-метода на языке C аргументами метода являются указатель на «среду» JNI (первый аргумент при вызове native-метода) и переданный native-методу объект. Прототипы метода GetObjectClass() для языков C и С++ соответственно, взятые из файла jni.h, приведены ниже:
jclass (JNICALL *GetObjectClass) (JNIEnv *env, jobject obj); // C
jclass GetObjectClass(jobject obj)
{return functions->GetObjectClass(this,obj); } // C++
Во-вторых, получив указатель на класс, необходимо получить идентификатор поля объекта Java, доступ к которому необходимо получить из native-метода. Это делается при помощи входящего в состав «среды» JNI метода GetFieldID(). Прототипы этих методов, приведенные ниже, также находятся в файле jni.h:
jfieldID (JNICALL *GetFieldID)
(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig)
{
return functions->GetFieldID(this, clazz, name, sig);
}
В этом случае аргументы методов не столь очевидны, как в случае GetObjectClass(). Думаю, что аргументы env и clazz понятны без объяснений. Аргумент name представляет собой имя поля, значение которого необходимо получить. А что представляет собой поле sig?
Дело в том, что у каждого поля и метода класса, реализованного на языке Java, есть не только имя, но и так называемая сигнатура. Для того, чтобы получить эту сигнатуру, можно воспользоваться утилитой javap, вызвав ее с опцией -s и передав ей в качестве аргумента имя класса, сигнатуры которого нужно получить. В частности, для класса Example, приведенного выше, утилита javap выдала следующий результат:
Compiled from Example.java
public class Example extends java.lang.Object {
int i;
/* I */
public Example();
/* ()V */
public static void main(java.lang.String[]);
/* ([Ljava/lang/String;)V */
public void printMessage();
/* ()V */
public native int nativeMethod();
/* ()I */
static {};
/* ()V */
}
Отсюда видно, что сигнатура поля i равна «I». Передав методу GetFieldID() сигнатуру поля в качестве последнего аргумента, мы получим идентификатор поля. Но это еще не все. Для того чтобы получить значение поля, следует обратиться к методу
GetIntField():
jint (JNICALL *GetIntField)
(JNIEnv *env, jobject obj, jfieldID fieldID);
jint GetIntField(jobject obj, jfieldID fieldID)
{
return functions->GetIntField(this, obj, fieldID);
}
Возвращенное этим методом значение и будет являться тем значением поля, которое мы хотим получить.
Замечу, что общее название группы методов, позволяющих получить значение поля – Get
Для того чтобы из native-метода вызвать метод, определенный в вызывающем классе, нужно затратить усилий не меньше, чем для того, чтобы получить значение поля. Во-первых, при помощи метода GetMethodId() следует получить идентификатор метода. Прототипы метода GetMethodId() приведены ниже:
jmethodID (JNICALL *GetMethodID) (JNIEnv *env,
jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig)
{
return functions->GetMethodID(this, clazz, name, sig);
}
Думаю, что назначения аргументов методов не должны вызывать вопросов. Метод возвращает идентификатор метода в классе. Зная этот идентификатор, можно обратиться к методу при помощи одного из методов группы Call
void (JNICALL *CallVoidMethod)
(JNIEnv *env, jobject obj, jmethodID methodID, ...);
void CallVoidMethod(jobject obj, jmethodID methodID, ...) {
va_list args;
va_start(args,methodID);
functions->CallVoidMethodV(this,obj,methodID,args);
va_end(args);
}
Ниже приведен исходный текст класса Java, значение поля i которого будет получено в native-методе и метод printMessage() которого будет вызван из native-метода:
public class Example
{
int i;
public static void main( String args[] )
{
Example ex = new Example();
ex.i = 28;
System.out.println( "Before native method call." );
int result = ex.nativeMethod();
System.out.println( "After native method call." );
System.out.println( "Result = " + result );
}
public void printMessage()
{
System.out.println( "This method was called from native method." );
}
public native int nativeMethod();
static
{
System.load( "f:/Article/Example/Article.dll" );
}
}
Исходный текст DLL, в которой находится код, осуществляющий доступ к полю класса Example и вызывающий метод printMessage(), также приведен ниже:
#include "stdafx.h"
#include
#include "Example.h"
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
JNIEXPORT jint JNICALL Java_Example_nativeMethod
(JNIEnv *je, jobject jo)
{
jclass javaClass;
jfieldID javaFieldID;
jmethodID javaMethodID;
printf( "!!! In the native method !!!\n" );
javaClass = je->GetObjectClass( jo );
javaFieldID = je->GetFieldID( javaClass, "i", "I" );
printf( "i = %d.\n", je->GetIntField( jo, javaFieldID ) );
javaMethodID = je->GetMethodID( javaClass, "printMessage", "()V" );
printf( "javaMethodID = %x.\n", javaMethodID );
je->CallVoidMethod( jo, javaMethodID );
printf( "!!! About to leave the native method !!!\n" );
return 123;
}
Результат работы связки «класс Example <-> native-метод Java_Example_nativeMethod» был следующим:
Before native method call.
!!! In the native method !!!
i = 28.
javaMethodID = 6ac790.
This method was called from native method.
!!! About to leave the native method !!!
After native method call.
Result = 123
Проанализировав приведенный выше результат, можно заметить, что первое сообщение выдается классом Example. Cледующие пять строк выдаются native-методом. При этом строка, начинающаяся с «This method…», выдана методом класса Example, вызванным из native-метода. И, наконец, последние два сообщения выдаются опять-таки классом Example. Связь между классом Java и native-методом налицо.
Заключение
Естественно, описанными выше возможностями интерфейс JNI не ограничивается. В частности, native-метод может получить идентификатор любого класса при помощи метода FindClass(), после чего создать объект этого класса при помощи метода NewObject() и обращаться к методам созданного объекта при помощи методов группы Call
Однако в каждой бочке меда есть своя ложка дегтя. Дело в том, что использование в языке Java native-методов нарушает принцип многоплатформенности языка Java. Программа, использующая DLL, становится заведомо «привязанной» к платформе, на которой реализована DLL. Использование native-методов можно порекомендовать в тех случаях, когда предполагается использование основной программы (класса Java) на разных платформах, в то время как машинно- или платформенно-зависимые части программы в виде native-методов планируется разработать для каждой конкретной платформы. Если программу на Java, использующую native-методы, планируется применять только на той платформе, на которой реализованы native-методы, то такая затея заведомо является бессмысленной.
И еще один, более серьезный аргумент против native-методов. Native-код может получить доступ к любой части системы, что в принципе является небезопасным. Поскольку одним из требований, предъявляемых к языку Java, является требование безопасности, применение native-методов опять-таки идет вразрез с идеологией языка Java.
Тем не менее, ответственность за принятие решения о применении в программе на Java native-методов, расширяющих стандартные возможности Java, лежит на программисте.