Thinking in Java, 2nd edition, Revision 11

©2000 by Bruce Eckel

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]

B: Java Native Interface (JNI)

Данное приложение было написано и используется с разрешения Andrea Parovaglio (www.AndreaProvaglio.com).

Язык 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 вы можете выполнить с собственными методами.

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

Мы начнем с простого примера: 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 <jni.h>
/* Заголовок для класса 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 <jni.h>
#include <stdio.h>
#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 программе доступно большое количество функций. Эти функции могут быть сгруппированы в следующие категории:

Количество 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 <jni.h>
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 функций для обработки исключений:

Среди перечисленных вы не можете игнорировать 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 в описании объектов до того как они будут использованы.

Дополнительная информация

Вы можете найти более подробное объяснение, включая примеры кода на С (скорее чем С++) и дискуссию относительно подхода Microsoft в Приложении А первой редакции этой книги (находиться на CD поставляемого с этой книгой или на Web-сайте www.BruceEckel.com). Более подробная информация находиться на сайте java.sun.com (в поисковой системе выберите “training & tutorials”, а в качестве ключа “native methods”). Глава 11 книги Core Java 2, Volume II, by Horstmann & Cornell (Prentice-Hall, 2000) содержит всеобъемлющее описание собственных методов.

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]