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:
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.Łą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.
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"!.
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.
Kompilujemy plik HelloWorld.java zawierający naszą klasę z poziomu powłoki (wiersza poleceń):
javac HelloWorld.javalub 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ć.
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.
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.
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!");
.
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):
Po kompilacji w katalogu bieżącym powstaje biblioteka dynamiczna HelloWorld.dll dla platformy Win32, lub libHelloWorld.so dla Linuxa.
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(...)
.
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
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.
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()
int
long getNumber(long interval)
long
z przedziału [0..interval]float getNumber(float left, float right)
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.GetNumberPo 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.
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 */ }
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;"
.
System.out.println(new Object[1][1]); System.out.println(new int[1]);Wypisze coś takiego:
[[Ljava.lang.Object;@126b249 [I@182f0dbPrzed 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 Javy Deskryptor pola boolean
Z
byte
B
char
C
short
S
int
I
long
J
float
F
double
D
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.
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
.
void f()
long f(int i)
String f(short s, boolean b)
Class[][] f(byte[] t, byte b)
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
.
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ą:
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
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 Javy Typ C/C++ Rozmiar w bitach boolean
jboolean
8, unsigned byte
jbyte
8 char
jchar
16, unsigned short
jshort
16 int
jint
32 long
jlong
64 float
jfloat
32 double
jdouble
64 void
void
jchar
jest 16 bitowy i służy do reprezentowania znaków
Unicode na platformach, które mają wsparcie dla tego systemu kodowania.
jboolean
zdefiniowano dwa makra:
JNI_TRUE
odpowiada wartości true
Javy
JNI_FALSE
odpowiada wartości false
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; }
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ą.
Obiekty Javy są przekazywane na zmiennych typu jobject
.
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:
typedef _jobject * jobject.
_jobject
.
Do zmiennych powyższych typów nie można się odwoływać bezpośrednio. Konieczne jest użycie specjalnych funkcji, o czym dalej.
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:
this
z Javy, w przykładach nazywany self
.
java.lang.Class
reprezentujący tę klasę),
przekazywane na zmiennych typu jclass
.
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.
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);
obj
(jest on obiektem klasy zwracanej jako wynik).
name
(patrz deskryptory).
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[]
.
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.
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.
Ł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.
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.
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.
*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).
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.
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.
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.
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ć.
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);
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
.
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
void stringDemo(){ String userTyped = stringUse("Prompt>"); System.out.println("User typed: " + userTyped); }
Długość napisu można pobrać funkcjami
jsize GetStringLength(jstring str); jsize GetStringUTFLength(jstring str);
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
.
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).
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:
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ą.
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
.
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 JNI Typ Javy jbooleanArray
boolean[]
jbyteArray
byte[]
jcharArray
char[]
jshortArray
short[]
jintArray
int[]
jlongAraay
long[]
jfloatArray
float[]
jdoubleArray
double[]
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:
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.
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 |
jarraytype | Type |
---|---|---|
jboolean |
jbooleanArray |
Boolean |
jbyte |
jbyteArray |
Byte |
jchar |
jcharArray |
Char |
jshort |
jshortArray |
Short |
jint |
jintArray |
Int |
jlong |
jlongArray |
Long |
jfloat |
jfloatArray |
Float |
jdouble |
jdoubleArray |
Double |
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
.
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:
JNI_COMMIT
- zapisz zmiany, ale nie zwalniaj wskaźnika
JNI_ABORT
- zwolnij wskaźnik nie zapisując zmian
mode
z reguły podaje się 0.
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
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
.
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.
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.
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:
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]); }
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
.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.
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.
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.
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);
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:
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.
const char * name
jest nazwą atrybutu, pod jaką jest on zdefiniowany w klasie.
const char * sig
jest deskryptorem pola
(patrz deskryptory).
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:
Typ
jtyp
Boolean
jboolean
Byte
jbyte
Char
jchar
Short
jshort
Int
jint
Long
jlong
Float
jfloat
Double
jdouble
Object
jobject
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++
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ę.
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);
NULL
. Jeśli w tym momencie nastąpi powrót z funkcji
natywnej, to w Javie zostanie zgłoszony wyjątek NoSuchMethodError
.
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ę.
name
jest nazwą metody.
sig
jest deskryptorem tej metody
(patrz deskryptory). Ma on postać:"(typy-argumentów)typ-wyniku"
.
clazz
rzeczywistą klasę obiektu, z którego ją wołamy.
Nie może to być nadklasa ani interfejs (ponieważ metody prywatne nie są wirtualne).
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ę:
...
) 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++.
Typ
jtyp
Void
void
jobject obj
jest odniesieniem do obiektu, na rzecz którego
wołamy metodę niestatyczną, natomiast jclass clazz
jest klasą,
z której wywołujemy metodę statyczną.
jmethodID methodID
jest identyfikatorem metody....
, przekazujemy oddzielone przecinkami
argumenty do wołanej metody (o ile są jakieś).
Ogólnie, argumenty dla metody można przekazać na trzy sposoby:
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;
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);
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:
super.fun();
w Javie.
Metoda natywna callMethod(short calls)
z klasy JNIRules
będzie wywoływać dwie metody:
System.currentTimeMillis()
calls
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.
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.
callMethod((short)4)
.
Rzutowanie argumentu jest konieczne, gdyż literały liczbowe są typu int
.
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).NewObjectV
i NewObjectA
pozwalające przekazać argumenty
jako listę va_list
bądź tablicę elementów typu jvalue
.
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:
JNIRules
(przekazane jako argument do funkcji
natywnej, bo implementuje ona metodę statyczną)
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.
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.
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.
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:
FatalError
.
Zmienić ten limit można funkcją
jint EnsureLocalCapacity(jint capacity);
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);
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
.
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.
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. NULL
jest równoważne porównaniu z odniesieniem
null
Javy.
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 #endifOdczytujemy 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.
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.
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); }
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.
Są dwa sposoby informowania wołającego funkcję o zajściu sytuacji wyjątkowej:
NULL
)throw
,
na poziomie natywnym specjalną funkcją
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.
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.
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.
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
.
return
, aby wyjątek
ujawnił się na poziomie Javy.
Ł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.
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).
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); } }
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 } // MonitorExitParametr
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.
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:
JNIEnv* env
również jest właściwy tylko
dla wątku, w którym został pobrany jako parametr funkcji natywnej.
Nie wolno go przekazywać do innych wątków, nawet na odniesieniach globalnych.
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++
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.
_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.