10. JNI - Programowanie mieszane


Wykład jest poświęcony łączeniu kodu napisanego w Javie z kodem napisanym w językach C lub C++. Pokazane zostaną techniki pozwalające wywoływać z poziomu Javy funkcje zaimplementowane w tych językach. Zobaczymy również jak z poziomu języków C/C++ odwoływać się do obiektów Javy, ich metod i atrybutów.

1. Wstęp

Programowanie mieszane polega na łączeniu kodu napisanego w różnych językach programowania. W przypadku Javy - jak na razie - możliwe jest łączenie z kodem napisanym w C lub C++. Technologia umożliwiająca to nazywa się Java Native Interface (w skrócie JNI). Jest to biblioteka funkcji dla języków C i C++, które pozwalają na dostęp do pól obiektów Javy, wywoływanie metod, tworzenie obiektów itp. za pośrednictwem maszyny wirtualnej.

JNI ma zastosowanie przede wszystkim w następujących sytuacjach:

Metoda natywna, to metoda zadeklarowana w klasie Javy przy użyciu słowa kluczowego native (bez podania ciała), zaimplementowana w C lub C++ i umieszczona w bibliotece dynamicznie ładowanej w czasie wykonania. Może być wywoływana w kodzie Javy tak, jak każda inna metoda.
Funkcja natywna to funkcja implementująca metodę natywną (w językach C/C++).

Łączenie Javy i C++ nie odbywa się na poziomie leksykalnym - poprzez fizyczne wstawianie kodu programu, lecz na poziomie kodu wykonywanego - poprzez dynamiczne łączenie skompilowanego kodu w trakcie wykonania. Program napisany w Javie i korzystający z metod natywnych, będzie wymagał załadowania dynamicznej biblioteki zawierającej implementacje tych metod.

Poziom natywny (lub natywny) programu, to jego część napisana w języku C lub C++ i wykonywana w metodach natywnych (przez funkcje natywne).

2. Szybki start

Typowy schemat postępowania podczas łączenia Javy z kodem natywnym przy użyciu JNI sprowadza się do kilku kroków, które zaprezentujemy na przykładzie programu wypisującego na konsoli komunikat "Hello World"!.

2.1. Przygotowanie kodu w Javie

Program składa się z klasy HelloWorld, w której deklarujemy natywną metodę sayHello(). Będzie ona wykonywać zadania oddelegowane do strony natywnej - w tym przypadku wyprowadzi na konsolę napis.
class HelloWorld {

  native public void sayHello();

  public static void main(String[] args){
	new HelloWorld().sayHello();
  }
}

W deklaracji metody sayHello() występuje słowo kluczowe native. Nie posiada ona ciała i jest zakończona średnikiem - podobnie jak metody abstrakcyjne. Można jej jednak używać jak zwykłej metody. Słowo kluczowe native informuje kompilator, że ma do czynienia z metodą natywną - taką, której implementacja została wykonana w innym języku programowania i zostanie dostarczona w osobnej bibliotece. Ponadto, deklaracja metody dostarcza informacji o jej sygnaturze (liczbie i typach parametrów), o typie zwracanej wartości oraz tym, że jest to instancyjna (niestatyczna) metoda publiczna. Informacje te będą potrzebne do jej implementacji.

2.2. Kompilacja

Kompilujemy plik HelloWorld.java zawierający naszą klasę z poziomu powłoki (wiersza poleceń):

javac HelloWorld.java
lub używając ulubionego środowiska.

W klasie HelloWorld znajduje się metoda startowa main(...), jednak próba uruchomienia programu da następujący rezultat:

Exception in thread "main" java.lang.UnsatisfiedLinkError: sayHello
        at HelloWorld.sayHello(Native Method)
        at HelloWorld.main(HelloWorld.java:6)
Program nie może się wykonać, ponieważ maszyna wirtualna nie może znaleźć implementacji metody sayHello() (bo jej nie ma). Trzeba ją zatem napisać i poinformować maszynę wirtualną gdzie należy jej szukać.

2.3. Generowanie pliku nagłówkowego

Aby zaimplementować metodę natywną, musimy wiedzieć pod jaką nazwą będzie jej poszukiwać maszyna wirtualna Javy. Dowiemy się tego generując plik nagłówkowy zawierający prototyp metody natywnej.

Do generowania plików nagłówkowych służy program javah, dostarczany wraz z pakietem SDK. Znajduje się on w podkatalogu bin pakietu, tam gdzie kompilator i interpreter Javy.

Zakładając, że w katalogu bieżącym znajduje się plik HelloWorld.class polecenie javah -jni HelloWorld wygeneruje plik HelloWorld.h zawierający prototypy funkcji implementujących metody natywne zadeklarowane w klasie HelloWorld. Oto jego zawartość:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorld
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Mimo, iż metoda sayHello() jest bezparametrowa, to jej natywna implementacja posiada dwa parametry. Są to standardowe argumenty przekazywane wszystkim funkcjom implementującym metody natywne. Ich znaczenie zostanie wyjaśnione w sekcji 4.

Makra JNIEXPORT i JNICALL są potrzebne (na platformie Win32) do wyeksportowania funkcji z biblioteki dynamicznej i zapewnienia odpowiedniego protokołu wywołania. Są one zdefiniowane w pliku nagłówkowym jni_md.h.

2.4. Implementacja metody natywnej

W wygenerowanym pliku nagłówkowym znajduje się prototyp funkcji, którą należy zaimplementować:

JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *, jobject);
W pliku HelloWorld.cpp zawierającym jej implementację należy włączyć wygenerowany plik nagłówkowy HelloWorld.h. Parametry JNIEnv* i jobject możemy na razie zignorować.

#include <jni.h>
#include <iostream>
#include "HelloWorld.h"
using namespace std;

JNIEXPORT void JNICALL 
Java_HelloWorld_sayHello(JNIEnv * env, jobject self)
{
    cout << "Hello World!" << endl;
}
Jak widać, natywna implementacja metody sayHello() robi dokładnie to samo, co w Javie
System.out.println("Hello World!");.

Na początku włączamy plik nagłówkowy jni.h, który zawiera deklaracje funkcji i typów JNI. Musi on być włączany przez każdy moduł implementujący funkcje natywne.

2.5. Utworzenie biblioteki dynamicznej

Biblioteka dynamiczna to zestaw procedur współdzielonych przez programy działające w systemie, który jest ładowany na żądanie, bądź podczas uruchamiania programu. W przeciwieństwie do bibliotek statycznych nie są one na stałe wkompilowane w program, lecz znajdują się w osobnym pliku.

Teraz należy skompilować plik z implementacją HelloWorld.cpp i utworzyć bibliotekę dynamiczną. Sposób realizacji tego kroku jest zależny od kompilatora i systemu operacyjnego. Jednak we wszystkich przypadkach trzeba poinformować kompilator gdzie znajdują się pliki nagłówkowe: jni.h oraz jni_md.h (dołączany przez jni.h). Oba pliki są dostarczane wraz z SDK Javy i znajdują się w podkatalogach głównego katalogu dystrybucji: include (jni.h) oraz include\win32 lub include/linux (jni_md.h) - zależnie od używanego systemu. Do określania ścieżek poszukiwań plików nagłówkowych służą odpowiednie opcje kompilatora, z reguły -I.

Oto konkretne wywołania dla różnych kompilatorów (przy założeniu, że pakiet SDK został zainstalowany w katalogu /j2sdk dla Linuxa i C:\j2sdk dla Win32):

Linux/GNU gcc
c++ -I/j2sdk/include -I/j2sdk/include/linux -shared -o libHelloWorld.so HelloWorld.cpp
Win32/Borland bcc32
bcc32 -IC:\j2sdk\include -IC:\j2sdk\include\win32 -tWD -eHelloWorld.dll HelloWorld.cpp
Win32/Microsoft VC++
cl /IC:\j2sdk\include /IC:\j2sdk\include\win32 /GX /LD HelloWorld.cpp /FeHelloWorld.dll

Nazwa biblioteki może być dowolna, ale musi mieć rozszerzenie .dll dla Win32 lub .so dla Linuxa. W drugim przypadku jej nazwa musi również zaczynać się od prefiksu lib.

Po kompilacji w katalogu bieżącym powstaje biblioteka dynamiczna HelloWorld.dll dla platformy Win32, lub libHelloWorld.so dla Linuxa.

2.6. Uruchomienie programu

W celu uruchomienia naszego programu należy spełnić jeszcze dwa warunki:
  1. Zażądać załadowania przez VM przygotowanej biblioteki.
  2. Poinformować system operacyjny gdzie należy jej szukać.

Pierwszy realizujemy wstawiając statyczny blok z instrukcją ładującą do ciała klasy (wymaga to powtórnej rekompilacji pliku HelloWorld.java). Zostanie on wykonany w momencie załadowania klasy, czyli podczas pierwszego odwołania do niej. W tym wypadku na początku wykonania programu, ponieważ klasa HelloWorld zawiera metodę startową main(...).

Nazwę ładowanej biblioteki na platformie Win32 otrzymujemy odrzucając rozszerzenie .dll z nazwy pliku.
Pod Linuxem należy odrzucić rozszerzenie .so oraz prefiks lib z nazwy pliku.
static {
  System.loadLibrary("HelloWorld");
}
Wywołanie ładujące bibliotekę nie musi znajdować się koniecznie w bloku statycznym w klasie zawierającej deklaracje metod natywnych, zaimplementowanych w ładowanej bibliotece. Może ono się znaleźć w statycznym bloku innej klasy, w metodzie startowej main(...), lub w dowolnym innym miejscu. Ważne jest jedynie to, by kod realizujący metodę natywną był załadowany przed jej pierwszym wywołaniem.

Drugi warunek spełniamy ustalając odpowiednią zmienną środowiskową. Pod Linuxem będzie to LD_LIBRARY_PATH, pod Win32 - PATH. Zależnie od używanej powłoki wykonujemy polecenie

Na platformie Win32 nie trzeba nic robić, o ile plik z biblioteką znajduje się w katalogu bieżącym.
bash, sh, ksh
export LD_LIBRARY_PATH=lib
tcsh, csh
setenv LD_LIBRARY_PATH lib
ms-dos, cmd.exe dla Win32
set PATH=%path%;lib

gdzie lib jest katalogiem zawierającym bibliotekę - plik libHelloWorld.so lub HelloWorld.dll

>java HelloWorld
Hello World

Teraz pozostało już tylko uruchomić maszynę wirtualną i jeśli wszystko zostało wykonane prawidłowo zobaczymy na konsoli znany komunikat.

Jeśli dynamiczna biblioteka nie znajduje się w podanej lokalizacji, albo jej w ogóle nie ma - zobaczymy komunikat:

Exception in thread "main" java.lang.UnsatisfiedLinkError: 
no HelloWorld in java.library.path
        at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1403)
        at java.lang.Runtime.loadLibrary0(Runtime.java:788)
        at java.lang.System.loadLibrary(System.java:832)
        at HelloWorld.<clinit>(HelloWorld.java:4)
Należy wtedy zweryfikować położenie biblioteki lub zmienne określające jej położenie.


3. Konwencje nazewnicze JNI

Plik zawierający skompilowaną klasę musi być widoczny na ścieżce poszukiwań klas CLASSPATH, której domyślną wartością jest katalog bieżący: ".", i którą można zmienić opcją -classpath programu javah.

Powyższy przykład dotyczył najprostszego przypadku, w którym klasa zawierająca metody natywne umieszczona była w pakiecie domyślnym. Jeśli klasa znajduje się w pakiecie nazwanym, to nazwa wygenerowanego pliku nagłówkowego będzie utworzona z nazw pakietu i klasy oddzielonych podkreśleniem: nazwaPakietu_NazwaKlasy.h. Jako argument generatorowi plików nagłówkowych należy podać wtedy pełną (kwalifikowaną nazwą pakietu) nazwę klasy z deklaracją metody natywnej. Wygenerowany plik nagłówkowy zostanie umieszczony w katalogu bieżącym.

Jako przykład rozważmy klasę GetNumber umieszczoną w pakiecie getter.number. Zadeklarowano w niej trzy przeciążone metody natywne o nazwie getNumber, pobierające liczbę z klawiatury:
int getNumber()
zwraca wartość typu int
long getNumber(long interval)
zwraca long z przedziału [0..interval]
float getNumber(float left, float right)
zwraca wartość typu float z przedziału [left..right]
package getter.number;

public class GetNumber {

  static {
    System.loadLibrary("getnum");
  }

  native int   getNumber();
  native long  getNumber(long interval);
  native float getNumber(float left, float right);

  public static void main(String[] args){
    GetNumber gn = new GetNumber();

    int num = gn.getNumber();
    System.out.println("Podano: " + num);

    long val = gn.getNumber(num);
    System.out.println("Podano: " + val + 
                       " z przedziału [0.." + num + "]");

    float point = gn.getNumber(val, num);
    System.out.println("Podano: " + point + " z przedziału [" + 
                       (float)val + ".." + (float)num + "]");
   }
}

Plik GetNumber.java znajduje się w katalogu getter/number/. Kompilujemy go z katalogu bieżącego, a następnie generujemy plik nagłówkowy:

javac getter/number/GetNumber.java
javah -jni getter.number.GetNumber
Po kompilacji, w katalogu getter/number/ znajduje się plik GetNumber.class. Po wygenerowaniu pliku nagłówkowego, w katalogu bieżącym pojawia się plik getter_number_GetNumber.h.

3.1. Deskryptory

Deskryptor jest symbolicznym kodem typu. Deskryptory są potrzebne do kodowania sygnatur metod natywnych, a także do identyfikowania składowych klas Javy. Są trzy rodzaje deskryptorów: klas, pól i metod.

Deskryptory pól i metod można poznać przy pomocy programu javap. Służy on do dekompilacji klas Javy. Opcja -s zleca wypisanie deskryptorów pól, opcja -p powoduje wypisanie informacji również dla składowych prywatnych.

Na przykład polecenie: javap -s -p HelloWorld wyprowadzi:
Compiled from HelloWorld.java
public class HelloWorld extends java.lang.Object {
    public HelloWorld();
        /*   ()V   */
    public native void sayHello();
        /*   ()V   */
    public static void main(java.lang.String[]);
        /*   ([Ljava/lang/String;)V   */
    static {};
        /*   ()V   */
}

3.1.1. Deskryptory klas

Deskryptor klasy nazwanej to łańcuch znakowy przedstawiający jej pełną kwalifikowaną nazwę (z nazwą pakietu, jeśli występuje), w której zamieniono kropki "." na ukośniki "/".
Na przykład deskryptorem klasy java.lang.Class jest "java/lang/Class".

Deskryptor klasy reprezentującej tablicę (są to wewnętrzne klasy tworzone przez VM) jest łańcuchem znakowym składającym się ze znaku "[" (nawias kwadratowy lewy), po którym następuje deskryptor elementów tablicy (ale nie deskryptor klasy, bo klasa nie może być elementem tablicy - patrz dalej). Na przykład deskryptor klasy reprezentującej tablicę int[] to "[I", a dla tablicy Object[][] to "[[Ljava/lang/Object;".

3.1.2. Deskryptory pól

Deskryptory nie są wyłączną właściwością JNI, lecz pojawiają się również w innych kontekstach w Javie - np. przy nazwach typów tablicowych. W szczególności kod:
System.out.println(new Object[1][1]);
System.out.println(new int[1]);
Wypisze coś takiego:
[[Ljava.lang.Object;@126b249
[I@182f0db
Przed znakiem '@' znajdują się deskryptory wyprowadzanych obiektów - dwuwymiarowa tablica obiektów klasy Object i tablica (jednowymiarowa) liczb typu int.

Deskryptory pól typów pierwotnych Javy pokazuje tabelka:

Typ JavyDeskryptor pola
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD

Deskryptor pola typu odnośnikowego (czyli odniesienia do obiektu) powstaje z litery "L", za którą umieszcza się deskryptor klasy zakończony średnikiem ";". Na przykład deskryptorem pola typu java.lang.Class jest "Ljava/lang/Class;".

Deskryptory pól tablicowych są takie same jak deskryptory klas tych tablic.

3.1.3. Deskryptory metod

Deskryptor metody składa się z deskryptorów (pól) parametrów umieszczonych w nawiasach okrągłych, za czym występuje deskryptor (pola) wyniku. Deskryptory parametrów nie są oddzielone spacjami ani żadnymi innymi znakami.

Jeśli metoda jest bezargumentowa, to nawiasy okrągłe są puste. Jeśli typem zwracanym jest void, to deskryptorem wyniku jest V. Dla konstruktorów w miejscu deskryptora wyniku podaje się również V.

Oto kilka przykładów:

3.2. Nazwy metod natywnych

Dla danej metody natywnej VM będzie poszukiwać implementacji funkcji o nazwie utworzonej z nazwy metody, klasy i pakietu w następujący sposób: Java_nazwaPakietu_nazwaKlasy_nazwaMetody. Jeśli klasa znajduje się w pakiecie domyślnym, to część nazwaPakietu_ nie występuje.

Oto zawartość pliku nagłówkowego getter_number_GetNumber.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class getter_number_GetNumber */

#ifndef _Included_getter_number_GetNumber
#define _Included_getter_number_GetNumber
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     getter_number_GetNumber
 * Method:    getNumber
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_getter_number_GetNumber_getNumber__
  (JNIEnv *, jobject);

/*
 * Class:     getter_number_GetNumber
 * Method:    getNumber
 * Signature: (J)J
 */
JNIEXPORT jlong JNICALL Java_getter_number_GetNumber_getNumber__J
  (JNIEnv *, jobject, jlong);

/*
 * Class:     getter_number_GetNumber
 * Method:    getNumber
 * Signature: (FF)F
 */
JNIEXPORT jfloat JNICALL Java_getter_number_GetNumber_getNumber__FF
  (JNIEnv *, jobject, jfloat, jfloat);

#ifdef __cplusplus
}
#endif
#endif

Jeśli metoda natywna jest przeciążona, to do nazwy funkcji natywnej ją implementującej zostanie doklejony sufiks złożony z dwóch znaków podkreślenia: "__" i deskryptorów parametrów. W powyższym przykładzie nazwa pierwszej funkcji, implementującej metodę bezparametrową, jest zakończona tylko znakami podkreślenia. Druga funkcja kończy się sufiksem __J, co odpowiada pojedynczemu parametrowi typu long, a trzecia sufiksem __FF odpowiadającym dwóm parametrom typu float.

3.3. Odwzorowanie typów pierwotnych

Natywna implementacja metody getNumber(long) jako ostatni argument pobierała zmienną typu jlong. Jest to odpowiednik typu long z Javy. Ogólnie, podczas przekazywania argumentów i zwracania wartości typów pierwotnych przez funkcje natywne następuje konwersja typów zgodnie z poniższą tabelką:

Czym są typy pierwotne JNI?
Oto wyjątek z pliku jni.h:
typedef unsigned char	jboolean;
typedef unsigned short	jchar;
typedef short		jshort;
typedef float		jfloat;
typedef double		jdouble;
Pozostałe - zależne od platformy - definicje typów pierwotnych znajdują się w pliku jni_md.h
Typ JavyTyp C/C++Rozmiar w bitach
booleanjboolean8, unsigned
bytejbyte8
charjchar16, unsigned
shortjshort16
intjint32
longjlong64
floatjfloat32
doublejdouble64
voidvoid
Nazwy typów z drugiej kolumny, zaczynające się od litery 'j' są innymi nazwami na standardowe typy C/C++ o czym można przekonać się oglądając pliki nagłówkowe jni.h oraz jni_md.h, w których są zdefiniowane.

Typ jchar jest 16 bitowy i służy do reprezentowania znaków Unicode na platformach, które mają wsparcie dla tego systemu kodowania.
Do posługiwania się wartościami typu jboolean zdefiniowano dwa makra:

Teraz jesteśmy gotowi do zaimplementowania przeciążonych metod natywnych z klasy GetNumber. Oto plik GetNumber.cpp

#include <jni.h>
#include <iostream>
#include <cassert>
#include "getter_number_GetNumber.h"
using namespace std;

JNIEXPORT jint JNICALL 
Java_getter_number_GetNumber_getNumber__(JNIEnv * env, 
                                         jobject self)
{
  cout << "Podaj liczbę" << endl;
  int num;
  cin >> num;
  return num;
}

JNIEXPORT jlong JNICALL 
Java_getter_number_GetNumber_getNumber__J(JNIEnv * env, 
                                          jobject self, 
                                          jlong interv)
{
  if (interv < 0)
    return 0;
  long num;
  do {
    cout << "Podaj liczbę z przedziału [0.." 
         << (long)interv << "]" << endl;
    cin >> num;
  }
  while (num < 0 || num > interv);
  return num;
}

JNIEXPORT jfloat JNICALL 
Java_getter_number_GetNumber_getNumber__FF(JNIEnv * env, 
                                           jobject self, 
                                           jfloat left, 
                                           jfloat right)
{
  assert(left <= right);
  float reslt;
  do {
    cout << "Podaj liczbę z przedziału [" 
         << left << ".." << right << "]" << endl;
    cin >> reslt;
  }
  while (reslt < left || reslt > right);
  return reslt;
}

Skąd w takim razie potrzeba wprowadzania nowych typów ? Rozmiar i zakres typów int i long jest inny w Javie i C/C++ na pewnych platformach. Dlatego typ int Javy może być odwzorowany np. na long w C/C++ (Win32), który zostanie nazwany jint w pliku jni_md.h.

Jak widać przekazywanie argumentów, oraz zwracanie wartości typów pierwotnych nie cechuje się niczym szczególnym. Posługujemy się nimi podobnie, jak zwykłymi typami C/C++.

Tak samo jak w pierwszym przykładzie kompilujemy powyższy plik pamiętając o podaniu opcji włączających biblioteki JNI oraz generujących bibliotekę dynamiczną.

3.4. Odwzorowanie typów obiektowych

Obiekty Javy są przekazywane na zmiennych typu jobject.

Z punktu widzenia języków C/C++ są to wskaźniki, jednak jest to niewidoczne dla programisty. Będziemy je nazywać odniesieniami (w specyfikacji JNI stosuje się termin opaque reference).

Dla wygody zdefiniowane są również dodatkowe typy, odpowiadające częściej używanym klasom Javy: jstring, jclass, jarray, jthrowable. Do posługiwania się tablicami służy jarray, który posiada dodatkowe podtypy odpowiadające tablicom typów pierwotnych i obiektów. Na poniższym rysunku przedstawiona jest hierarchia dziedziczenia tych typów:

W C++ zmienne typów obiektowych są wskaźnikami do obiektów specjalnych klas, o nazwach utworzonych ze znaku podkreślenia '_' i nazwy typu:
typedef _jobject * jobject
.
Hierarchia dziedziczenia tych klas odpowiada hierarchii dziedziczenia ich odpowiedników w Javie. Ta odpowiedniość jest jedynym powodem ich istnienia (ciała tych klas są puste) - wymuszają one w C++ kontrolę typów JNI.
W języku C takie zmienne są wskaźnikami do (pustej) struktury _jobject.

Do zmiennych powyższych typów nie można się odwoływać bezpośrednio. Konieczne jest użycie specjalnych funkcji, o czym dalej.


4. Dostęp do Javy od strony natywnej

Jednym z podstawowych problemów powstających podczas łączenia aplikacji napisanej w Javie z kodem napisanym w C/C++ jest umożliwienie na poziomie natywnym dostępu do danych Javy. Służą do tego specjalne funkcje biblioteczne JNI. Pozwalają one przetwarzać zmienne typów pierwotnych i obiektowych Javy przekazywane jako argumenty i zwracane jako rezultaty metod natywnych w języku C/C++. Pozwalają one również na tworzenie obiektów oraz wysyłanie wyjątków Javy z poziomu natywnego. Osobny zestaw funkcji umożliwia koordynację wątków Javy, w szczególności tych, które wykonują kod natywny.

Pierwszym parametrem funkcji natywnych jest zawsze wskaźnik do środowiska JNIEnv * env. Jest to struktura, w której są zadeklarowane wszystkie funkcje JNI (dla kompilatora języka C będą to wskaźniki do funkcji). Jest ona zdefiniowana w pliku nagłówkowym jni.h (i zajmuje ponad 90% jego objętości). Poprzez wskaźnik env do owej struktury należy wywoływać wszystkie funkcje biblioteczne.

Drugim parametrem zawsze jest:

Pozostałe parametry odpowiadają parametrom metody natywnej, którą ta funkcja implementuje. Oczywiście, ich typy są natywnymi odpowiednikami typów Javy.

Wszystkie funkcje JNI wywołuje się na rzecz wskaźnika do środowiska JNIEnv * env, który jest przekazywany do funkcji natywnych jako argument.

Odniesienia do klas

Niektóre funkcje JNI wymagają jako argumentu odniesienia (wskaźnika) do obiektu typu jclass reprezentującego określoną klasę lub interfejs Javy. Można go uzyskać metodami:

jclass GetObjectClass(jobject obj); 
jclass FindClass(const char *name);

Na przykład:
jclass string_cls = env->FindClass("java/lang/String");
jclass objectArray_cls = env->FindClass("[Ljava/lang/Object;");
Pierwsze wywołanie zwraca odniesienie do klasy java.lang.String. Drugie zwraca odniesienie do klasy reprezentującej tablice obiektów java.lang.Object[].

Klasa demonstracyjna

Zasady łączenia Javy z językiem C++ zaprezentujemy na przykładzie klasy JNIRules. Zawiera ona krótkie metody demonstrujące poszczególne aspekty łączenia Javy z kodem natywnym. Metody te zostaną dalej szczegółowo omówione.

Oto klasa JNIRules:
public class JNIRules {

  native String stringUse(String arg);       // użycie łańcuchów znakowych
    
  native int[] arrayUse(double[] arg);       // używanie tablic

  boolean instanceField;
  static String staticField = "Java";
  native void fieldAccess();                 // dostęp do atrybutów
    
  native void callMethod(short calls);       // wywoływanie metod 

  native static JNIRules instanceCreation(); // tworzenie obiektów
  private JNIRules(boolean b){
    instanceField = b;
  }

  public static void main(String[] args){
    System.loadLibrary("rules");           // ładowanie biblioteki 
    JNIRules rules = instanceCreation();  
    rules.stringDemo();
    rules.arrayDemo();
    rules.fieldAccessDemo();
    rules.callMethod((short)4);
  }
	
    /*
     * metody testujące
     */	 	
}	

Przy okazji widzimy jak w inny sposób można załadować bibliotekę dynamiczną rules.dll lub librules.so, zawierającą implementacje powyższych metod natywnych. Wywołanie metody ładującej bibliotekę System.loadLibrary("rules"); jest umieszczone na początku metody main, przed pierwszym odwołaniem do którejkolwiek metody natywnej z klasy JNIRules.

Implementacje metod natywnych znajdują się w pliku JNIRules.cpp i będą przedstawiane w kolejnych podpunktach.

4.1. Łańcuchy znakowe

Łańcuchy znakowe są traktowane w sposób szczególny ze względu na częste stosowanie. Przetwarzanie każdego łańcucha jako obiektu klasy java.lang.String byłoby kosztowne i wymagałoby ciągłego powtarzania stałych fragmentów kodu. Dlatego do reprezentacji łańcuchów Javy został stworzony osobny typ jstring (w C++ jest podtypem jobject) i zestaw funkcji umożliwiających konwersje pomiędzy łańcuchami Javy i natywnymi.

Zmienne typu jstring nie są jednak zwykłymi łańcuchami C/C++, czyli wskaźnikami typu char*. Są to obiekty klasy String. W związku z tym nie można ich używać bezpośrednio w funkcjach operujących na napisach (np. printf() czy cout << ). Można jednak uzyskać wskaźnik char* specjalną funkcją i dalej przetwarzać tak uzyskany napis w sposób właściwy dla C/C++.

Łańcuchy znakowe w Javie są złożone z 16 bitowych znaków Unicode. Z kolei w C/C++ znaki reprezentowane są na 8 bitach. Aby móc przetwarzać argumenty typu String (po stronie natywnej jstring), jak również zwracać łańcuch znakowy jako rezultat funkcji natywnej, konieczne są funkcje konwertujące oba systemy reprezentacji łańcuchów. Ale jak zamienić dwubajtowy znak Javy na jednobajtowy znak natywny? Umożliwia to system kodowania UTF-8.

W systemie UTF-8 (w wersji używanej przez maszynę wirtualną Javy), jeśli najstarszy 8 bit znaku jest równy 0, to 7 bitów młodszych jest traktowane jako kod znaku w systemie ASCII. Jeśli natomiast najstarszy bit jest równy 1, to dany bajt jest częścią dwu- lub trzybajtowej sekwencji kodującej 16-bitowy znak Unicode.
Napisy UTF-8 są zakończone znakiem '\0', tak jak zwykłe łańcuchy C/C++.

4.1.1. Uzyskiwanie łańcucha natywnego

Do zamiany argumentu typu jstring na natywny łańcuch znakowy, czyli char* (reprezentowany w systemie UTF-8), służy funkcja

const char* GetStringUTFChars(jstring str, jboolean *isCopy);
Zwraca ona wskaźnik do znaku będący reprezentacją w systemie UTF-8 łańcucha Javy. Zwrócenie wartości NULL oznacza, że maszyna wirtualna nie mogła zaalokować wystarczającej ilości pamięci do przechowania łańcucha.

Na poziomie Javy zostanie wtedy zgłoszony wyjątek OutOfMemoryError. Nie spowoduje to jednak przerwania wykonywania metody natywnej, dlatego należy natychmiast opuścić ją przy pomocy instrukcji return. Szerzej o mechanizmie wyjątków w sekcji 5.2.

Testowanie wartości zwracanej na okoliczność pojawienia się wartości NULL jest dobrą praktyką, trzeba jednak pamiętać, że w tym przypadku może się ona pojawiać w bardzo określonych i rzadkich okolicznościach - kiedy istnieje zagrożenie brakiem pamięci spowodowanym wywołaniem tej funkcji.

Pierwszym parametrem powyższej funkcji jest łańcuch typu jstring, z reguły przekazany jako argument metody natywnej.

Jeśli po wyjściu z funkcji zachodzi
*isCopy == JNI_FALSE, to kod natywny nie może (tzn. nie powinien) modyfikować tego napisu. Spowodowałoby to modyfikację oryginalnego łańcucha Javy, naruszając tym samym aksjomat niezmienniczości obiektów klasy String (reprezentują one napisy niemodyfikowalne).

Jeśli drugi argument jest różny od NULL, to zostanie na nim przekazana informacja o tym, czy została wykonana lokalna kopia łańcucha, czy też nie. Jeśli po wyjściu z funkcji *isCopy ma wartość JNI_TRUE, to zwrócony wskaźnik odnosi się do lokalnej kopii łańcucha. Jeśli ma wartość JNI_FALSE, to odnosi się on do oryginalnego łańcucha zawartego w obiekcie klasy java.lang.String.

Jako drugi argument przeważnie przekazuje się NULL, ponieważ z reguły nie jesteśmy zainteresowani tym, czy operujemy na oryginalnym łańcuchu, czy też na jego kopii. Nie można też tego w żaden sposób przewidzieć.

Jeśli napis składa się wyłącznie z 7-bitowych znaków ASCII, to pozyskany w ten sposób wskaźnik można już przetwarzać w sposób typowy dla C/C++. W przeciwnym wypadku należy przekształcić łańcuch do tablicy bajtów metodą String.getBytes() i utworzyć na jej podstawie natywną tablicę znaków z tą samą zawartością. Szczegółowo procedura ta jest opisana w części 5.4.

4.1.2. Zwalnianie wskaźnika

Po zakończeniu używania pobranego łańcucha, a najpóźniej przed zakończeniem wywołania funkcji natywnej, należy koniecznie zwolnić dowiązanie do niego. Pozwoli to odśmiecaczowi na odzyskanie pamięci przydzielonej na lokalny łańcuch UTF-8. Zaniedbanie tego obowiązku prowadzi do wycieków pamięci.

Po uwolnieniu wskaźnika wywołaniem poniższej funkcji, odwoływanie się do napisu poprzez uwolniony wskaźnik może spowodować błąd dostępu do pamięci.

Do zwalniania łańcuchów uzyskanych metodą GetStringUTFChars służy funkcja
void ReleaseStringUTFChars(jstring str, const char* chars);
Jej pierwszym argumentem jest łańcuch znakowy, z którego pozyskany został napis UTF-8 przekazany jako drugi argument.

Wywołanie powyższej funkcji jest niezbędne, niezależnie od tego, czy podczas pobierania wskaźnika metodą GetStringUTFChars na ostatnim parametrze isCopy zwrócona została wartość JNI_TRUE (wtedy faktycznie została zaalokowana pamięć, którą należy zwolnić), czy JNI_FALSE. W ostatnim przypadku pamięć nie została zaalokowana, ale obiekt Javy został unieruchomiony w pamięci (tak, by odśmiecacz nie mógł go przesunąć w inny obszar) i należy go teraz odblokować.

4.1.3. Tworzenie łańcuchów znakowych

Aby zwrócić łańcuch znakowy jako rezultat, należy go najpierw utworzyć podając napis UTF-8 jako wzorzec. Służy do tego funkcja

jstring NewStringUTF(const char *utf);
Metoda NewStringUTF może zwrócić NULL. Podobnie jak w przypadku funkcji GetStringUTFChars oznacza to, że VM nie może zaalokować pamięci i na poziomie Javy (po wyjściu z metody natywnej) zostanie zgłoszony wyjątek OutOfMemoryError

W szczególności, jako argument można przekazać napis składający się wyłącznie ze znaków ASCII. Napis typu jstring uzyskany tą metodą jest kodowany w systemie Unicode. Jeśli zostanie zwrócony jako wynik, to nie trzeba go zwalniać, ponieważ zajmie się tym odśmiecacz Javy. Jest to po prostu obiekt klasy java.lang.String.

4.1.4. Przykład

Metoda stringUse z naszej demonstracyjnej klasy JNIRules wyprowadza na konsolę łańcuch pobrany jako argument i pobiera wiersz wprowadzony z klawiatury, który następnie jest zwracany jako wynik. Oto jej implementacja:

JNIEXPORT jstring JNICALL 
Java_JNIRules_stringUse(JNIEnv * env, jobject self, jstring javaString)
{
  const char * cppString = env->GetStringUTFChars(javaString, 0);
  if (cppString == NULL)
    return NULL;					 		  
  cout << cppString;
  env->ReleaseStringUTFChars(javaString, cppString);

  const int LEN = 80;
  char buf[LEN];
  cin.getline(buf, LEN);
  jstring retchars = env->NewStringUTF(buf);
  return retchars;
}
A tak wygląda metoda testująca (również z klasy JNIRules) oraz wynik działania
Prompt>jni
User typed: jni
   void stringDemo(){
     String userTyped = stringUse("Prompt>");
     System.out.println("User typed: " + userTyped);
   }

4.1.5. Pobieranie długości łańcucha

Długość napisu można pobrać funkcjami

jsize GetStringLength(jstring str); 
jsize GetStringUTFLength(jstring str);
Jeśli łańcuch str zawiera znaki spoza kodu ASCII, to zwrócona wartość będzie większa od liczby znaków składających się na napis str ze względu na dwu i trzybajtowe sekwencje kodujące.

Pierwsza zwraca liczbę znaków, z których składa się napis str. Druga zwraca liczbę bajtów potrzebną do reprezentacji łańcucha str w systemie UTF-8, nie licząc końcowego znaku '\0'. Rezultat jest typu jsize, który jest inną nazwą na jint.

4.1.6. Kopiowanie do bufora

Jeśli zachodzi potrzeba skopiowania fragmentu łańcucha typu jstring do tablicy znaków, to należy posłużyć się funkcją

void GetStringUTFRegion(jstring str, jsize start, jsize len, char *buf);
Kopiuje ona len znaków z łańcucha str do bufora buf począwszy od pozycji start (łańcucha źródłowego).

Łańcuch źródłowy jest w formacie Unicode, natomiast w buforze zostaną umieszczone znaki w systemie UTF-8. W związku z tym, jeśli napis zawiera znaki spoza kodu ASCII, liczba skopiowanych bajtów może być większa niż liczba znaków podana jako argument len.

Jeśli przetwarzany łańcuch znakowy nie jest długi, to powyższej funkcji można i należy używać w miejsce GetStringUTFChars z dwóch powodów:

4.1.7. Łańcuchy natywne Unicode

Dla systemów posiadających wsparcie dla formatu Unicode (np. WinNT i podobne) został przewidziany zestaw funkcji operujących na łańcuchach znakowych bez pośrednictwa systemu UTF-8:

jstring NewString(const jchar *unicode, jsize len);
jsize GetStringLength(jstring str); 
const jchar * GetStringChars(jstring str, jboolean *isCopy);
void ReleaseStringChars(jstring str, const jchar *chars);
void GetStringRegion(jstring str, jsize start, jsize len, jchar *buf);
W miejsce wskaźnika typu char* używają one jchar*. Typ jchar jest 16 bitowym znakiem odwzorowanym na unsigned short. Do przetwarzania takich łańcuchów potrzebne są specjalne funkcje dostarczane przez platformę natywną.

4.2. Tablice

W funkcjach natywnych tablice Javy reprezentowane są za pomocą zmiennych typu jarray i pochodnych (patrz rys. w sekcji 3.4). Podobnie jak w przypadku łańcuchów znakowych bezpośrednie odwoływanie się do tych zmiennych jest nielegalne - do przetwarzania tablic należy stosować specjalne funkcje. Tablice elementów typów pierwotnych są traktowane w specjalny sposób. Poszczególnym typom pierwotnym odpowiadają specjalne typy tablicowe JNI przedstawione w tabelce oraz zestawy funkcji dostępu do nich. Tablice obiektów są zawsze odwzorowywane na typ jobjectArray i sposób ich przetwarzania jest inny, niż w przypadku tablic złożonych z elementów typów pierwotnych. W szczególności tablice wielowymiarowe - ponieważ w Javie są to tablice obiektów - są przedstawiane na zmiennych typu jobjectArray.

Typy tablicowe są podtypami typu jarray. Oznacza to, że zmienne tych typów są również typu jarray. W związku z tym np. zmienna typu jbooleanArray może być traktowana jako zmienna typu jarray.

Typ JNITyp Javy
jbooleanArrayboolean[]
jbyteArraybyte[]
jcharArraychar[]
jshortArrayshort[]
jintArrayint[]
jlongAraaylong[]
jfloatArrayfloat[]
jdoubleArraydouble[]

Zasady postępowania z jednowymiarowymi tablicami złożonymi z elementów typów pierwotnych są podobne do przetwarzania łańcuchów znakowych. Typowy schemat przetwarzania takiej tablicy przekazanej jako argument metody natywnej składa się z następujących kroków:

  1. Pobranie długości tablicy.
  2. Uzyskanie wskaźnika, poprzez który będzie odbywał się dostęp do jej elementów.
  3. Przetwarzanie elementów tablicy w sposób właściwy dla języka C/C++, za pośrednictwem pobranego wskaźnika.
  4. Zwolnienie pozyskanego wskaźnika.
Tablica natywna, do której odnosi się wskaźnik uzyskany w p.2 może być kopią oryginalnej tablicy Javy, dlatego niezbędne jest zwrócenie pamięci przez nią zajmowanej, kiedy już nie będzie potrzebna. Może też być nią samą (tzn. oryginałem) - wskaźnik odnosi się wtedy do obiektu Javy umieszczonego w obszarze pamięci będącym pod kontrolą odśmiecacza. W tej sytuacji, na czas przetwarzania tablicy po stronie natywnej zostaje ona unieruchomiona w pamięci, tak by odśmiecacz nie mógł jej przemieścić (spowodowałoby to unieważnienie wskaźnika, którym dysponujemy). Zwolnienie wskaźnika, o którym mowa w p. 4 oznacza w tym wypadku anulowanie unieruchomienia, tak by odśmiecacz mógł ją w razie potrzeby przemieszczać.

W kolejnych podpunktach zostaną omówione poszczególne aspekty przetwarzania tablic i funkcje JNI z nimi związane.

4.2.1. Obliczanie długości tablicy

Długość tablicy (liczbę elementów) pobieramy funkcją:

jsize GetArrayLength(jarray array);
Typ jarray jest nadtypem wszystkich typów tablicowych. Zatem można poprzez parametr tego typu przekazywać wszystkie tablice.

4.2.2. Uzyskiwanie wskaźnika do tablicy elementów typów pierwotnych

Aby móc przetwarzać tablicę Javy w języku C/C++ trzeba uzyskać wskaźnik do pierwszego elementu, który pozwoli na dostęp do pozostałych zwykłą arytmetyką wskaźników, bądź operatorem []. Jeśli posiadamy już tablicę natywną (np. zaalokowaną na stosie), do której chcemy skopiować elementy tablicy Javy, to należy skorzystać z funkcji omówionych niżej. W przeciwnym wypadku należy pobrać wskaźnik do elementu jedną z funkcji:

jtype * GetTypeArrayElements(jarraytype array, jboolean *isCopy); 
jtype jarraytypeType
jboolean jbooleanArray Boolean
jbyte jbyteArray Byte
jchar jcharArray Char
jshort jshortArray Short
jint jintArray Int
jlong jlongArray Long
jfloat jfloatArray Float
jdouble jdoubleArray Double

Związek pomiędzy Type, jtype i jarraytype wyjaśnia tabelka:

Parametr array jest odniesieniem do tablicy Javy. Parametr isCopy ma podobne znaczenie jak w przypadku łańcuchów znakowych (metoda GetStringUTFChars) - zostanie na nim przekazana informacja o tym, czy została utworzona lokalna kopia tablicy (*isCopy == JNI_TRUE), czy też wskaźnik odnosi się do tablicy zawartej w obiekcie Javy (*isCopy == JNI_FALSE) . Z reguły podaje się NULL, co oznacza, że nie jesteśmy tym zainteresowani.

Funkcja zwraca NULL, jeśli zabrakło pamięci na utworzenie kopii. Na poziomie Javy (po wyjściu z funkcji natywnej, w której wywołano powyższą) zostanie wtedy zgłoszony wyjątek OutOfMemoryError.

Uzyskany w ten sposób wskaźnik jest aktualny do momentu uwolnienia go funkcją zwalniającą. Po uwolnieniu odwoływanie się do niego będzie powodować błąd w dostępie do pamięci.

4.2.3. Zwalnianie tablic

Kiedy tablica nie jest już potrzebna, a najpóźniej przed wyjściem z funkcji natywnej, należy uwolnić pozyskany wskaźnik do elementów. Spowoduje to zwrócenie pamięci zajmowanej przez kopie elementów, jeśli tablica była kopią. Jeśli wskaźnik odnosił się do oryginału, uwolnienie umożliwi odśmiecaczowi przemieszczanie tablicy, jeśli będzie to potrzebne.

Do zwalniania pamięci zajmowanej przez tablice służą funkcje:

void ReleaseTypeArrayElements(jarraytype array, jtype *elems, jint mode); 
Parametr array jest tablicą, z której pobrano wskaźnik elems. Parametr mode określa sposób zwalniania i może przyjmować następujące wartości: Jako wartość argumentu mode z reguły podaje się 0.

4.2.4. Kopiowanie fragmentów tablic

Funkcje GetTypeArrayElements() mogą utworzyć kopię oryginalnej tablicy. Jeśli jest ona duża, a potrzebny jest dostęp tylko do jej fragmentu, to aby uniknąć zbędnych narzutów czasowych i pamięciowych należy posłużyć się następującą funkcją:

void GetTypeArrayRegion(jarraytype array, jsize start, jsize len, jtype *buf);
Jej wywołanie powoduje skopiowanie obszaru o początku start i długości len z tablicy array do bufora buf

Dla tablic niewielkich rozmiarów zaleca się użycie powyższej funkcji zamiast GetTypeArrayElements() z powodów podobnych jak w przypadku łańcuchów znakowych i funkcji GetStringUTFRegion: nie wymaga ona alokacji pamięci na stercie (o ile dostarczymy bufor zaalokowany na stosie) oraz nie zgłasza wyjątków (oprócz przekroczenia zakresu tablicy). Aby użyć jej do uzyskania zawartości tablicy, jako wartość argumentu start podajemy 0 a jako wartość len podajemy długość tablicy.

Kopiowanie w drugą stronę wykonują funkcje:

void SetTypeArrayRegion(jarraytype array, jsize start, jsize len, jtype *buf);
Kopiują one len elementów z bufora buf do tablicy array począwszy od jej indeksu start. Można w ten sposób skopiować jeden element jak i całą tablicę.

Funkcje kopiujące SetTypeArrayRegion dokonują sprawdzenia przekroczenia zakresu tablicy i jeśli takowy wystąpił, to po opuszczeniu ciała metody natywnej na poziomie Javy zostanie zgłoszony wyjątek ArrayIndexOutOfBoundsException.

4.2.5. Tworzenie tablic

Do tworzenia nowych tablic służą funkcje:

jarraytype NewTypeArray(jsize len);
jobjectArray NewObjectArray(jsize len, jclass clazz, jobject init);
Pierwsza tworzy tablicę elementów pierwotnych, druga - obiektów. Parametr len określa rozmiar tablicy. Parametr jclass clazz jest klasą, której obiekty będą przechowywane w tablicy. Trzeci parametr jobject init jest obiektem, którym zostaną wypełnione wszystkie komórki tablicy. Przeważnie podaje się NULL - wtedy tablica będzie pusta (będzie zawierała puste miejsca na len elementów).

Tak utworzone tablice są obiektami Javy i mogą być zwrócone jako wynik metody natywnej.

4.2.6. Przykład

Jak stosować powyższe funkcje zobaczymy na przykładzie metody natywnej int[] arrayUse(double[]) z klasy JNIRules. Dla każdej liczby typu double z tablicy przekazanej jako argument oblicza ona podłogę z pierwiastka i umieszcza wynik w tablicy liczb całkowitych zwracanej jako wynik.

Dodatkowa tablica jest potrzebna ze względów efektywnościowych: pozwala uniknąć wielokrotnego wywoływania funkcji SetIntArrayRegion dla ustalenia wartości kolejnych elementów tablicy retarr zwracanej jako wynik. Zamiast tego, po wykonaniu obliczeń cała tablica z wynikami zostanie skopiowana jednym wywołaniem do tablicy zwracanej na zewnątrz, co oczywiście będzie szybsze, nawet wziąwszy pod uwagę koszt utworzenia i usunięcia tablicy tymczasowej.

Parametr input typu double[], jest w kodzie natywnym reprezentowany jako jdoubleArray. Na początku tworzymy nową tablicę retarr typu jintArray przeznaczoną na wynik zwracany przez metodę (typu int[]). Dodatkowo tworzymy pomocniczą tablicę temparr elementów typu jint, do której będą zapisywane wyniki obliczeń, i która potem zostanie w całości skopiowana do wynikowej tablicy retarr. W pętli dla każdego elementu tablicy input obliczamy podłogę z jego pierwiastka zapamiętując wynik w tablicy tymczasowej temparr. Na końcu kopiujemy tablicę tymczasową do zwracanej tablicy retarr i zwalniamy używaną pamięć: tablicę tymczasową operatorem delete [], a przekazaną jako argument input funkcją ReleaseDoubleArrayElements.

JNIEXPORT jintArray JNICALL 
Java_JNIRules_arrayUse(JNIEnv * env, jobject self, jdoubleArray input)
{
  jsize tlen = env->GetArrayLength(input);
  jdouble * body = env->GetDoubleArrayElements(input, 0);
  if (body == NULL)
    return NULL;
  jintArray retarr = env->NewIntArray(tlen);
  if (retarr == NULL)
    return NULL;
  jint * temparr = new jint[tlen];          
  for (int i = 0; i < tlen; i++)
    temparr[i] = (long)floor(sqrt(body[i]));
  env->SetIntArrayRegion(retarr, 0, tlen, temparr);
  env->ReleaseDoubleArrayElements(input, body, 0);
  delete [] temparr;
  return retarr;
}

A tak wygląda metoda testująca (z klasy JNIRules) i wynik działania:
sqrt(1.0)=1
sqrt(4.5)=2
sqrt(10.01)=3
  void arrayDemo(){
    double[] in = new double[]{1, 4.5, 10.01};
    int[] out = arrayUse(in);
    for (int i = 0; i < in.length; i++)
      System.out.println("sqrt(" + in[i] + ")=" + out[i]);
  }

4.2.7. Tablice obiektów

Do przetwarzania tablic obiektów służy zestaw metod:

jobject GetObjectArrayElement(jobjectArray array, jsize index);
void SetObjectArrayElement(jobjectArray array, jsize index, jobject val);
Pierwsza z nich zwraca odniesienie do obiektu znajdującego się na pozycji index w tablicy array.
Druga ustala element z tablicy array na pozycji index jako (odniesienie do) val.

Jak widać, metody powyższe umożliwiają dostęp tylko do pojedynczego elementu tablicy. Nie można przetwarzać całych tablic ani ich fragmentów, ponieważ nie można uzyskać wskaźnika do elementu tablicy, który pozwalałby przeglądać całą tablicę. Wynika to z faktu, że w przeciwieństwie do typów pierwotnych, typy obiektowe nie mają swoich odpowiedników po stronie natywnej. Nie wiadomo zatem jaki jest ich rozmiar, co jest konieczne dla arytmetyki wskaźnikowej. W związku z tym po zakończeniu przetwarzania tablicy obiektów nie trzeba jej zwalniać, bo nie ma czego.

4.2.8. Tablice wielowymiarowe

Tablice wielowymiarowe przekazywane są jako tablice obiektów - tym są w istocie w Javie. Aby móc traktować element typu jobject (czyli Object) jako tablicę należy dokonać zwykłego rzutowania - podobnie jak się to robi w Javie. Rzutować będziemy pozyskany element tablicy głównej (właściwie odniesienie do niego) na typ tablicy elementów, które przechowuje (tu jest to tablica obiektów):

jobject array = env->GetObjectArrayElement(object2DimArray, x);
jobjectArray row = (jobjectArray)array;
jobject elem = env->GetObjectArrayElement(row, y);   
object2DimArray jest tu dwuwymiarową tablicą obiektów, np. przekazaną jako argument do funkcji natywnej (jako argument typu jobjectArray). Jej elementem na pozycji x jest array. Wiedząc, że ten element jest w istocie tablicą (jednowymiarową), możemy go bezpiecznie rzutować na typ jobjectArray. Z uzyskanej w ten sposób jednowymiarowej tablicy obiektów row korzystamy jak z normalnej tablicy - pobieramy element z pozycji y. Teraz na zmiennej elem mamy obiekt z pozycji [x][y] tablicy object2DimArray.

W części 5.2 opisującej w program mnożący macierze, zobaczymy dokładniej jak posługiwać się tablicami obiektów i tablicami wielowymiarowymi.

4.3. Dostęp do składowych

Z poziomu natywnego można odwoływać się do atrybutów obiektów, jak również modyfikować ich wartości. Dwa zestawy funkcji JNI pozwalają na operowanie na atrybutach statycznych i niestatycznych obiektów Javy.

4.3.1. Pobranie identyfikatora atrybutu

Dostęp do atrybutów odbywa się za pośrednictwem zmiennych typu jfieldID, które są identyfikatorami składowych na poziomie natywnym. Aby można było wykonywać operacje na atrybucie należy najpierw uzyskać jego identyfikator. Służą do tego funkcje:

jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);

Identyfikator atrybutu jest niezależny od obiektów. Jest wyznaczony przez klasę, w której jest zdefiniowany. W jej podklasach (o ile jest on dziedziczony), będzie miał taką samą wartość.

Obie funkcje zwracają identyfikator składowej albo NULL, jeśli operacja się nie powiodła. Na poziomie Javy (po wyjściu z metody natywnej) zostanie wtedy zgłoszony wyjątek, z reguły NoSuchFieldError.

Znaczenie poszczególnych argumentów:

  1. jclass clazz jest odniesieniem do klasy w której jest widoczny atrybut. Jego definicja może się znajdować w klasie clazz, jej nadklasie (jeśli nie jest prywatny) lub implementowanym interfejsie.
  2. const char * name jest nazwą atrybutu, pod jaką jest on zdefiniowany w klasie.
  3. const char * sig jest deskryptorem pola (patrz deskryptory).

Pozyskany identyfikator składowej jest aktualny do momentu odładowania klasy, z której ona pochodzi (o odładowaniu decyduje odśmiecacz). Można go przez ten czas przechowywać na zmiennej i używać w kolejnych odwołaniach do tej składowej. Jeśli klasa została odładowana, to po ponownym jej załadowaniu należy pozyskać identyfikator jeszcze raz w ten sam sposób.

4.3.2. Zapis i odczyt atrybutu

Mając identyfikator możemy operować na atrybucie używając funkcji odpowiadających jego typowi i rodzajowi (statyczny/niestatyczny):

jtyp GetStaticTypField(jclass clazz, jfieldID fieldID);
void SetStaticTypField(jclass clazz, jfieldID fieldID, jtyp value);
jtyp GetTypField(jobject obj, jfieldID fieldID);
void SetTypField(jobject obj, jfieldID fieldID, jtyp val); 
Jako jtyp zwracana jest wartość atrybutu określonego przez argumenty. Parametr jclass clazz jest odniesieniem do klasy, w której atrybut statyczny jest zdefiniowany. Podobnie jobject obj jest odniesieniem do obiektu zawierającego dany atrybut. jfieldID jest identyfikatorem tego pola, a val jest nową wartością, która zostanie mu nadana. Związek między jtyp a Typ wyjaśnia tabelka:

Uzyskiwanie identyfikatorów pól i metod (o tym dalej) jest dość kosztowną operacją. Dlatego, w sytuacjach kiedy przewidujemy wiele odwołań do składowej, dobrze jest przetrzymywać raz pobrany identyfikator na zmiennej. Można to zrobić na dwa sposoby: Pierwszy sposób jest zalecany, ponieważ nie wymaga troszczenia się o to, by klasa zawierająca atrybut nie została odładowana z pamięci.
Typjtyp
Booleanjboolean
Bytejbyte
Charjchar
Shortjshort
Intjint
Longjlong
Floatjfloat
Doublejdouble
Objectjobject

4.3.3. Przykład

Metoda fieldAccess() z demonstracyjnej klasy JNIRules pokazuje jak postępować z atrybutami statycznymi (w przykładzie: staticField typu String) oraz niestatycznymi (instanceField typu boolean). Jej działanie sprowadza się do wypisania wartości obu pól, ich modyfikacji i wypisania wartości po modyfikacji.

JNIEXPORT void JNICALL 
Java_JNIRules_fieldAccess(JNIEnv * env, jobject self)
{
  jclass jniRules = env->GetObjectClass(self);
  jfieldID fid = env->GetFieldID(jniRules, "instanceField", "Z");
  jfieldID s_fid = env->GetStaticFieldID(jniRules, 
                                         "staticField", 
                                         "Ljava/lang/String;");
  if (fid == NULL || s_fid == NULL)
    return;

  jboolean fld_val = env->GetBooleanField(self, fid);
  jstring s_fld_val = 
           (jstring)env->GetStaticObjectField(jniRules, s_fid);

  const char * cstr = env->GetStringUTFChars(s_fld_val, NULL);
  if (cstr == NULL)
    return;

  cout << "W trakcie Java_JNIRules_fieldAccess()"
       << " - wartosci argumentow:\n" 
       << "\tinstanceField = " 
       << (fld_val == JNI_TRUE ? "true" : "false")
       << "\n\tstaticField   = " << cstr << endl;

  env->ReleaseStringUTFChars(s_fld_val, cstr);
  cstr = "C++";
  s_fld_val = env->NewStringUTF(cstr);
  if (s_fld_val == NULL)
    return;

  env->SetBooleanField(self, fid, JNI_FALSE);
  env->SetStaticObjectField(jniRules, s_fid, s_fld_val);

  cout << "W trakcie Java_JNIRules_fieldAccess()"
       << " - wartosci po modyfikacji:\n" 
       << "\tinstanceField = " 
       << (fld_val == JNI_TRUE ? "true" : "false")
       << "\n\tstaticField   = " << cstr << endl;
}

Na początku pobieramy odniesienie jniRules do klasy JNIRules, które będzie potrzebne do uzyskania identyfikatorów składowych, wartości atrybutu statycznego oraz ustalenia jego nowej wartości. Następnie pobieramy identyfikator atrybutu niestatycznego fid i statycznego s_fid. Jeśli funkcja przekazująca identyfikator zwróci NULL, to najprawdopodobniej został popełniony błąd przy podawaniu nazwy składowej, jej deskryptora, albo też takiej składowej nie ma (możliwe są też inne przyczyny, które tu pomijamy). Należy wtedy opuścić funkcję natywną, po czym na poziomie Javy zostanie automatycznie zgłoszony wyjątek NoSuchFieldError (lub inny, jeśli przyczyna była inna).

Dysponując identyfikatorami, możemy odczytać wartości atrybutów. Ponieważ staticField jest typu String, jego wartość trzeba pobrać funkcją przeznaczoną dla pól obiektowych (zwracającą jobject - dlatego trzeba rzutować wynik na jstring), a następnie przetworzyć na łańcuch znakowy C/C++ - typu char*. Ponieważ wyciągnięcie wskaźnika może się nie powieść z braku pamięci, sprawdzamy czy nie została zwrócona wartość NULL i jeśli tak, to opuszczamy funkcję natywną (zostanie zgłoszony wyjątek OutOfMemoryError).

Po wypisaniu wartości atrybutów pobrany wskaźnik nie jest już potrzebny, więc go zwalniamy i tworzymy nowy łańcuch znakowy Javy (sprawdzając, czy nie został zwrócony NULL), który będzie nową wartością atrybutu staticField. Na końcu ustalamy nowe wartości składowych i wyprowadzamy je na standardowe wyjście.
Zmiennymi typu jboolean posługujemy się za pomocą stałych JNI_TRUE i JNI_FALSE.

Oto metoda testująca oraz deklaracje atrybutów (z klasy JNIRules):

boolean instanceField;    // wartość true nadaje konstruktor 
static String staticField = "Java";

void fieldAccessDemo(){
  System.out.println("Przed fieldAccess():");
  System.out.println("\tinstanceField = " + instanceField);
  System.out.println("\tstaticField   = " + staticField);
  fieldAccess();
  System.out.println("Po fieldAccess():");
  System.out.println("\tinstanceField = " + instanceField);
  System.out.println("\tstaticField   = " + staticField);
}
I wydruk działania programu:
Przed fieldAccess():
        instanceField = true
        staticField   = Java
W trakcie Java_JNIRules_fieldAccess() - wartosci argumentow:
        instanceField = true
        staticField   = Java
W trakcie Java_JNIRules_fieldAccess() - wartosci po modyfikacji:
        instanceField = true
        staticField   = C++
Po fieldAccess():
        instanceField = false
        staticField   = C++

4.4. Wywoływanie metod

Wywoływanie metod Javy z poziomu natywnego sprowadza się do podobnego schematu, co w przypadku atrybutów. Najpierw pozyskujemy identyfikator metody (typu jmethodID), a następnie używamy odpowiedniej funkcji JNI do wykonania właściwego wywołania. Ze względu na polimorfizm, wywoływana metoda nie musi być zadeklarowana w klasie obiektu, na rzecz którego jest wołana. Może ona być zadeklarowana w jego nadklasie, a nawet w interfejsie implementowanym przez jego klasę.

4.4.1. Pobranie identyfikatora metody

Do pozyskania identyfikatora metody używamy jednej z funkcji:

jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig);
Jeśli w danej klasie nie jest widoczna metoda o podanej nazwie i sygnaturze, to powyższe funkcje zwracają NULL. Jeśli w tym momencie nastąpi powrót z funkcji natywnej, to w Javie zostanie zgłoszony wyjątek NoSuchMethodError.

Argumenty mają znaczenie podobne jak w przypadku dostępu do składowych:
  1. clazz jest klasą lub interfejsem, w której metoda będzie poszukiwana, tzn. albo jest w niej zdefiniowana, albo odziedziczona z nadklasy lub interfejsu. Najczęściej będzie to klasa obiektu, na rzecz którego wołamy metodę.
  2. name jest nazwą metody.
  3. sig jest deskryptorem tej metody (patrz deskryptory). Ma on postać:
    "(typy-argumentów)typ-wyniku".

Wołając metodę prywatną, jej identyfikator należy uzyskać podając jako argument clazz rzeczywistą klasę obiektu, z którego ją wołamy. Nie może to być nadklasa ani interfejs (ponieważ metody prywatne nie są wirtualne).

4.4.2. Wywołanie metody

Mając identyfikator metody, wywołujemy ją za pośrednictwem jednej z wielu funkcji JNI. Nazwa odpowiadającej funkcji zależy od typu rezultatu wołanej metody. Metodom statycznym odpowiadają funkcje zawierające słowo Static w nazwie.

jtyp CallTypMethod(jobject obj, jmethodID methodID, ...);
jtyp CallStaticTypMethod(jclass clazz, jmethodID methodID, ...);
Jako wynik zwracają one to, co zwracają wołane metody Javy. Typ i jtyp mają takie znaczenie jak w przypadku metod odwołujących się do atrybutów (patrz tabelka z poprzedniej sekcji), ale dodatkowo obejmują poniższą parę:
Wielokropek (...) kończący listę parametrów oznacza, że funkcja może pobrać w tym miejscu dowolną liczbę argumentów dowolnego typu oddzielonych przecinkami. Jest to normalna konstrukcja języków C/C++.
Typjtyp
Voidvoid

Ogólnie, argumenty dla metody można przekazać na trzy sposoby:

  1. Pierwszy, pokazany powyżej, polega na umieszczeniu ich kolejno po identyfikatorze metody.
  2. Drugi polega na umieszczeniu ich w tablicy unii jvalue i przekazaniu wskaźnika jvalue* jako trzeciego argumentu funkcji wołającej. Nazwa funkcji wołającej w ten sposób (poprzez 3 argumenty) metodę jest taka jak w pierwszym przypadku z dodaną literą A na końcu, np.:
    jtyp CallTypMethodA(jobject obj, jmethodID methodID, jvalue * args);
    
    Unia jvalue jest zdefiniowana w pliku nagłówkowym jni.h tak:
    typedef union jvalue {
        jboolean z;
        jbyte    b;
        jchar    c;
        jshort   s;
        jint     i;
        jlong    j;
        jfloat   f;
        jdouble  d;
        jobject  l;
    } jvalue;
    
  3. Trzeci sposób przekazania argumentów wykorzystuje listę typu va_list (zadeklarowaną w cstdarg lub stdarg.h). Nazwa funkcji wołającej zakończona jest wtedy literą V:
    jtyp CallTypMethodV(jobject obj, jmethodID methodID, va_list args);
    

4.4.3. Wywoływanie metody z klasy bazowej

Jeśli zachodzi potrzeba wywołania metody z klasy bazowej, która została przesłonięta w danej klasie, należy posłużyć się jedną z metod zawierających w nazwie słowo Nonvirtual.

jtyp CallNonvirtualTypMethod(jobject obj, jclass clazz, jmethodID methodID, ...);
Argumenty w tym przypadku to:
  1. Odniesienie do obiektu, na rzecz którego wywołujemy metodę
  2. Odniesienie do klasy bazowej (nadklasy) dla klasy tego obiektu.
  3. Identyfikator metody pozyskany z klasy bazowej (jako pierwszy argument funkcji zwracającej identyfikator należy podać odniesienie do nadklasy).
  4. Argumenty dla wołanej metody.
W ten sposób nie można wywołać metod statycznych klas bazowych.

Mechanizm ten jest odpowiednikiem wywołania super.fun(); w Javie.

4.4.4. Przykład

Metoda natywna callMethod(short calls) z klasy JNIRules będzie wywoływać dwie metody:

W trakcie wywołań będzie wypisywać numer wywołania (poziom rekurencji) oraz czas zwracany przez metodę System.currentTimeMillis. Aby uniknąć wielokrotnego pozyskiwania identyfikatorów tych metod (będą takie same przy każdym wywołaniu), zapamiętamy je na statycznych (w sensie C++) zmiennych lokalnych funkcji natywnej.

JNIEXPORT void JNICALL 
Java_JNIRules_callMethod(JNIEnv * env, jobject self, jshort calls)
{
  static jmethodID sys_curtime_id = NULL;
  static jmethodID this_method_id = NULL;

  jclass this_cls = env->GetObjectClass(self);
  jclass sys_cls = env->FindClass("java/lang/System");
  if (sys_cls == NULL)
    return;

  if (this_method_id == NULL){
    this_method_id = 
	  env->GetMethodID(this_cls, "callMethod", "(S)V");
    if (this_method_id == NULL)
      return;
  }
  if (sys_curtime_id == NULL){
    sys_curtime_id = 
	  env->GetStaticMethodID(sys_cls, "currentTimeMillis", "()J");
    if (sys_curtime_id == NULL)
      return;
  }
  jlong now = env->CallStaticLongMethod(sys_cls, sys_curtime_id);
  cout << "calls = " << calls << "\ttime = " << (long)now << endl;

  if (calls > 0)
    env->CallVoidMethod(self, this_method_id, calls-1);
}

Na początku pobieramy odniesienia do klasy JNIRules (klasy obiektu this) oraz do klasy System. Ponieważ metoda FindClass może zwrócić NULL, jeśli klasy o podanym deskryptorze nie znaleziono, trzeba to sprawdzić i ewentualnie opuścić funkcję natywną (zostanie wtedy zgłoszony wyjątek NoClassDefFoundError lub inny - zależnie od przyczyny).
Dalej pobieramy identyfikatory metod. Robimy to tylko przy pierwszym wywołaniu funkcji natywnej. Oczywiście, jeśli identyfikatora nie udało się uzyskać, to wracamy z funkcji.

calls = 4 time = 1052435405305
calls = 3 time = 1052435405315
calls = 2 time = 1052435405325
calls = 1 time = 1052435405345
calls = 0 time = 1052435405355

Kiedy wszystko jest gotowe możemy przystąpić do wywołań. Najpierw pobieramy aktualny czas - jest to liczba (typu long) sekund, które upłynęły od 1.01.1970 - i wyprowadzamy go na standardowe wyjście razem z numerem wywołania calls metody natywnej callMethod. Potem, jeśli numer wywołania jest dodatni, wywołujemy ją rekurencyjnie zmniejszając numer wywołania o 1.

W ramce widzimy efekt wywołania metody callMethod((short)4). Rzutowanie argumentu jest konieczne, gdyż literały liczbowe są typu int.

4.5. Tworzenie obiektów

Do tworzenia obiektów Javy służy funkcja

jobject NewObject(jclass clazz, jmethodID methodID, ...);
Zwraca ona odniesienie do nowego obiektu klasy podanej jako pierwszy argument. Nie może to być tablica. Drugim argumentem jest identyfikator metody dla konstruktora, którego chcemy użyć do skonstruowania obiektu. Można go uzyskać w zwykły sposób podając funkcji GetMethodID jako nazwę metody <init>, oraz "V" jako nazwę typu zwracanego w deskryptorze metody. Konstruktor musi być zdefiniowany w klasie clazz (a nie w jej nadklasie).
Argumenty konstruktora podaje się jak przy wywoływaniu metod. W szczególności istnieją dodatkowe funkcje NewObjectV i NewObjectA pozwalające przekazać argumenty jako listę va_list bądź tablicę elementów typu jvalue.

Przykład

W klasie JNIRules znajduje się dokładnie jeden konstruktor zadeklarowany jako private. Oznacza to, że niemożliwe jest utworzenie obiektu tej klasy w metodzie innej klasy. Do tworzenia obiektów naszej klasy wykorzystamy statyczną natywną metodę instanceCreation(). Będzie ona wywoływać konstruktor, przekazując mu argument typu boolean.

  native static JNIRules instanceCreation();
  
  private JNIRules(boolean b){
    instanceField = b;
  }

Oto implementacja metody natywnej:

JNIEXPORT jobject JNICALL 
Java_JNIRules_instanceCreation(JNIEnv * env, jclass clazz)
{
  jmethodID constr_id = 
    env->GetMethodID(clazz, "<init>", "(Z)V");
  if (constr_id == NULL)
    return NULL;
  jobject result = env->NewObject(clazz, constr_id, JNI_TRUE);
  return result;
}
Jako nazwę metody przy pobieraniu identyfikatora podajemy "<init>". Deskryptorem konstruktora jest "(Z)V" - ma on parametr typu boolean, a jako typ wartości zwracanej dla konstruktorów podaje się V. Obowiązkowo sprawdzamy, czy udało się uzyskać identyfikator - w przeciwnym wypadku opuszczamy funkcję natywną i na poziomie Javy zostanie zgłoszony wyjątek. Następnie tworzymy obiekt funkcją NewObject podając jako argumenty:
  1. odniesienie do klasy JNIRules (przekazane jako argument do funkcji natywnej, bo implementuje ona metodę statyczną)
  2. identyfikator metody dla używanego konstruktora
  3. argument dla konstruktora - wartość logiczną JNI_TRUE, która zostanie przekształcona w true na poziomie Javy.

W metodzie main tworzymy obiekt naszej klasy tak:

JNIRules rules = instanceCreation();
Poza tą klasą trzeba oczywiście poprzedzić nazwę metody statycznej nazwą klasy.


5. Zaawansowane szczegóły i zastosowania

W tej części zostaną przedstawione zaawansowane aspekty technologii JNI, takie jak obsługa wyjątków z poziomu natywnego czy programowanie wielowątkowe. Pokażemy też różnice w implementacji funkcji natywnych, jakie występują pomiędzy językami C i C++. Najpierw jednak kilka uwag dotyczących posługiwania się obiektami Javy przy pomocy odniesień, które są przekazywane jako argumenty funkcjom natywnym, a także zwracane przez niektóre funkcje JNI.

5.1. Uwagi o odniesieniach

Na poziomie natywnym obiekty Javy są reprezentowane przy pomocy odniesień. Są one tak naprawdę wskaźnikami do wewnętrznych struktur maszyny wirtualnej, lecz jest to ukryte przed programistą i dostęp do obiektów odbywa wyłącznie poprzez funkcje JNI (patrz też część dot. odwzorowywania typów obiektowych).

Owe odniesienia mają pewne cechy odniesień Javy: wiążą zmienną z obiektem znajdującym się w obszarze pamięci dynamicznej Javy zarządzanym przez odśmiecacz. W związku z tym, dopóki do obiektu istnieją jakieś odniesienia natywne, to nie może on być usunięty z pamięci. Podobnie jak w Javie, przy opuszczaniu ciała funkcji natywnej maszyna wirtualna usuwa wszystkie odniesienia lokalne, które zostały tam utworzone, więc nie musimy się o nie martwić. Jeśli jednak chcemy, aby nasz kod natywny był efektywny pamięciowo i niezawodny, to niezbędne jest bezpośrednie operowanie odniesieniami. Wiedza na temat odniesień jest też konieczna do tworzenia globalnych zmiennych odnośnikowych w celu przekazywania ich pomiędzy różnymi wątkami czy wywołaniami funkcji natywnych.

Są trzy rodzaje odniesień:

5.1.1. Odniesienia lokalne

Większość funkcji JNI wywoływanych w funkcjach natywnych tworzy odniesienia lokalne (np. NewObject tworzy nowy obiekt i zwraca lokalne odniesienie do niego). Są one automatycznie usuwane po wyjściu z funkcji natywnej. W związku z tym, nie można ich przechowywać pomiędzy wywołaniami funkcji natywnych na zmiennych globalnych, czy też statycznych zmiennych zadeklarowanych w tych funkcjach. Jeśli chcemy uniknąć powtarzania tych samych, kosztownych operacji w celu uzyskania odniesienia do jakiegoś obiektu używanego w funkcji, to należy zapamiętać je na odniesieniu globalnym.
Oczywiście można zwracać lokalne odniesienie jako rezultat metody natywnej. Mimo, iż zostanie ono usunięte przy jej opuszczeniu, to na poziomie Javy będziemy dysponować odniesieniem przekazanym jako wynik i dzięki temu utworzony obiekt nie zostanie usunięty przez odśmiecacz.

Używanie odniesienia lokalnego (na poziomie natywnym) po zakończeniu funkcji natywnej, w której zostało ono uzyskane, jest niedozwolone.

Oprócz tego, że odniesienia lokalne są zwalniane automatycznie przy wyjściu z funkcji natywnej, można je również zwalniać funkcją

void DeleteLocalRef(jobject localRef);
Po co zwalniać lokalne odniesienie, skoro zostanie ono i tak zwolnione po wyjściu z funkcji natywnej? Jego istnienie uniemożliwia usunięcie obiektu przez odśmiecacz. Zatem w następujących sytuacjach może to być konieczne: Dalej zobaczymy przykłady zastosowania powyższych funkcji.

5.1.2. Odniesienia globalne

Globalne odniesienie można uzyskać wyłącznie funkcją:

jobject NewGlobalRef(jobject obj);
Parametr obj może być dowolnym odniesieniem (lokalnym, globalnym albo słabym).

Uzyskane w ten sposób odniesienie do obiektu Javy może być przekazywane pomiędzy wywołaniami różnych funkcji natywnych i różnymi wątkami. Kiedy nie jest już potrzebne, musi zostać uwolnione funkcją

void DeleteGlobalRef(jobject gref);

W przeciwieństwie do odniesień lokalnych, które są zwracane przez większość funkcji JNI, odniesienia globalne można uzyskać tylko w jeden sposób i nie są one automatycznie zwalniane. Można ich używać do momentu ręcznego uwolnienia.

5.1.3. Odniesienia słabe

Do tworzenia i zwalniania odniesień słabych służą funkcje:

jweak NewWeakGlobalRef(jobject obj);
void DeleteWeakGlobalRef(jobject wref);
jweak jest inną nazwą na jobject

Usuwać słabe odniesienie, przekazane jako argument wref, można tylko jeden raz.

Podobnie do odniesień globalnych, odniesienia słabe mogą być przekazywane pomiędzy różnymi wywołaniami funkcji natywnych oraz pomiędzy wątkami. W przeciwieństwie do nich, obiekty do których się odnoszą mogą zostać usunięte z pamięci przez odśmiecacz. Wartość słabego odniesienia będzie wtedy ustalona na NULL.

W Javie również są odniesienia słabe o podobnych właściwościach. Są to klasy pakietu java.lang.ref.

Słabe odniesienia stosuje się głównie do przetrzymywania odniesień do klas (jclass). Dzięki temu, klasy mogą zostać odładowane, kiedy nie są już potrzebne.

5.1.4. Porównywanie obiektów

Do porównywania odniesień do obiektów służy funkcja

jboolean IsSameObject(jobject ref1, jobject ref2);
Jej działanie jest analogiczne do operatora == Javy. Jako wynik zwraca JNI_TRUE lub JNI_FALSE, zależnie od tego, czy ref1 i ref2 odnoszą się do tego samego obiektu, czy też nie.
Porównanie z argumentem NULL jest równoważne porównaniu z odniesieniem null Javy.

5.2. Przykład: mnożenie macierzy

Jako ilustrację omówionych do tej pory zagadnień, rozważymy program mnożący macierze. Klasa Matrix reprezentująca macierze zawiera jako atrybut dwuwymiarową tablicę liczb typu double. Natywna metoda Matrix mul(Matrix m) mnoży macierz reprezentowaną przez this i macierz dostarczoną jako argument tej metody (jest on prawym argumentem mnożenia) zwracając iloczyn (nowy obiekt) jako wynik.

Przy okazji zobaczymy drugi sposób zapamiętywania często używanych zmiennych natywnych, takich jak identyfikatory pól i metod, czy odniesienia do klas. Pierwszy, polegający na użyciu zmiennych statycznych w funkcji, jest przedstawiony w przykładzie z punktu 4.4 poświęconego wywoływaniu metod. Opis obu sposobów znajduje się w ramce w punkcie 4.3 poświęconym odwołaniom do atrybutów.
Druga metoda polega na zapamiętaniu na zmiennych globalnych poziomu natywnego często używanych wartości w momencie ładowania klasy. Wymaga to wywołania metody natywnej, która je obliczy, w bloku statycznym klasy po instrukcji ładującej bibliotekę dynamiczną. Oczywiście taką metodę natywną należy zaimplementować jak każdą inną metodę natywną.

Rozpoczynamy od przedstawienia głównej części klasy Matrix.

public class Matrix {

  static {
    System.loadLibrary("matrix");   
    initNative();                   
  }

  private double[][] matrix;

  public Matrix(double[][] matr){
    matrix = matr;
  }

  private static native void initNative();

  public native Matrix mul(Matrix arg);

  public static void main(String[] args){
    int dim = Integer.parseInt(args[0]);
    Matrix a = randomatrix(dim, dim);
    Matrix b = randomatrix(dim, dim);
    Matrix c = a.mul(b);
    System.out.println(a);
    System.out.println(b);
    System.out.println(c);
  }

Na początku, w bloku statycznym ładujemy bibliotekę dynamiczną zawierającą implementacje metod natywnych. Następnie wywołujemy prywatną, statyczną metodę natywną initNative(), która zainicjuje zmienne globalne: identyfikator pola matrix, identyfikator metody mul oraz globalne odniesienia do klasy Matrix i klasy tablicy double[], która będzie potrzebna podczas mnożenia.
Pole matrix jest macierzą, którą będziemy mnożyć, reprezentowaną jako dwuwymiarowa tablica liczb rzeczywistych. Konstruktor pobiera taką tablicę jako argument i na jej podstawie tworzy obiekt. Ten konstruktor będzie wywoływany w metodzie mnożącej macierze mul do zwrócenia wyniku w postaci nowego obiektu klasy Matrix. Lewym argumentem mnożenia będzie macierz reprezentowana przez this, natomiast prawym - przez argument arg tej metody.
Prywatna metoda natywna initNative() jest statyczna, ponieważ będzie wywołana w momencie, kiedy nie istnieje jeszcze żaden obiekt tej klasy. Wykonuje ona zresztą czynności właściwe dla całej klasy, a nie dla konkretnego jej obiektu.
W metodzie startowej main tworzymy dwie macierze kwadratowe o wymiarach podanych jako argument wywołania programu, a następnie je mnożymy. Do utworzenia obiektów wykorzystujemy funkcję randomatrix(), która tworzy tablicę double[][], wypełnia ją liczbami (całkowitymi od 0 do 5 - aby było łatwiej analizować wyniki) i przekazuje ją do konstruktora zwracając utworzony obiekt. Po wymnożeniu macierzy wyprowadzamy je na konsolę - do tego jest potrzebna metoda toString().

  public static Matrix randomatrix(int rows, int cols){
    double[][] mtr = new double[rows][cols];
    java.util.Random rand = new java.util.Random();
    for(int r = 0; r < mtr.length; r++)
      for(int c = 0; c < mtr[0].length; c++)
        mtr[r][c] = rand.nextInt(5);
      return new Matrix(mtr);
  }

  public String toString(){
    StringBuffer retsbf = 
	  new StringBuffer(4*matrix.length*matrix[0].length);
    for(int r = 0; r < matrix.length; r++){
      for(int c = 0; c < matrix[r].length; c++){
        retsbf.append(matrix[r][c]);
        retsbf.append(' ');
      }
      retsbf.append('\n');
    } 
    return retsbf.toString();
  }
}

Po kompilacji generejumy plik nagłówkowy Matrix.h poleceniem javah -jni Matrix

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Matrix */

#ifndef _Included_Matrix
#define _Included_Matrix
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Matrix
 * Method:    initNative
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Matrix_initNative
  (JNIEnv *, jclass);

/*
 * Class:     Matrix
 * Method:    mul
 * Signature: (LMatrix;)LMatrix;
 */
JNIEXPORT jobject JNICALL Java_Matrix_mul
  (JNIEnv *, jobject, jobject);

#ifdef __cplusplus
}
#endif
#endif
Odczytujemy z niego nazwy funkcji natywnych i deskryptory metod. Metoda mul ma parametr i wynik typu Matrix, więc jej deskryptor wygląda tak (LMatrix;)LMatrix;.

Implementacja powyższych funkcji znajduje się w pliku Matrix.cpp (jego nazwa może być dowolna). Oto jego początek:

#include <jni.h>
#include "Matrix.h"

jfieldID  matrix_fid = NULL;  // identyfikator pola matrix
jmethodID constr_mid = NULL;  // identyfikator konstruktora
jclass    matrix_cls = NULL;  // odniesienie globalne do klasy Matrix
jclass    dblarr_cls = NULL;  // odniesienie globalne do klasy double[]

Uzyskiwanie identyfikatorów pól i metod jest czasochłonne, dlatego są one obliczane tylko raz przez funkcję inicjującą i zapamiętane na zmiennych globalnych. Odniesienia do klas (również obliczane przez funkcję inicjującą) mogą być przechowywane na zmiennych globalnych, ponieważ są to odniesienia globalne (wynika to ze sposobu ich uzyskania, a nie typu). Odniesień lokalnych tak przechowywać nie wolno.

Klasa Matrix jest zapamiętana na globalnym odniesieniu matrix_cls, zatem nie zostanie nigdy odładowana. W związku z tym, identyfikatory pola i metody nie zostaną zdezaktualizowane.
JNIEXPORT void JNICALL 
Java_Matrix_initNative(JNIEnv * env, jclass mcls)
{
  matrix_fid = env->GetFieldID(mcls, "matrix", "[[D");
  if (matrix_fid == NULL)
    return;
  constr_mid = env->GetMethodID(mcls, "", "([[D)V");
  if (constr_mid == NULL)
    return;
  jclass dbarr = env->FindClass("[D");
  if (dbarr == NULL)
    return;
  matrix_cls = (jclass)env->NewGlobalRef(mcls);
  dblarr_cls = (jclass)env->NewGlobalRef(dbarr);
}

Funkcja natywna Java_Matrix_initNative implementuje metodę initNative. Ponieważ jest to metoda statyczna, jej drugim argumentem jest (lokalne) odniesienie do klasy Matrix. Najpierw obliczane są identyfikatory pola matrix i konstruktora, a następnie odniesienie do klasy tablic typu double[]. Pobranie globalnych odniesień znajduje się na końcu, na wypadek gdyby któraś z wcześniejszych operacji się nie powiodła. Nie musimy dzięki temu zwalniać odniesień, co byłoby konieczne w przypadku wystąpienia błędu i konieczności przedwczesnego opuszczenia ciała tej funkcji.
Dla funkcji pobierających identyfikatory, błędy mogą pojawić się w przypadku podania niewłaściwej nazwy bądź deskryptora. Podobnie jest z funkcją FindClass.

Funkcja natywna implementująca metodę mul() korzysta z dwóch funkcji pomocniczych. Pierwsza z nich o nazwie clean() służy do zwalniania dynamicznie alokowanych tablic języka C++. Ponieważ zwalnianie pamięci dynamicznej jest konieczne nie tylko po zakończeniu jej używania, ale również w przypadku wystąpienia błędów zmuszających do opuszczenia ciała funkcji przed wykonaniem zadania, więc aby nie powtarzać tych samych wierszy kodu w kilku miejscach został on umieszczony w funkcji pomocniczej. Nie jest to funkcja natywna, ani funkcja z biblioteki JNI, dlatego nie posiada standardowego dla nich zestawu parametrów: JNIEnv* i jobject lub jclass.

void clean(double ** thisMatrix, double ** margMatrix, double * rowArr)
{
    delete [] thisMatrix[0];
    delete [] thisMatrix;
    delete [] margMatrix[0];
    delete [] margMatrix;
    delete [] rowArr;
}

Druga funkcja pomocnicza służy do wyciągnięcia z obiektu klasy Matrix macierzy przechowywanej w tablicy i zwrócenia jej na wskaźniku do dynamicznie zaalokowanego obszaru pamięci (zwalnianego potem przez clean()). Jako dodatkowy wynik, na argumentach referencyjnych rows i cols przekazuje wymiary tablicy. Funkcja getMatrix dostaje jako argument wskaźnik do otoczenia JNIEnv * env z funkcji ją wywołującej, ponieważ wywołuje ona funkcje JNI. Drugim parametrem jest obiekt klasy Matrix, z którego wyciągamy macierz.

Najpierw na zmienną marray pobieramy wartość atrybutu matrix jako tablicę (natywną) obiektów. Przechowuje ona wiersze macierzy. Z tej tablicy pobieramy kolejne elementy jako jednowymiarowe tablice liczb rzeczywistych i kopiujemy do zaalokowanego obszaru pamięci dynamicznej. Pamięć dynamiczna jest zorganizowana w ten sposób, że w ciągłym jej obszarze mbody przechowujemy wiersze macierzy ułożone kolejno, natomiast wskaźniki do ich początków są trzymane w tablicy matrix, która będzie zwrócona jako wynik.
Każdy wiersz macierzy pobieramy z głównej tablicy marray i kopiujemy do pamięci dynamicznej, po czym zwalniamy odniesienie do pobranego elementu tablicy głównej. Dzięki temu nie trzeba zwiększać limitu 16 odniesień lokalnych przysługującemu funkcjom natywnym, zaoszczędzimy też pewną ilość pamięci.
Pierwszy wiersz przetwarzamy poza pętlą, ponieważ musimy poznać liczbę kolumn macierzy, aby zaalokować odpowiednią ilość pamięci dynamicznej.
Na końcu usuwamy lokalne odniesienie do składowej obiektu, ponieważ jest to funkcja pomocnicza, a nie natywna i w związku z tym, po jej zakończeniu lokalne odniesienia nie będą automatycznie zwalniane.

Lokalne odniesienia utworzone w funkcjach pomocniczych muszą być jawnie uwalniane funkcjami bibliotecznymi.

Co prawda, po wyjściu z metody natywnej, która wywołała funkcję pomocniczą, utworzone przez tą funkcję odniesienia lokalne zostaną zwolnione - często nie wiadomo jednak ile razy funkcja pomocnicza będzie wywoływana (i skąd), zatem powinna zostawiać środowisko w stanie sprzed wywołania.

double ** getMatrix(JNIEnv * env, jobject matr, int& rows, int& cols)
{
  jobjectArray marray = 
    (jobjectArray)env->GetObjectField(matr, matrix_fid);
  rows = env->GetArrayLength(marray);

  jdoubleArray subarr = 
    (jdoubleArray)env->GetObjectArrayElement(marray, 0);
  cols = env->GetArrayLength(subarr);

  double ** matrix = new double*[rows];
  double *  mbody  = new double[rows*cols]; 

  matrix[0] = mbody;
  env->GetDoubleArrayRegion(subarr, 0, cols, matrix[0]);
  env->DeleteLocalRef(subarr);

  for (int i = 1; i < rows; i++){
    subarr = (jdoubleArray)env->GetObjectArrayElement(marray, i);
    matrix[i] = mbody+i*cols;
    env->GetDoubleArrayRegion(subarr, 0, cols, matrix[i]);
    env->DeleteLocalRef(subarr);           
  }
  env->DeleteLocalRef(marray);           
  return matrix;
}

Powyższa funkcja jest wywoływana na początku funkcji natywnej Java_Matrix_mul dla obu argumentów mnożenia: reprezentowanego przez this i parametr metody mul(). Jako rezultat tych wywołań będziemy mieli, oprócz dwuwymiarowych dynamicznych tablic z zawartościami macierzy, również ich wymiary przekazane przez referencje. Po uzyskaniu tych tablic, tworzymy tablicę Javy matrixArr przeznaczoną na wynik. Jest to tablica obiektów, poszczególne jej elementy będą również tablicami - wierszami macierzy wynikowej. W razie niepowodzenia przy tworzeniu nowego obiektu musimy zakończyć działanie tej funkcji (zostanie wtedy zgłoszony wyjątek na poziomie Javy), ale przed tym należy zwolnić zaalokowane tablice dynamiczne.
Przed przystąpieniem do mnożenia, alokujemy jeszcze miejsce na bufor rowArr dla aktualnie wyliczanego wiersza. Po obliczeniu iloczynu dla kolejnego wiersza tworzymy tablicę Javy (dblArr, typu double[]), do której kopiujemy wyliczony wiersz (sprawdzając przed tym, czy udało się tablicę Javy utworzyć). Utworzoną tablicę dblArr zapisujemy jako kolejny wiersz wynikowej tablicy obiektów matrixArr, po czym usuwamy lokalne odniesienie do bieżącego wiersza-obiektu Javy. Zaniechanie usuwania odniesień lokalnych w tym przypadku doprowadziłoby do przekroczenia limitu odniesień, bowiem dla każdego wiersza tworzy się nowe.
Po utworzeniu tablicy wynikowej sprzątamy stertę i tworzymy nowy obiekt klasy Matrix podając tablicę wynikową konstruktorowi jako argument. Pozostałe argumenty dla funkcji NewObject, czyli odniesienie do klasy i identyfikator konstruktora, zostały obliczone w funkcji inicjującej. Nową macierz zwracamy jako wynik metody natywnej.

 
JNIEXPORT jobject JNICALL 
Java_Matrix_mul(JNIEnv * env, jobject self, jobject marg)
{
  int thisRows, thisCols, margRows, margCols;  // wymiary macierzy
  double ** thisMatrix = getMatrix(env, self, thisRows, thisCols);
  double ** margMatrix = getMatrix(env, marg, margRows, margCols);

  if (thisCols != margRows){                    // czy mnożenie jest wykonalne
    clean(thisMatrix, margMatrix, NULL);
    return NULL;
  }
                                                  
  jobjectArray matrixArr =                     // pole matrix wyniku
    env->NewObjectArray(thisRows, dblarr_cls, NULL);
  if (matrixArr == NULL){
    clean(thisMatrix, margMatrix, NULL);
    return NULL;
  }
  double * rowArr = new double[margCols];      // wiersz macierzy wynikowej
  for (int r = 0; r < thisRows; r++){          // mnożenie ...
    for (int c = 0; c < margCols; c++){
      rowArr[c] = 0;
      for (int m = 0; m < thisCols; m++)
        rowArr[c] += thisMatrix[r][m]*margMatrix[m][c];
    }
    jdoubleArray dblArr = env->NewDoubleArray(margCols);
    if (dblArr == NULL){
      clean(thisMatrix, margMatrix, rowArr);
      return NULL;
    }
    env->SetDoubleArrayRegion(dblArr, 0, margCols, rowArr);
    env->SetObjectArrayElement(matrixArr, r, dblArr);
    env->DeleteLocalRef(dblArr);
  }
  clean(thisMatrix, margMatrix, rowArr);

  return env->NewObject(matrix_cls, constr_mid, matrixArr);
}

5.3. Wyjątki

Obsługa wyjątków na poziomie natywnym różni się od ich obsługi w Javie. Podstawowa różnica polega na tym, że fakt zgłoszenia błędu przez maszynę wirtualną, czy też wyjątku przez wywoływaną z kodu natywnego metodę Javy, nie powoduje automatycznego przerwania wykonywania funkcji natywnej. Jeśli nie zrobimy tego sami (instrukcją return), to ciało funkcji będzie wykonywane tak, jakby nic się nie stało, a zgłoszony wyjątek będzie oczekiwał na ewentualne przechwycenie. Jeśli do niego nie dojdzie, to po opuszczeniu metody natywnej zostanie zgłoszony na poziomie Javy. Jednak wywoływanie funkcji JNI w sytuacji, gdy jakiś wyjątek oczekuje na przechwycenie będzie prowadzić do nieprzewidywalnych błędów (ograniczenie to nie dotyczy oczywiście funkcji potrzebnych do obsługi wyjątków). Dlatego o kontrolę sterowania w sytuacjach wyjątkowych musi troszczyć się programista.

Jeśli został zgłoszony wyjątek, należy albo go przetworzyć (przechwycić), albo natychmiast opuścić funkcję natywną, koniecznie zwalniając zajęte w ciele funkcji zasoby: odniesienia i bloki pamięci dynamicznej.

Ale jak sprawdzić, czy został zgłoszony wyjątek?

Funkcje JNI stosują oba sposoby jednocześnie, o ile jest to możliwe (funkcja zwraca wartość).
Badanie wartości zwracanej jest efektywniejsze niż wywoływanie poniższych funkcji sprawdzających - a zatem zalecane.

Są dwa sposoby informowania wołającego funkcję o zajściu sytuacji wyjątkowej:

W pierwszym przypadku po prostu sprawdzamy wartość zwracaną. W drugim trzeba wywołać jedną z poniższych funkcji, aby przekonać się, czy został zgłoszony wyjątek.

jthrowable ExceptionOccurred();
jboolean ExceptionCheck();

Pierwsza z nich zwraca lokalne odniesienie do obiektu wyjątku, który został zgłoszony. Jeśli zwróconą wartością jest NULL, to wyjątku nie było. Druga zwraca JNI_TRUE, jeśli wyjątek został zgłoszony i JNI_FALSE w przeciwnym przypadku.

Jeśli jakaś funkcja JNI zwróci wartość oznaczającą błąd (przeważnie NULL), to następujące po niej wywołanie ExceptionCheck zwróci JNI_TRUE.

Powyższe funkcje nie powodują przechwycenia wyjątku - sprawdzają jedynie, czy został zgłoszony (tak jak sprawdzenie wartości zwracanej). Jeśli nie zostanie on przechwycony, to po opuszczeniu metody natywnej zostanie zgłoszony w kodzie Javy. Aby tego uniknąć, należy wywołać jedną z funkcji służących do przechwytywania wyjątków:

void ExceptionClear();
void ExceptionDescribe();
Druga z nich, tak jak pierwsza powoduje skonsumowanie wyjątku, ale oprócz tego wyprowadza na wyjście diagnostyczne zawartość stosu (tak jak metoda printStackTrace() z klasy Throwable).

Przedstawione wyżej cztery funkcje pełnią rolę frazy catch w Javie. Po wywołaniu funkcji mogącej skutkować zgłoszeniem wyjątku (również wywołaniu metody Javy) należy najpierw sprawdzić, czy wyjątek został zgłoszony a następnie go przechwycić lub opuścić funkcję, pamiętając o zwalnianiu zasobów. Odpowiednikiem bloku try są instrukcje poprzedzające obsługę wyjątku. Nie ma jednak określonego początku takiego bloku. Najlepiej, jeśli składa się on z wywołania jednej funkcji, tzn. po każdym wywołaniu mogącym skutkować wyjątkiem umieszczamy instrukcje przechwytujące.

Ignorowanie zgłoszonych wyjątków i wywoływanie w tej sytuacji innych funkcji JNI może prowadzić do błędów.

Zwykle, obsługa wyjątków sprowadza się do następującego schematu:

env->CallVoidMethod(obj, mid);      // wywołanie metody Javy
exc = env->ExceptionOccurred();     // czy zgłosiła wyjątek 
if (exc) {                          // jeśli tak   
  env->ExceptionClear();            // przechwycenie wyjątku
  /*
   *                                // obsługa sytuacji wyjątkowej
   */                               // zgłoszonej przez metodę Javy
}  
Po wywołaniu metody sprawdzamy, czy nie został zgłoszony wyjątek. Jeśli tak, to go konsumujemy i wykonujemy czynności związane z zaistniałą sytuacją.

Można też tak:

const jchar * cstr = env->GetStringChars(jstr);
if (cstr == NULL)  
  return;
W tym przypadku, wracając z funkcji natywnej po zgłoszeniu wyjątku (zwrócona wartość NULL o tym świadczy) wymuszamy jego obsługę na poziomie Javy.

Do zgłaszania wyjątków z poziomu natywnego służą funkcje

jint Throw(jthrowable obj);
jint ThrowNew(jclass clazz, const char * message);
Jako wynik obie zwracają 0, jeśli wyjątek udało się zgłosić i wartość ujemną w przeciwnym przypadku.
Pierwsza jako argument obj pobiera istniejący obiekt wyjątku, natomiast druga tworzy nowy obiekt wyjątku klasy clazz z opisem message (może być pusty - NULL). Klasa wyjątku (w obu przypadkach) musi być podklasą java.lang.Throwable.

Zgłoszenie w ten sposób wyjątku nie powoduje zmiany przepływu sterowania. Należy opuścić funkcję natywną instrukcją return, aby wyjątek ujawnił się na poziomie Javy.

5.4. Napisy ze znakami narodowymi

Łańcuchy znakowe reprezentowane w systemie UTF-8 są trudne do przetwarzania, jeśli zawierają znaki narodowe. Jest tak dlatego, że znaki spoza kodu ASCII (w tym znaki narodowe) są reprezentowane na dwóch lub trzech bajtach. Jak napisać natywną funkcję, która będzie odwracała napis zawierający takie znaki? Skoro niektóre znaki w tablicy uzyskanej funkcją GetStringUTFChars mogą być wielobajtowe, to należałoby najpierw rozpoznać, które to są, a potem przestawiać zachowując kolejność bajtów w grupie. To oczywiście jest bardzo kłopotliwe. Ale przecież systemy kodowania takie jak iso-8859-2 czy cp-1250 przechowują znaki narodowe w jednym bajcie - czy nie można tego jakoś wykorzystać ? Można. Używając tablicy bajtów jako pośredniej reprezentacji łańcuchów znakowych i korzystając z faktu, że klasa String potrafi posługiwać się takimi tablicami, przy zastosowaniu określonego systemu kodowania (strony kodowej), napiszemy pomocnicze funkcje konwertujące. Będą one zamieniać napisy Javy na tablice znaków i odwrotnie, przy użyciu domyślnego dla platformy systemu kodowania.

5.4.1. Tworzenie łańcuchów Javy

Oto funkcja tworząca na podstawie podanej tablicy znaków i domyślnej strony kodowej nowy obiekt klasy String (na poziomie natywnym reprezentowany jako jstring).

jstring newNatString(JNIEnv * env, const char * str)
{
  if (env->EnsureLocalCapacity(2) < 0)   
    return NULL;
  int len = strlen(str);
  jbyteArray bytes = env->NewByteArray(len);
  if (bytes == NULL) 
    return NULL;
  env->SetByteArrayRegion(bytes, 0, len, (jbyte *)str);
  jstring natstr = 
    (jstring)env->NewObject(string_cls, constr_mid, bytes);
  env->DeleteLocalRef(bytes);
  return natstr;
}
Pierwszym parametrem tej funkcji jest wskaźnik do środowiska, na rzecz którego będą wywoływane funkcje JNI - zostanie on przekazany z funkcji natywnej wykorzystującej powyższą. Drugim jest tablica znaków, na podstawie której zostanie utworzony łańcuch Javy zwracany jako wynik.

Na początku rezerwujemy miejsce na lokalne odniesienia, które będą potrzebne w ciele funkcji. Jest to konieczne, ponieważ nie jest to implementacja metody natywnej, a więc nie ma ona swojej tablicy odniesień lokalnych, lecz korzysta z tablicy (a więc i limitu liczby odniesień) funkcji natywnej, która ją wywołała. Ponieważ nie wiadomo, czy wołająca funkcja natywna nie wykorzystała wszystkich swoich 16 odniesień, więc trzeba sobie zapewnić, że zostaną one przydzielone.
Dalej tworzymy tablicę bajtów, do której wpisujemy znaki z tablicy przekazanej jako argument. Ta tablica będzie argumentem konstruktora klasy String, którego użyjemy do wyprodukowania nowego obiektu. Interpretuje on elementy tablicy jako znaki w domyślnym systemie kodowania. Odniesienie globalne string_cls do klasy String, jak również identyfikator konstruktora constr_mid zostaną wyliczone i zapamiętane w innej funkcji. Przed zwróceniem obiektu usuwamy lokalne odniesienie do tablicy bytes. Drugiego utworzonego tu lokalnego odniesienia (natstr) nie usuwamy, ponieważ jest ono zwrócone jako wynik (będzie musiał się o to troszczyć wołający).

5.4.2. Pobieranie łańcuchów natywnych

Podobnie, za pośrednictwem tablicy bajtów, wyciągniemy z obiektu klasy String napis reprezentowany w C/C++ przez char*.

char * getNatChars(JNIEnv * env, jstring jstr)
{
  if (env->EnsureLocalCapacity(2) < 0) 
    return NULL; 
  jbyteArray bytes = 
    (jbyteArray)env->CallObjectMethod(jstr, gbytes_mid);
  jint len = env->GetArrayLength(bytes);
  char * nchars = new char[len+1];
  if (nchars == NULL) {      
    jclass cls = env->FindClass("java/lang/OutOfMemoryError");
    if (cls != NULL) // wpp. będzie zgłoszony NoClassDefFoundError         
      env->ThrowNew(cls, "");
    env->DeleteLocalRef(cls);
    env->DeleteLocalRef(bytes);
    return NULL;     
  }
  env->GetByteArrayRegion(bytes, 0, len, (jbyte *)nchars);
  nchars[len] = '\0';
  env->DeleteLocalRef(bytes);
  return nchars;
}

Na początku, podobnie jak poprzednio zapewniamy sobie możliwość utworzenia koniecznej liczby odniesień lokalnych. Następnie na rzecz obiektu klasy String przekazanego jako argument wywołujemy metodę getBytes(), która zwróci tablicę bajtów reprezentującą napis przy użyciu domyślnej strony kodowej. Identyfikator metody gbytes_mid został obliczony w innej funkcji. Alokujemy dynamiczną tablicę o długości o 1 większej niż tablica bajtów, ponieważ nie zawiera ona znaku '\0' kończącego napisy C/C++ (więc trzeba go dodać). Jeśli nie udało się zaalokować tablicy, należy zgłosić stosowny wyjątek i opuścić funkcję, zwalniając zajęte odniesienia lokalne. Jeśli alokacja się powiodła, kopiujemy zawartość tablicy bajtów do tablicy znaków dodając znak kończący napisy. Przed zwróceniem wyniku, zwalniamy odniesienie pozostawiając odpowiedzialność za tablicę dynamiczną wołającemu.

Powyższe funkcje konwertujące mogą być wykorzystane w dowolnym programie. Trzeba oczywiście obliczyć identyfikatory używanych metod oraz odniesienie do klasy String. Zrobi to funkcja implementująca metodę natywną initID z klasy NatChar. Będzie ona wywołana w bloku statycznym tej klasy zaraz po załadowaniu biblioteki dynamicznej z jej implementacją. Oprócz niej, w rzeczonej klasie znajdzie się metoda natywna odwracająca napisy, jako przykład zastosowania pokazanych wyżej funkcji. Kod funkcji inicjującej pokażemy wraz z fragmentem pliku NatChar.cpp, który zawiera implementacje metod natywnych tej klasy.

#include <jni.h>
#include <cstring>
#include <iostream>
#include "NatChar.h"

using namespace std;

char * getNatChars(JNIEnv * env, jstring jstr);
jstring newNatString(JNIEnv * env, const char * str);

jclass    string_cls;
jmethodID constr_mid;
jmethodID gbytes_mid;

JNIEXPORT void JNICALL 
Java_NatChar_initID(JNIEnv * env, jclass ntchr)
{
  jclass strcls = env->FindClass("java/lang/String");
  if (strcls == NULL)
    return;
  constr_mid = env->GetMethodID(strcls, "<init>", "([B)V");
  if (constr_mid == NULL)
    return;
  gbytes_mid = env->GetMethodID(strcls, "getBytes", "()[B");
  if (gbytes_mid == NULL)
    return;
  string_cls = (jclass)env->NewGlobalRef(strcls);
}

Najpierw pobieramy odniesienie lokalne do klasy String, z którego na końcu zostanie utworzone odniesienie globalne do przechowywania wartości pomiędzy wywołaniami funkcji. Dalej pobieramy identyfikator konstruktora String(byte[] bytes), którego deskryptorem jest "([B)V" oraz identyfikator metody byte[] getBytes(), o deskryptorze "()[B".

W klasie NatChar mamy jeszcze funkcję natywną reverse(), odwracającą napisy:

JNIEXPORT jstring JNICALL 
Java_NatChar_reverse(JNIEnv * env, jclass ntchr, jstring nstr)
{
  jsize lenUTF = env->GetStringUTFLength(nstr);
  jsize lenUNI = env->GetStringLength(nstr);
  cout << "dlugosc UTF:     " << lenUTF << endl
       << "dlugosc UNICODE: " << lenUNI << endl;

  char * natchr = getNatChars(env, nstr);
  int len = strlen(natchr);
  for(int i = 0; i < len/2; i++){
    char chr = natchr[i];
    natchr[i] = natchr[len-i-1];
    natchr[len-i-1] = chr;
  }
  jstring rstr = newNatString(env, natchr);
  delete [] natchr;
  return rstr;
}

Na początku pobieramy i wyprowadzamy na standardowe wyjście liczbę znaków przekazanego napisu oraz liczbę znaków potrzebną do jego reprezentacji UTF-8 (jeśli łańcuch zawiera znaki narodowe, to będzie większa, ze względu na znaki wielobajtowe spoza kodu ASCII).
Funkcją getNatChars() tworzymy łańcuch znakowy, który można przetwarzać w C/C++. Po odwróceniu go, zwracamy zbudowany na nim łańcuch Javy.

Klasa NatChar zawiera tylko opisane wyżej metody: initID oraz reverse(). W metodzie startowej main() pobierany jest łańcuch przy pomocy okna dialogowego i po odwróceniu wyświetlany.

import javax.swing.*;

public class NatChar {

  static {
    System.loadLibrary("natchr");
    initID();
  }
  native static private void initID();
  native static String reverse(String str);
    
  public static void main(String[] args){
    String msg = "Zażółć gęślą jaźń";
    String input = null;
    while((input = JOptionPane.showInputDialog("Podaj napis", msg)) 
           != null){
      String rev = reverse(input);
      JOptionPane.showMessageDialog(null, rev);
    }
    System.exit(0);
  }
}

5.5. Wątki

Z poziomu natywnego możliwa jest synchronizacja wątków przy pomocy funkcji:

jint MonitorEnter(jobject obj);
jint MonitorExit(jobject obj);
Wyznaczają one początek i koniec bloku synchronizowanego, w Javie wyznaczanego słowem kluczowym synchronized i nawiasami klamrowymi:
synchronized (obj) {     // MonitorEnter

}                        // MonitorExit 
Parametr obj jest obiektem-synchronizatorem (ryglem). Można przekazywać dowolne obiekty dowolnych typów, również jstring, jarray itp. Obie funkcje zwracają 0 ( ==JNI_OK), jeśli operacja się powiodła i wartość ujemną w przypadku niepowodzenia.

Każdemu wywołaniu MonitorEnter musi odpowiadać kończące sekcję krytyczną wywołanie MonitorExit. Jego brak prowadzi do blokady.

Biblioteka funkcji JNI nie zawiera odpowiedników metod klasy Object, służących do kontrolowania wątków: wait(), notify() czy notifyAll(). Jeśli zachodzi potrzeba ich użycia, to trzeba je wywołać jak normalną metodę Javy (poprzez Call...).

Korzystając z wielu wątków na poziomie natywnym należy pamiętać, że:

5.6. C a C++

Zaprezentowanie dotychczas przykłady napisane były w języku C++. Oczywiście do implementacji funkcji natywnych można posłużyć się również językiem C. Występują jednak dwie ważne różnice w użyciu funkcji i typów JNI pomiędzy tymi językami.

Wszystkie funkcje biblioteki są wywoływane na rzecz struktury JNIEnv poprzez wskaźnik do niej. W języku C funkcje te nie są jej funkcjami składowymi, lecz zwykłymi atrybutami typu wskaźnikowego. W związku z tym wywołanie funkcji w C++

Ta funkcja sprawdza czy obiekt object jest klasy clazz. Pełni więc rolę operatora instanceof Javy.
jboolean is = env->IsInstanceOf(object, clazz);
w języku C przyjmuje postać:
jboolean is = (*env)->IsInstanceOf(env, object, clazz);
Pierwszym parametrem wszystkich funkcji, nieobecnym w C++, jest wskaźnik do środowiska JNIEnv * env, na rzecz którego funkcja została wywołana. W C++ jest on obecny w ciele funkcji jako wskaźnik this, więc przekazywanie go jako argument jest zbędne.

W języku C wszystkie typy odnośnikowe są innymi nazwami na wskaźnik do struktury _jobject. Oto wyjątek z pliku jni.h:
struct _jobject;
typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jstring;
typedef jobject jarray;

Druga różnica dotyczy typów odnośnikowych. W języku C nie ma dziedziczenia, więc nie ma też żadnej zależności pomiędzy typami takimi jak: jobject, jclass, jarray (patrz punkt 3.4). Dzięki temu nie jest konieczne rzutowanie wyniku funkcji zwracających jobject, jak w przykładzie z mnożeniem macierzy (funkcja getMatrix()).
Tu marray jest dwuwymiarową tablicą, a poniższa instrukcja wydobywa z niej jednowymiarowy wiersz. Ponieważ funkcja zwraca obiekt typu jobject a wiemy, że jest on tablicą - musimy rzutować go na typ jarray przed pobraniem elementów.

jdoubleArray subarr = (jdoubleArray)env->GetObjectArrayElement(marray, 0);
W języku C powyższy fragment kodu wygląda tak:
jdoubleArray subarr = (*env)->GetObjectArrayElement(env, marray, 0);
Taki kod jest bardziej narażony na błędy, ponieważ kompilator nie sprawdza poprawności typów przekazywanych argumentów: jako argument marray typu jarray można przekazać dowolny jobject. Błędy pojawią się oczywiście w fazie wykonania.


Dokumentacja i literatura

Java Tutorial
Rozdział poświęcony JNI w podręczniku on-line Java Tutorial (wersja html).
The Java Native Interface Programmer's Guide and Specification
Obszerny podręcznik użytkownika, wraz ze specyfikacją zawierającą opis wszystkich funkcji. Jest dostępny w kilku formatach on-line (wersja html), jak również w wersji książkowej (Addison Wesley, 1999).
Java Native Interface Specification
Specyfikacja, trochę zdezaktualizowana, dostępna on-line (wersja html). Jest również rozprowadzana wraz z dokumentacją do SDK Javy.