28
Programowanie
Java
www.sdjournal.org
Software Developer’s Journal 8/2006
Java Native Interface
– łączenie Javy i C/C++
J
NI (ang. Java Native Interface) jest technologią
pozwalającą na wywoływanie z poziomu Javy
funkcji zaimplementowanych w C/C++. Imple-
mentacje tych funkcji muszą znajdować się w biblio-
tekach dynamicznych ładowanych na żądanie przez
maszynę wirtualną Javy. Z poziomu takich funkcji
– nazywanych funkcjami rodzimymi albo natywny-
mi (ang. native method) – mamy dostęp do elemen-
tów środowiska Javy: możemy wywoływać metody,
odwoływać się do atrybutów, tworzyć obiekty. Sa-
ma procedura zastosowania funkcji rodzimej i wywoły-
wania jej w programie jest bardzo prosta i opiszę ją za
chwilę na przykładzie. Pewnej wiedzy wymaga jedynie
odwoływanie się z poziomu takich funkcji do składo-
wych obiektów czy przetwarzanie tablic Javy. Szczegó-
ły zostaną wyjaśnione w dalszej części artykułu.
Szybki start
Napiszemy wersję JNI nieśmiertelnego programu
HelloWorld. Wykorzystamy funkcję rodzimą do wy-
świetlenia komunikatu.
Przygotowanie kodu źródłowego
Program składa się z klasy
HelloWorld
zawierającej
metodę
sayHello()
. Jak widać na Listingu 1, nie posia-
da ona ciała a jej deklaracja jest poprzedzona słowem
kluczowym
native
. Oznacza ono, że JVM będzie po-
szukiwać implementacji tej metody w załadowanych
bibliotekach dynamicznych. Takie metody nazywane
są metodami rodzimymi.
Musimy zatem taką bibliotekę załadować. Wykorzy-
stamy statyczną metodę
System.loadLibrary(String)
.
Jako argument pobiera ona nazwę biblioteki. Biblioteka
dynamiczna musi zostać załadowana przed pierwszym
odwołaniem do metody rodzimej. W związku z tym,
wywołanie metody
loadLibrary()
najlepiej jest umie-
ścić na początku metody
main()
, albo w bloku statycz-
nym – będzie on wykonany podczas ładowania klasy.
Kod klasy
HelloWorld
prezentuje Listing 1.
Kompilacja
Plik HelloWorld.java zawierający klasę
HelloWorld
kom-
pilujemy normalnie: javac HelloWorld.java Jednak nie
możemy jeszcze wykonać naszego programu. Próba
uruchomienia da rezultat widoczny na Rysunku 1. Wy-
jątek
UnsatisfiedLinkError
jest spowodowany brakiem
biblioteki dynamicznej o nazwie
HelloWorld
(nie jest to
nazwa pliku z biblioteką – patrz dalej). Podobny komu-
nikat (Rysunek 2) zobaczymy, jeśli przed odwołaniem
do metody rodzimej w programie nie nastąpi wywoła-
nie metody
loadLibrary()
ładującej podaną bibliotekę
(np. bo zapomnieliśmy je umieścić w kodzie). Musimy
w takim razie napisać funkcję rodzimą i utworzyć bi-
bliotekę dynamiczną zawierającą jej implementację.
Prototyp funkcji implementującej (w szczególności jej
nazwa) nie może być przypadkowy. Aby się dowie-
dzieć pod jaką nazwą JVM szuka implementacji na-
szej metody rodzimej musimy wygenerować plik na-
główkowy, zawierający jej deklarację.
Generowanie pliku nagłówkowego
Do generowania plików nagłówkowych służy program
javah, dostarczany wraz z pakietem SDK Javy. Znajduje
się on w podkatalogu bin pakietu, tam gdzie kompilator
i interpreter. Zakładając, że w katalogu bieżącym znaj-
duje się plik HelloWorld.class, polecenie javah -jni Hello-
World wygeneruje plik HelloWorld.h, zawierający proto-
typy funkcji implementujących metody rodzime zadekla-
rowane w klasie
HelloWorld
. Jego zawartość przedsta-
wia Listing 2. Mimo, iż metoda
sayHello()
jest bezpara-
metrowa, to jej rodzima implementacja posiada dwa pa-
rametry. Są to standardowe argumenty przekazywane
wszystkim funkcjom implementującym metody rodzime.
Ich znaczenie zostanie wyjaśnione dalej. Makra
JNIE-
XPORT
i
JNICALL
są potrzebne (na platformie Win32) do
wyeksportowania funkcji z biblioteki dynamicznej i za-
pewnienia odpowiedniego protokołu wywołania. Są one
zdefiniowane w pliku nagłówkowym jni_md.h.
Implementacja metody rodzimej
Zatem nasza funkcja nazywa się
Java _ HelloWorld _
sayHello
i pobiera dwa argumenty (
JNIEnv *, jobject
),
Bartłomiej Starosta
Bartłomiej Starosta jest wykładowcą w Polsko-Japoń-
skiej Wyższej Szkole Technik Komputerowych.
Jest autorem kilku książek poświęconych Javie.
Kontakt: barstar@pjwstk.edu.pl.
Rysunek 1.
Próba uruchomienia programu bez
biblioteki dynamicznej
Rysunek 2.
Próba uruchomienia programu bez
załadowania biblioteki dynamicznej
Java Native Interface – łączenie Javy i C/C++
29
www.sdjournal.org
Software Developer’s Journal 8/2006
które na razie ignorujemy. Przedrostek
Java
występujący w na-
zwie zawsze towarzyszy funkcjom implementującym metody ro-
dzime. Dalej następuje nazwa klasy, podkreślenie i nazwa me-
tody. Tak wygląda budowa nazwy funkcji rodzimej w najprost-
szym przypadku. Jeśli klasa znajduje się w pakiecie nazwanym
i/lub zawiera przeciążone metody rodzime, to ich nazewnictwo
się komplikuje. Implementację funkcji
Java _ HelloWorld _ sayHel-
lo
umieścimy w pliku HelloWorld.cpp (nazwa jest dowolna). Jego
zawartość prezentuje Listing 3. Należy pamiętać o włączeniu pli-
ku nagłówkowego HelloWorld.h na początku pliku z implementa-
cją. Dołącza on plik nagłówkowy jni.h, który zawiera deklaracje
funkcji, typów i makr niezbędnych do pracy z JNI.
Utworzenie biblioteki dynamicznej
Kolejnym krokiem jest skompilowanie kodu C++ do posta-
ci biblioteki dynamicznej. Niezależnie od stosowanej platfor-
my musimy poinformować kompilator, gdzie znajdują się pliki
nagłówkowe jni.h i jni_md.h (włączany przez jni.h). Wchodzą
one w skład SDK Javy i znajdują się w podkatalogach główne-
go katalogu dystrybucji: include (jni.h) oraz include\win32 lub
include/linux (jni_md.h) – zależnie od używanego systemu.
Listing 4 przedstawia konkretne wywołania dla popular-
nych kompilatorów. Zakładamy, że pakiet SDK został zainsta-
lowany w katalogu /j2sdk dla Linuxa i C:\j2sdk dla Win32.
Nazwa pliku zawierającego bibliotekę jest nazwą biblioteki
z dodanym rozszerzeniem .dll dla Win32 lub .so dla Linuksa. Do-
datkowo, w drugim przypadku nazwa pliku musi zaczynać się od
prefiksu lib (nie wchodzi on jednak w skład nazwy biblioteki, jaką
podajemy do metody
loadLibrary()
). Jako wynik kompilacji po-
winniśmy otrzymać plik z biblioteką dynamiczną: HelloWorld.dll
na platformie Win32, lub libHelloWorld.so dla Linuksa.
Uruchomienie programu
Przyszedł wreszcie czas na uruchomienie programu. Aby za-
kończyło się ono sukcesem, musimy poinformować system
operacyjny gdzie należy szukać naszej biblioteki dynamicz-
nej. W tym celu nadajemy odpowiednią wartość zmiennej sys-
temowej określającej położenia bibliotek dynamicznych. Pod
Linuksem jest to
LD _ LIBRARY _ PATH
a pod Win32 po prostu
PATH
. W drugim przypadku nie trzeba nic robić, jeśli biblioteka
znajduje się w katalogu bieżącym. Przykładowe komendy za-
mieszczone są w ramce. Teraz można już uruchomić maszy-
nę wirtualną i jeśli wszystko zostało wykonane prawidłowo zo-
baczymy na konsoli znany komunikat (Rysunek 3).
Konwencje nazewnicze JNI
Wiemy już jak wykorzystać kod C/C++ w Javie, ale nie wiemy
jeszcze jak go napisać. Oczywiście nie chodzi nam tu o pisa-
nie zwykłego kodu w tych językach, lecz o dostęp do danych
i metod Javy, w celu ich wykorzystania bądź przetworzenia.
Aby móc w pełni zastosować mechanizmy, które to umożliwia-
ją, musimy zapoznać się z pewnymi konwencjami nazewni-
czymi, jak również poznać typy C/C++, które reprezentują ty-
py Javy na poziomie rodzimym.
Deskryptory
Deskryptor jest symbolicznym kodem typu. Deskryptory są po-
trzebne do kodowania sygnatur metod rodzimych, a także do
identyfikowania składowych klas Javy. Są trzy rodzaje deskryp-
torów: klas, pól i metod. Deskryptory pól i metod można poznać
przy pomocy programu javap, który służy do dekompilacji klas
Javy. Opcja
-s
zleca wypisanie deskryptorów pól, opcja
-p
powo-
Listing 1.
Klasa HelloWorld
public
class
HelloWorld
{
static
{
System
.
loadLibrary
(
"HelloWorld"
)
;
}
public
native
void
sayHello
()
;
public
static
void
main
(
String
[]
args
){
new
HelloWorld
()
.
sayHello
()
;
}
}
javah a pakiety nazwane
Jeśli klasa
Klasa
znajduje się w pakiecie nazwanym
foo.bar
(za-
tem jej kwalifikowaną nazwą jest
foo.bar.Klasa
), to programowi
javah przekazujemy pełną nazwę: javah -jni foo.bar.Klasa. Zakła-
da on – podobnie jak interpreter Javy – że skompilowany plik Kla-
sa.class umieszczony jest w strukturze katalogowej odpowiadającej
strukturze pakietów. W tym przypadku jest to foo/bar/Klasa.class.
Natomiast wygenerowany plik nagłówkowy zostanie umieszczony
w katalogu bieżącym.
Funkcje rodzime a pakiety nazwane
Jeśli klasa zawierająca metodę rodzimą
fun()
znajduje się w pakie-
cie nazwanym, na przykład
foo.bar.Klasa
, to funkcja rodzima do-
starczająca implementacji nazywa się
Java _ foo _ bar _ Klasa _
fun()
. Jak widać, pomiędzy obowiązkowym prefiksem
Java
a na-
zwą klasy pojawiła się nazwa pakietu z tym, że zamiast kropek ma-
my podkreślenia.
Ustawienie zmiennej środowiskowej
wskazującej położenie biblioteki
dynamicznej
•
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
•
lib
jest katalogiem zawierającym bibliotekę – plik libHello-
World.so lub HelloWorld.dll.
Nazwy metod przeciążonych
Jeśli w klasie występują przeciążone metody rodzime, to do nazw
funkcji implementujących dokleja się sufiks złożony z dwóch zna-
ków podkreślenia
_ _
, po którym następują deskryptory para-
metrów. Jeśli jedna z metod jest bezargumentowa, to otrzymuje
tylko sufiks
_ _
.
Na przykład dla klasy
Klasa
zawierającej trzy przeciążone me-
tody:
public class Klasa { native float fun(); native long
fun(long l); native int fun(int i, int j); }
prototypy funk-
cji implementujących będą takie:
jfloat Java _ Klasa _ fun _ _
(JNIEnv *, jobject); jlong Java _ Klasa _ fun _ _ J(JNIEnv *,
jobject, jlong); jint Java _ Klasa _ fun _ _ II(JNIEnv *, jo-
bject, jint, jint);
Ich deskryptory to
()F
,
(J)J
i
(II)I
.
30
Programowanie
Java
www.sdjournal.org
Software Developer’s Journal 8/2006
duje wypisanie informacji również dla składowych prywatnych.
Wynik polecenia
javap -s -p HelloWorld
przedstawia Rysunek 4.
Deskryptorem klasy jest po prostu jej pełna kwalifikowana
nazwa, w której kropki oddzielające składniki pakietu (o ile wy-
stępują) zostały zamienione na ukośniki "
/
". Na przykład de-
skryptorem klasy
java.lang.Class
jest
"java/lang/Class"
.
Deskryptor klasy reprezentującej tablicę (są to wewnętrz-
ne klasy tworzone przez JVM) jest napisem składającym się
z jednego lub więcej – ich liczba jest równa liczbie wymiarów
tablicy – znaków
"["
(nawias kwadratowy lewy), po którym na-
stępuje deskryptor pola elementów tablicy. Na przykład de-
skryptor klasy reprezentującej tablicę
int[]
to
"[I"
, a dla tabli-
cy
Object[][]
to
"[[Ljava/lang/Object;"
. Deskryptory pól typów
pierwotnych Javy pokazuje Tabela 1.
Deskryptor pola typu referencyjnego zaczyna się od litery
"
L
", za którą umieszcza się deskryptor klasy zakończony śred-
nikiem "
;
". Na przykład deskryptorem pola typu
java.lang.Class
jest
"Ljava/lang/Class;"
. Deskryptor metody składa się z de-
skryptorów pól parametrów umieszczonych w nawiasach
okrągłych, za czym występuje deskryptor pola wyniku. De-
skryptory parametrów nie są oddzielone spacjami ani żadny-
mi innymi znakami. Jeśli metoda jest bezargumentowa, to na-
wiasy okrągłe są puste. Jeśli typem zwracanym jest
void
, to
deskryptorem wyniku jest
V
. Dla konstruktorów w miejscu de-
skryptora wyniku podaje się również
V
. Oto kilka przykładów:
•
"()V"
jest deskryptorem metody
void f()
•
"(I)J"
jest deskryptorem metody
long f(int i)
•
"(SZ)Ljava/lang/String;"
jest deskryptorem metody
String
f(short s, boolean b)
•
"([BB)[[Ljava/lang/Class;"
jest deskryptorem metody
Class[][] f(byte[] t, byte b)
Mapowanie typów
Typy Javy są na poziomie C/C++ reprezentowane przez specy-
ficzne typy JNI zdefiniowane w plikach nagłówkowych jni.h i jni_
md.h. W przypadku typów pierwotnych nie są to nowe typy, lecz
nowe nazwy na istniejące typy wprowadzone przy pomocy
ty-
pedef
. Odwzorowanie typów pierwotnych pokazuje Tabela 2. Typ
jchar
jest 16 bitowy i służy do reprezentowania znaków Unico-
de na platformach, które mają wsparcie dla tego systemu kodo-
wania. Do posługiwania się wartościami typu
jboolean
zdefinio-
wano dwa makra:
JNI _ TRUE
i
JNI _ FALSE
. Obiekty Javy są prze-
kazywane na zmiennych typu
jobject
. Z punktu widzenia języ-
ków C/C++ są to wskaźniki, jednak nie wolno odwoływać się do
Listing 2.
Plik nagłówkowy wygenerowany dla klasy
HelloWorld
/* 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
Kodowanie 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łod-
szych jest traktowane jako kod znaku w systemie ASCII. Jeśli na-
tomiast 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ńcu-
chy C/C++.
Funkcje pobierające wskaźnik do pierwszego
elementu tablicy
•
jboolean* GetBooleanArrayElements(jbooleanArray a,
jboolean *c)
•
jbyte* GetByteArrayElements(jbyteArray a, jboolean
*c)
•
jchar* GetCharArrayElements(jcharArray a, jboolean
*c)
•
jshort* GetShortArrayElements(jshortArray a, jboolean
*c)
•
jint* GetIntArrayElements(jintArray a, jboolean *c)
•
jlong* GetLongArrayElements(jlongArray a, jboolean
*c)
•
jfloat* GetFloatArrayElements(jfloatArray a, jboolean
*c)
•
jdouble* GetDoubleArrayElements(jdoubleArray a, jbo-
olean *c)
Parametr
c
ma podobne znaczenie jak w przypadku łańcuchów
znakowych (metoda
GetStringUTFChars()
) -– zostanie na nim
przekazana informacja o tym, czy została utworzona lokalna ko-
pia tablicy (
*c == JNI_TRUE
), czy też wskaźnik odnosi się do tabli-
cy zawartej w obiekcie Javy (
*c == JNI_FALSE
) . Jeśli nie jesteśmy
tym zainteresowani to podajemy
NULL
.
Odniesienia do klas
Wskaźnik do obiektu typu
jclass
reprezentującego klasę lub in-
terfejs Javy można uzyskać funkcjami:
jclass GetObjectClass(jobject obj);
jclass FindClass(const char *name);
Pierwsza zwraca odniesienie do obiektu reprezentującego klasę ar-
gumentu
obj
(jest on obiektem klasy zwracanej jako wynik). Druga
zwraca odniesienie do klasy, której deskryptor został podany jako
argument
name
. Na przykład:
jclass string_cls = env->FindClass("java/lang/String");
jclass objArray_cls = env->FindClass("[Ljava/lang/Object;");
Pierwsze wywołanie zwraca odniesienie do klasy
ja-
va.lang.String
. Drugie zwraca odniesienie do klasy reprezentują-
cej tablice obiektów
java.lang.Object[]
.
Java Native Interface – łączenie Javy i C/C++
31
www.sdjournal.org
Software Developer’s Journal 8/2006
nich bezpośrednio, lecz poprzez wywołania specjalnych funkcji
opisanych dalej. Zmienne tego rodzaju będziemy nazywać od-
niesieniami (ang. opaque reference). Najczęściej używane kla-
sy mają zdefiniowane swoje odpowiedniki, np.:
jstring
,
jclass
,
jarray
,
jthrowable
. Wszystkie one są podtypami
jobject
(jeśli im-
plementujemy w C++, w C są to te same typy strukturalne). Typ
jarray
służy do reprezentowania tablic Javy. Posiada on dodat-
kowe podtypy reprezentujące tablice różnych typów pierwotnych
i obiektowych. Rysunek 5 przedstawia hierarchię typów JNI.
Dostęp do Javy z poziomu C/C++
Jesteśmy już gotowi do zapoznania się z tajnikami JNI. Na po-
ziomie C/C++ mamy dostęp do obiektów Javy i ich zawartości
za pośrednictwem specjalnych funkcji bibliotecznych. Umożli-
wiają one również zgłaszanie wyjątków z poziomu rodzimego
i koordynację wątków. Możemy zatem przetwarzać argumenty
przekazywane do funkcji rodzimych z poziomu Javy jak również
utworzyć nowy obiekt i zwrócić go jako wynik metody rodzimej.
Pierwszym parametrem funkcji rodzimych jest zawsze wskaźnik
do środowiska
JNIEnv * env
. Jest to struktura, w której są zade-
klarowane wszystkie funkcje JNI. Jest ona zdefiniowana w pliku
nagłówkowym jni.h. Poprzez wskaźnik
env
do owej struktury na-
leży wywoływać wszystkie funkcje biblioteczne. Drugim parame-
trem zawsze jest:
• dla metod niestatycznych – odniesienie do obiektu, z któ-
rego wywołano metodę. Jest to odpowiednik
this
z Javy,
w przykładach nazywany
self,
• dla metod statycznych – odniesienie do klasy, z której wywo-
łano metodę, przekazywane na zmiennych typu
jclass
.
Pozostałe parametry odpowiadają parametrom metody rodzi-
mej, którą dana funkcja implementuje. Oczywiście, ich typy są
rodzimymi odpowiednikami typów Javy. Przyjrzyjmy się bliżej
poszczególnym aspektom dostępu do Javy. Najpierw opisze-
my przetwarzanie napisów i tablic, następnie dostęp do skła-
dowych i tworzenie obiektów.
Przetwarzanie napisów
Ze względu na częste stosowanie napisy Javy jako obiek-
ty klasy
String
są reprezentowane po stronie rodzimej przez
zmienne specjalnego typu
jstring
. Nie są to jednak łańcuchy
znakowe w sensie C/C++ (typu
char *
), lecz prawdziwe obiek-
ty Javy (klasy
String
). Aby możliwe było przetwarzanie napi-
su reprezentowanego w ten sposób, konieczne jest uzyskanie
– za pomocą specjalnej funkcji – wskaźnika typu
char *
repre-
zentującego zawartość tego obiektu jako tablicę znaków.
I tu powstaje problem, ponieważ znaki Unicode wchodzące
w skład napisów Javy są 2 bajtowe, podczas gdy typ
char
w C/
C++ jest jednobajtowy. Potrzebne są więc specjalne funkcje kon-
wertujące. Maszyna wirtualna używa kodowania UTF-8 do we-
wnętrznej reprezentacji napisów i w takiej postaci będzie napis
C/C++ uzyskany z obiektu klasy
String
. Do zamiany argumentu
typu
jstring
na rodzimy łańcuch znakowy
char*
służy funkcja
const char* GetStringUTFChars(jstring str, jboolean *isCopy);
Zwraca ona wskaźnik do znaku będący reprezentacją napisu
Javy w systemie UTF-8. Pierwszym parametrem jest łańcuch
przekazany jako argument do metody rodzimej. Jeśli drugi ar-
gument jest różny od
NULL
, to zostanie na nim przekazana in-
formacja o tym, czy została wykonana lokalna kopia napisu
(będzie miał wartość
JNI _ TRUE
), czy też nie – wtedy uzyska-
ny łańcuch jest oryginalnym łańcuchem zawartym w obiekcie
Javy. Z reguły przekazuje się
NULL
. Jeśli napis składa się wy-
łącznie z 7-bitowych znaków ASCII, to pozyskany w ten spo-
só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()
(wywołaną z pozio-
Tabela 2.
Mapowanie typów pierwotnych
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
Dokumentacja i literatura
• The Java Native Interface Programmer's Guide and Specification
– Obszerny podręcznik użytkownika, wraz ze specyfikacją zawie-
rającą opis wszystkich funkcji. Jest dostępny w kilku formatach on-
line http://java.sun.com/docs/books/jni/html/jniTOC.html)
• Java Native Interface Specification – dostępna on-line pod adre-
sem http://java.sun.com/j2se/1.5.0/docs/guide/jni. Wchodzi rów-
nież w skład pakietu dokumentacji Javy w katalogu docs/guide/jni.
• Książka „Nowe Metody Programowania, Tom 2”, wyd. PJWSTK
2006. Zawiera rozdział poświęcony JNI. Autorzy: Krzysztof Bar-
teczko, Wojciech Drabik, Bartłomiej Starosta.
Tabela 1.
Deskryptory pól typów pierwotnych
Typ Javy
Deskryptor pola
boolean
Z
byte
B
char
C
short
S
int
I
long
J
float
F
double
D
Rysunek 3.
Uruchomienie programu HelloWorld
32
Programowanie
Java
www.sdjournal.org
Software Developer’s Journal 8/2006
mu C/C++) i utworzyć na jej podstawie rodzimą tablicę zna-
ków z tą samą zawartością, którą już można normalnie prze-
twarzać. Metoda
getBytes()
uwzględnia kodowanie obowiązu-
jące w danym systemie, zatem uzyskany w ten sposób łań-
cuch będzie zakodowany np. w iso-8859-2 czy cp-1250.
Niezmiernie ważna kwestią jest zwalnianie tak pozyska-
nego wskaźnika, kiedy nie jest on już potrzebny – najpóźniej
przed zakończeniem wywołania funkcji rodzimej, w której zo-
stał pobrany. Służy do tego funkcja
void ReleaseStringUTFChars(jstring str, const char* chars);
Jej pierwszym argumentem jest łańcuch znakowy, z które-
go pozyskany został napis UTF-8 przekazany jako drugi ar-
gument. Utworzyć łańcuch klasy
String
na podstawie napisu
UTF-8 można funkcją
jstring NewStringUTF(const char *utf);
Wynik jest obiektem Javy zakodowanym w Unicode, który
można zwrócić jako rezultat metody rodzimej. Nie trzeba go
zwalniać, albowiem zajmie się tym odśmiecacz.
Zademonstrujemy przekazywanie napisów z Javy do
C++ i z powrotem. Klasa
JNIString
widoczna na Listingu 5
zawiera metodę
main()
, w której najpierw ładujemy bibliote-
kę dynamiczną libstring.so zawierającą implementację sta-
tycznej metody
stringUse(String s)
, a następnie wywołuje-
my tę metodę przekazując jakiś napis Javy jako argument.
Działanie tej metody, widocznej na Listingu 6, polega na
wypisaniu przekazanego komunikatu, a następnie pobraniu
napisu od użytkownika i zwróceniu go do Javy jako wynik.
W funkcji rodzimej
Java _ JNIString _ stringUse()
po pozy-
skaniu łańcucha
cppString
ze źródłowego napisu
javaString
sprawdzamy, czy nie jest on
NULL
co się może zdarzyć je-
śli JVM nie jest w stanie zaalokować niezbędnej pamięci.
Po wypisaniu komunikatu poprzez
cout
zwalniamy wskaźnik
cppString
. Dalej pobieramy ze standardowego wejścia ciąg
znaków do tablicy i na tej podstawie tworzymy obiekt
ret-
chars
klasy
String
zwracany do środowiska Javy.
Aby uruchomić nasz program stosujemy wcześniej opisaną
procedurę. Najpierw kompilujemy JNIString.java, potem generu-
jemy plik nagłówkowy JNIString.h i kompilujemy JNIString.cpp do
biblioteki dynamicznej. Na koniec ustawiamy zmienne systemo-
we (jeśli trzeba) i uruchamiamy interpreter (Rysunek 6).
Przetwarzanie tablic
Po stronie rodzimej tablice Javy są reprezentowane przez
zmienne typu
jarray
bądź typów pochodnych, odpowiada-
jących tablicom konkretnych typów pierwotnych lub referen-
cyjnych Javy. Tabela 3 pokazuje mapowanie typów tablico-
wych. Tablice obiektów są zawsze odwzorowywane na typ
jobjectArray
(niezależnie od zawartości) i sposób ich prze-
twarzania jest inny, niż w przypadku tablic złożonych z ele-
mentów typów pierwotnych. W szczególności tablice wielo-
wymiarowe – ponieważ w Javie są to tablice obiektów - są
przedstawiane na zmiennych typu
jobjectArray
. Zasady do-
tyczące posługiwania się tablicami są podobne, jak w przy-
Rysunek 5.
Hierarchia typów JNI
�������
���������
�������������
������
���������������
�������
����������������
����������
�������������������
������
����������
����������
�����������
����������
������������
������������
�����������
Listing 3.
Implementacja metody sayHello z klasy
HelloWorld
#
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
;
}
Rysunek 4.
Analiza klasy HelloWorld programem javap
33
Java Native Interface – łączenie Javy i C/C++
www.sdjournal.org
Software Developer’s Journal 8/2006
padku napisów. Typowy schemat postępowania obejmuje
następujące kroki:
• pobranie długości tablicy,
• uzyskanie wskaźnika, poprzez który będzie odbywał się
dostęp do jej elementów,
• przetwarzanie elementów tablicy w sposób właściwy dla
języka C/C++, za pośrednictwem pobranego wskaźnika,
• zwolnienie pozyskanego wskaźnika.
Do pobrania liczby elementów tablicy służy funkcja
jsize
GetArrayLength(jarray array);
Za zmienną
array
możemy podsta-
wić dowolną tablicę jednego z typów wymienionych w Tabela 3.
Typ
jsize
to inna nazwa na
jint
czyli
int
. Do posługiwania się
tablicą w C/C++ niezbędny jest wskaźnik do pierwszego elemen-
tu. Dla tablic elementów typów pierwotnych pobieramy go jedną
z funkcji przedstawionych w Ramce. Podobnie jak w przypadku
napisów, wskaźnik reprezentujący tablicę trzeba zwolnić, kiedy
nie jest już potrzebny, aby zwrócić pamięć przez nią zajmowaną.
Po uwolnieniu wskaźnika nie można się do niego więcej odwoły-
wać. Do zwalniana wskaźników służy zestaw funkcji:
void ReleaseTypeArrayElements(jTypearray array,
jtype *elems,
jint mode);
Za
type
wstawiamy nazwę typu pierwotnego Javy, otrzymując
efekt podobny jak w Ramce „Funkcje pobierające wskaźnik do
pierwszego elementu w tablicy”. Parametr
array
jest tablicą,
z której pobrano wskaźnik
elems
. Parametr
mode
określa tryb
zwalniania i może przyjmować następujące wartości (z regu-
ły podaje się
0
):
•
0
– zapisz zmiany w oryginalnej tablicy i zwolnij wskaźnik,
•
JNI _ COMMIT
– zapisz zmiany, ale nie zwalniaj wskaźnika,
•
JNI _ ABORT
– zwolnij wskaźnik nie zapisując zmian.
Jeśli tablica jest duża, to pobieranie wskaźnika do całości
w powyższy sposób może okazać się nieefektywne (mo-
że wymagać kopiowania całej tablicy), zwłaszcza w sytuacji,
gdy chcemy operować tylko na niewielkim jej fragmencie. Na
szczęście mamy do dyspozycji dwa zestawy funkcji umożli-
wiające dostęp do określonych zakresów tablic. Funkcje po-
staci (za
Type
wstawiamy odpowiedni typ pierwotny Javy):
void GetTypeArrayRegion(jTypearray array,
jsize start,
jsize len,
jtype *buf);
kopiują obszar o początku
start
i długości
len
z tablicy
array
do bufora
buf
. Operację odwrotną wykonują funkcje:
void SetTypeArrayRegion(jTypearray array,
jsize start,
jsize len,
jtype *buf);
Kopiują one
len
elementów z bufora
buf
do tablicy
array
po-
cząwszy od jej indeksu
start
. Można w ten sposób skopiować
jeden element jak i całą tablicę.
Tablice obiektów muszą być traktowane w inny sposób,
ponieważ nie wiadomo jaki rozmiar mają obiekty w nich za-
warte (więc nie jest możliwa arytmetyka na wskaźnikach). Za-
tem nie możemy dostać całej tablicy od razu, w jednym wy-
wołaniu. Możemy jedynie dostać się do poszczególnych ko-
mórek. Mamy do dyspozycji dwie funkcje:
jobject GetObjectArrayElement(jobjectArray arr, jsize index)
void SetObjectArrayElement(jobjectArray array,
jsize index, jobject val)
Listing 4.
Utworzenie biblioteki dynamicznej
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
Listing 5.
Klasa JNIString
public
class
JNIString
{
native
static
String
stringUse
(
String
arg
)
;
public
static
void
main
(
String
[]
args
){
System
.
loadLibrary
(
"string"
)
;
String
nativeString
=
stringUse
(
"Podaj napis"
)
;
System
.
out
.
println
(
"Podano napis: "
+
nativeString
)
;
}
}
Tabela 3.
Mapowanie typów tablicowych
Typ JNI
Typ Javy
jbooleanArray
boolean[]
jbyteArray
byte[]
jcharArray
char[]
jshortArray
short[]
jintArray
int[]
jlongAraay
long[]
jfloatArray
float[]
jdoubleArray
double[]
jobjectArray
Object[]
34
Programowanie
Java
www.sdjournal.org
Software Developer’s Journal 8/2006
Pierwsza z nich zwraca element z pozycji
index
na zmiennej typu
jobject
. Druga pozwala zapisać do tablicy
array
element
val
na
pozycji
index
. Tablice wielowymiarowe, które są tablicami obiek-
tów traktujemy podobnie. Najpierw pobieramy element z określo-
nej pozycji jako
jobject
a następnie rzutujemy go na odpowied-
ni typ tablicowy. Poniższa sekwencja pokazuje jak dostać się do
elementu
elem
o współrzędnych
[x][y]
z dwuwymiarowej tablicy
Object[][]
.
jobject array = env->GetObjectArrayElement(object2DArr, x);
jobjectArray row = (jobjectArray)array;
jobject elem = env->GetObjectArrayElement(row, y);
Argument
object2DArr
reprezentuje dwuwymiarową tablicę
obiektów na zmiennej typu
jobjectArray
. Nową tablicę – jako
obiekt Javy – możemy utworzyć jedną z funkcji:
jTypearray NewTypeArray(jsize len);
jobjectArray NewObjectArray(jsize len,
jclass clazz, jobject init);
Pierwsza tworzy tablicę elementów pierwotnych typu
ty-
pe
, druga – tablicę obiektów klasy
clazz
(sposób pozyska-
nia takiej zmiennej opisuje Ramka). Parametr
len
określa roz-
miar tablicy. Trzeci parametr
init
jest obiektem, którym zosta-
ną wypełnione wszystkie komórki tablicy. Przeważnie podaje
się
NULL
– wtedy tablica będzie pusta. Schemat posługiwania
się tablicami zaprezentujemy na przykładzie metody rodzimej,
która oblicza podłogę z pierwiastka dla każdego elementu ta-
blicy (typu
double[]
) przekazanej jako argument i zapisuje wy-
nik w nowej tablicy (typu
int[]
) zwracanej jako rezultat.
Kod klasy
JNIArray
widoczny jest na Listingu 7. W metodzie
main()
tworzymy obiekt tej klasy, wejściową tablicę liczb i prze-
kazujemy ją do metody rodzimej
arrayUse(double[] arg)
, która
zwraca jako wynik tablicę
int[]
. Listing 8 zawiera implementację
w postaci funkcji rodzimej
Java _ JNIArray _ arrayUse()
. Najpierw
pobieramy długość i zawartość tablicy wejściowej
input
. Następ-
nie tworzymy tablicę
retarr
przeznaczoną na wynik. Tablica tym-
czasowa
temparr
jest potrzebna ze względów wydajnościowych:
nie chcemy po każdym obliczeniu wywoływać funkcji
SetIntAr-
rayRegion()
aby umieścić pojedynczy wynik w tablicy
retarr
, lecz
przekopiować wszystkie jednym wywołaniem. Na końcu zwal-
niamy pobrany wskaźnik do tablicy wejściowej i oczywiście tabli-
cę tymczasową. Kompilację, uruchomienie i wynik działania pre-
zentuje Rysunek 7.
Dostęp do atrybutów
Z poziomu C/C++ możemy dostać się do atrybutów obiektów Ja-
vy. Dwa zestawy funkcji JNI pozwalają na operowanie na polach
statycznych i niestatycznych. Aby można było wykonać jakąkol-
wiek operację trzeba najpierw pobrać identyfikator atrybutu re-
prezentowany przez zmienną typu
jfieldID
. Dla atrybutów instan-
cyjnych robimy to funkcją:
jfieldID GetFieldID(jclass clazz,
const char *name,
const char *sig);
Listing 6.
Implementacja rodzimej metody
stringUse(String s)
#
include
<
jni
.
h
>
#
include
<
iostream
>
#
include
"JNIString.h"
using
namespace
std
;
JNIEXPORT
jstring
JNICALL
Java_JNIString_stringUse
(
JNIEnv
*
env
,
jclass
clazz
,
jstring
javaString
) {
const
char
*
cppString
=
env
->
GetStringUTFChars
(
javaString
, 0
)
;
if
(
cppString
==
NULL
)
return
NULL
;
cout
<<
cppString
<<
endl
;
env
->
ReleaseStringUTFChars
(
javaString
,
cppString
)
;
const
int
LEN
=
80
;
char
buf
[
LEN
]
;
cin
.
getline
(
buf
,
LEN
)
;
jstring
retchars
=
env
->
NewStringUTF
(
buf
)
;
return
retchars
;
}
Listing 7.
Klasa JNIArray
public
class
JNIArray
{
native
int
[]
arrayUse
(
double
[]
arg
)
;
static
{
System
.
loadLibrary
(
"array"
)
;
}
public
static
void
main
(
String
[]
args
){
JNIArray
arrDemo
=
new
JNIArray
()
;
double
[]
in
=
new
double
[]{
1, 4.5, 10.01
}
;
int
[]
out
=
arrDemo
.
arrayUse
(
in
)
;
for
(
int
i
=
0
;
i
<
in
.
length
;
i
++
)
System
.
out
.
println
(
"sqrt("
+
in
[
i
]
+
")="
+
out
[
i
])
;
}
}
Rysunek 7.
Uruchomienie programu JNIArray
Rysunek 6.
Uruchomienie programu JNIString
Java Native Interface – łączenie Javy i C/C++
Software Developer’s Journal 8/2006
Natomiast identyfikatory atrybutów statycznych pobiera się
podobną funkcją:
jfieldID GetStaticFieldID(jclass clazz,
const char *name,
const char *sig);
W obu przypadkach znaczenie argumentów jest następujące:
•
jclass clazz
jest odniesieniem do klasy w której jest wi-
doczny atrybut,
•
const char * name
jest nazwą atrybutu, pod jaką jest on
zdefiniowany w klasie,
•
const char * sig
jest deskryptorem pola.
Zapis i odczyt atrybutów instancyjnych wykonują funkcje z rodziny:
jtyp GetTypField(jobject obj, jfieldID fieldID);
void SetTypField(jobject obj, jfieldID fieldID, jtyp val);
Za
Typ
podstawiamy nazwę typu pierwotnego Javy (pisa-
nego dużą literą) lub
Object
a za
jtyp
– jego odpowied-
nik JNI. Argument
jobject obj
jest odniesieniem do obiek-
tu zawierającego dany atrybut,
jfieldID
jest identyfikato-
rem tego pola, a
val
jest nową wartością, która zostanie
mu nadana. Dla atrybutów statycznych przewidziano po-
dobne funkcje:
jtyp GetStaticTypField(jclass clazz, jfieldID fieldID);
void SetStaticTypField(jclass clazz,
jfieldID fieldID,
jtyp value);
Znaczenie argumentów jest podobne z tym, że zamiast odnie-
sienia do obiektu przekazujemy odniesienie do klasy
jclass
clazz
, której dotyczy operacja. Dostęp do atrybutów zilustru-
Listing 8.
Implementacja metody rodzimej arrayUse()
z klasy JNIArray
#
include
<
jni
.
h
>
#
include
<
iostream
>
#
include
<
cmath
>
#
include
"JNIArray.h"
using
namespace
std
;
JNIEXPORT
jintArray
JNICALL
Java_JNIArray_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
;
}
Listing 9.
Klasa JNIAttributes
public
class
JNIAttributes
{
boolean
instanceField
=
true
;
static
String
staticField
=
"Java"
;
native
void
fieldAccess
()
;
public
static
void
main
(
String
[]
args
)
{
System
.
loadLibrary
(
"attributes"
)
;
JNIAttributes
attr
=
new
JNIAttributes
()
;
System
.
out
.
println
(
"Przed fieldAccess():"
)
;
System
.
out
.
println
(
"
\t
instanceField = "
+
attr
.
instanceField
)
;
System
.
out
.
println
(
"
\t
staticField = "
+
attr
.
staticField
)
;
attr
.
fieldAccess
()
;
System
.
out
.
println
(
"Po fieldAccess():"
)
;
System
.
out
.
println
(
"
\t
instanceField = "
+
attr
.
instanceField
)
;
System
.
out
.
println
(
"
\t
staticField = "
+
attr
.
staticField
)
;
}
}
Rysunek 8.
Uruchomienie programu JNIAttributes
36
Programowanie
Java
www.sdjournal.org
Software Developer’s Journal 8/2006
Listing 10.
Imeplementacja metody rodzimej fieldAccess() z klasy JNIAttributes
jemy przykładem zawartym w klasie
JNIAttributes
zapre-
zentowanej na Listingu 9. Zawiera ona dwa pola: instan-
cyjne typu
boolean
i statyczne typu
String
. Metoda rodzi-
ma
fieldAccess()
będzie je modyfikować. Implementację tej
metody widzimy na Listingu 10.
Funkcja rodzima
Java _ JNIAttributes _ fieldAccess()
naj-
pierw tworzy identyfikatory pól
instanceField
i
staticField
zadeklarowanych w klasie
JNIAttributes
. Następnie pobie-
ra ich wartości i wypisuje na standardowym wyjściu me-
chanizmami C++. Dalej tworzymy napis "
C++
" (obiekt klasy
String
), który będzie nową wartością atrybutu
staticField
.
Na koniec nadajemy nowe wartości atrybutom i wypisujemy
je. Wynik działania przedstawia Rysunek 8.
Wywoływanie metod i tworzenie obiektów
Wywoływanie metod Javy z poziomu C/C++ sprowadza się
do podobnego schematu, co w przypadku atrybutów. Naj-
pierw 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 obiek-
tu, na rzecz którego jest wołana. Może ona być zadeklarowa-
na w jego nadklasie, a nawet w interfejsie implementowanym
przez jego klasę. Do pozyskania identyfikatora metody uży-
wamy jednej z funkcji:
GetMethodID(jclass clazz, const char *name, const char *sig)
GetStaticMethodID(jclass clazz,
const char *name,
const char *sig)
Obie zwracają wartość typu
jmethodID
. Argumenty mają zna-
czenie podobne jak w przypadku dostępu do składowych:
•
clazz
jest klasą lub interfejsem, w której metoda będzie
poszukiwana, najczęściej będzie to klasa obiektu, na
rzecz którego wołamy metodę,
•
name
jest nazwą metody,
•
sig
jest deskryptorem tej metody.
Wykonanie metody następuje na skutek wywołania jednej
z funkcji JNI:
jtyp CallTypMethod(jobject obj, jmethodID methodID, ...);
jtyp CallStaticTypMethod(jclass clazz, jmethodID methodID,
...);
Wielokropek na końcu listy parametrów oznacza, że funk-
cja pobiera nieokreśloną ich liczbę (podobnie jak
printf()
).
#
include
<
jni
.
h
>
#
include
<
iostream
>
#
include
"JNIAttributes.h"
using
namespace
std
;
JNIEXPORT
void
JNICALL
Java_JNIAttributes_fieldAccess
(
JNIEnv
*
env
,
jobject
self
)
{
// pobieramy odniesienie do klasy tego obiektu
jclass
jniAttrs
=
env
->
GetObjectClass
(
self
)
;
// pobieramy identyfikatory pól
jfieldID
fid
=
env
->
GetFieldID
(
jniAttrs
,
"instanceField"
,
"Z"
)
;
jfieldID
s_fid
=
env
->
GetStaticFieldID
(
jniAttrs
,
"staticField"
,
"Ljava/lang/String;"
)
;
// jeśli błąd to kończymy
if
(
fid
==
NULL
||
s_fid
==
NULL
)
return
;
// pobieramy wartości pól
jboolean
fld_val
=
env
->
GetBooleanField
(
self
,
fid
)
;
jstring
s_fld_val
=
(
jstring
)
env
->
GetStaticObjectField
(
jniAttrs
,
s_fid
)
;
// przekształcamy String na napis UTF-8
const
char
*
cstr
=
env
->
GetStringUTFChars
(
s_fld_val
,
NULL
)
;
if
(
cstr
==
NULL
)
return
;
cout
<<
"W trakcie Java_JNIAttributes_fieldAccess() "
<<
"- wartosci argumentow:
\n
"
<<
"
\t
instanceField = "
<<
(
fld_val
==
JNI_TRUE
?
"true"
:
"false"
)
<<
endl
<<
"
\t
staticField = "
<<
cstr
<<
endl
;
// zwalniamy napis UTF-8
env
->
ReleaseStringUTFChars
(
s_fld_val
,
cstr
)
;
cstr
=
"C++"
;
// tworzymy nowy String na podstawie napisu UTF-8
s_fld_val
=
env
->
NewStringUTF
(
cstr
)
;
if
(
s_fld_val
==
NULL
)
return
;
// nadajemy nowe wartości atrybutom
fld_val
=
JNI_FALSE
;
env
->
SetBooleanField
(
self
,
fid
,
fld_val
)
;
env
->
SetStaticObjectField
(
jniAttrs
,
s_fid
,
s_fld_val
)
;
cout
<<
"W trakcie Java_JNIAttributes_fieldAccess() "
<<
"- wartosci po modyfikacji:
\n
"
<<
"
\t
instanceField = "
<<
(
fld_val
==
JNI_TRUE
?
"true"
:
"false"
)
<<
endl
<<
"
\t
staticField = "
<<
cstr
<<
endl
;
}
Rysunek 9.
Uruchomienie programu JNIMetods
Java Native Interface – łączenie Javy i C/C++
37
www.sdjournal.org
Software Developer’s Journal 8/2006
W to miejsce będą podstawiane argumenty dla wołanej
metody. Podobnie jak w przypadku dostępu do atrybutów,
oprócz identyfikatora metody
methodID
, przekazujemy tym
funkcjom odniesienie do obiektu (
obj
) lub klasy (
clazz
), na
rzecz której metoda będzie wołana. Obie funkcje zwracają
rezultat wywołanej metody. Jeśli jest ona typu
void
, to jako
typ wyniku
jtyp
podajemy
void
, a jako
Typ
(w nazwie funk-
cji) podajemy
Void
. W przeciwnym przypadku podajemy od-
powiednie typy.
Zaprezentujemy teraz rekurencyjną metodę rodzimą –
z poziomu implementacji w C++ będziemy wywoływać ją sa-
mą jako metodę Javy. Kod klasy
JNIMetods
prezentuje Listing
11. Metodę
callMethod(short)
wywołujemy tyle razy, ile wyno-
si argument plus 1.
Implementacja metody
callMethod()
jest zawarta na Li-
stingu 12. Operacja pobrania identyfikatora metody jest
dość kosztowna czasowo, dlatego pozyskany identyfikator
zapamiętujemy na statycznej zmiennej, aby odwoływać się
do niego w przyszłych wywołaniach bez potrzeby ponow-
nego pobierania go. Rysunek 9 prezentuje kompilację i wy-
konanie programu. Identyfikatory metod są potrzebne rów-
nież do tworzenia obiektów. Funkcja
jobject NewObject(jclass clazz, jmethodID methodID, ...);
zwraca nowy obiekt klasy
clazz
. Parametr
methodID
jest
identyfikatorem (metody) konstruktora, którego chcemy
użyć do utworzenia obiektu. Ewentualne argumenty kon-
struktora przekazujemy dalej. Pobierając identyfikator me-
tody dla konstruktora należy podać funkcji
GetMethodID()
ja-
ko nazwę metody napis
"<init>"
, oraz
"V"
jako nazwę typu
zwracanego w deskryptorze metody. Tak utworzone obiek-
ty można oczywiście przekazywać na poziom Javy jako re-
zultaty metod, przypisując do atrybutów obiektów czy też
jako elementy tablic.
C a C++
W zaprezentowanych przykładach używaliśmy języka C++.
Funkcje rodzime można również implementować w czystym
C. Kod napisany w C różni się od kodu C++ dwoma detala-
mi.
Po pierwsze, funkcje biblioteczne JNI w wersji C++ są
funkcjami składowymi struktury
JNIEnv
i są wywoływane po-
przez wskaźnik
env
do niej (przesyłany zawsze jako pierwszy
argument do funkcji rodzimych), na przykład:
env->IsInstanceOf(object, clazz);
W wersji C zmienna typu
JNIEnv
jest wskaźnikiem do innej
struktury zawierającej odpowiednie funkcje biblioteczne (ja-
ko wskaźniki). A więc pierwszy parametr funkcji rodzimych
(typu
JNIEnv*
) jest tu podwójnym wskaźnikiem. Te same
funkcje biblioteczne w C pobierają dodatkowo jako pierw-
szy argument wskaźnik typu
JNIEnv*
(w C++ tę rolę spełnia
this
). Zatem powyższe wywołanie w C ma postać:
(*env)->IsInstanceOf(env, object, clazz);
Druga różnica polega na tym, że w języku C nie ma rela-
cji dziedziczenia pomiędzy typami reprezentującymi obiek-
ty, takimi jak:
jobject
,
jclass
,
jarray
,
jdoubleArray
. W związ-
ku z tym nie jest konieczne rzutowanie zmiennych typu
jo-
bject
na jakiś jego podtyp (na przykład przy przetwarzaniu
tablic wielowymiarowych). Oczywiście, utrudnia to wykrycie
potencjalnych błędów.
Zastosowania
Po co łączyć Javę z C/C++? Oczywiście, efektywność ko-
du napisanego w Javie jest z reguły niższa od jego odpo-
wiednika w C/C++, więc jeśli zależy nam na czasie wyko-
nania, to można krytyczne fragmenty kodu zaimplemento-
wać w C/C++ (ewentualnie poddając optymalizacjom) i od-
woływać się do niego w Javie poprzez metody rodzime. Ale
nie tylko zwiększenie efektywności może być zachętą do
stosowania JNI. Jeśli mamy gotowe i sprawdzone bibliote-
ki funkcji C i nie chcemy przepisywać ich od nowa w Ja-
vie to bardzo dobrym rozwiązaniem jest wykorzystanie ich
za pomocą tej technologii. Kolejnym obszarem zastosowań
są sytuacje, kiedy niezbędnego fragmentu kodu nie da się
napisać w Javie. Mogą to być np. sterowniki urządzeń albo
funkcje specyficzne dla danej platformy. We wszystkich ta-
kich przypadkach piszemy funkcje rodzime odwołujące się
do istniejącego kodu, które następnie wywołujemy w Javie
jako metody rodzime. n
Listing 11.
Klasa JNIMetods
public
class
JNIMethods
{
native
void
callMethod
(
short
calls
)
;
public
static
void
main
(
String
[]
args
)
{
System
.
loadLibrary
(
"methods"
)
;
new
JNIMethods
()
.
callMethod
((
short
)
4
)
;
}
}
Listing 12.
Imeplementacja rodzimej metody callMethod()
z klasy JNIMetods
#
include
<
jni
.
h
>
#
include
<
iostream
>
#
include
"JNIMethods.h"
using
namespace
std
;
JNIEXPORT
void
JNICALL
Java_JNIMethods_callMethod
(
JNIEnv
*
env
,
jobject
self
,
jshort
calls
) {
static
jmethodID
this_method_id
=
NULL
;
jclass
this_cls
=
env
->
GetObjectClass
(
self
)
;
if
(
this_method_id
==
NULL
){
this_method_id
=
env
->
GetMethodID
(
this_cls
,
"callMethod"
,
"(S)V"
)
;
if
(
this_method_id
==
NULL
)
return
;
}
cout
<<
"wywołanie "
<<
calls
<<
endl
;
if
(
calls
>
0
)
env
->
CallVoidMethod
(
self
,
this_method_id
,
calls
-
1
)
;
}