background image

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 

background image

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

background image

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[]

background image

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 

background image

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 

background image

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[]

background image

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 

background image

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 

background image

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 

background image

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

)

;

}