JNI definuje następujące typy danych w środowisku natywnym, które odpowiadają typom Javy:
Poniżej znajduje się schemat działania funkcji natywnej:
Najbardziej wyzywającym zadaniem w programowaniu funkcji JNI jest konwersja pomiędzy typami natywnymi a typami referencyjnymi JNI. Do tego celu służy wiele funkcji interfejsu środowiska JNI. JNI jest interfejsem języka C, który nie jest językiem zorientowanym obiektowo i w rzeczywistości nie pozwala na przekazywanie obiektów.
Przekazywanie typów podstawowych jest oczywiste. W natywnym środowisku zdefiniowany jest typ jxxx, np. jint, jbyte, jshort, jlong, jfloat, jdouble, jchar i jboolean dla każdego typu podstawowego Javy, czyli int, byte, short, long, float, double, char i boolean odpowiednio.
Przykładowy program - TestJNIPrimitive.java:
public class TestJNIPrimitive {
static {
System.loadLibrary("myjni"); // libmyjni.so (Linux)
}
// Deklaracja natywnej metody average(), która pobiera dwa parametry typu podstawowego ints i zwraca
// ich średnią w postaci typu podstawowego double
private native double average(int n1, int n2);
// Test metody natywnej
public static void main(String args[]) {
System.out.println("W Javie, średnia wynosi " + new TestJNIPrimitive().average(3, 2));
}
}
Powyższy program wywołujący metodę JNI ładuje bibliotekę dynamiczną libmyjni.so. Deklaruje on natywną metodę average, która przyjmuje dwa parametry typu int i zwraca rezultat w postaci typu double. Zwracana wartość jest średnią przekazanych parametrów. Funkcja main tworzy nowy obiekt klasy TestJNIPrimitive, na którym wywołuje metodę natywną average i wypisuje wynik na standardowym wyjściu.
Skompiluj program do pliku TestJNIPrimitive.class i wygeneruj plik nagłówkowy
TestJNIPrimitive.h poleceniami:
javac TestJNIPrimitive.java
javah TestJNIPrimitive
Implementacja w C - TestJNIPrimitive.c:
Plik nagłówkowy TestJNIPrimitive.h zawiera deklarację funkcji Java_TestJNIPrimitive_average, która
przyjmuje jako parametry zmienne typu JNIEnv * (dający dostęp do środowiska interfejsu JNI),
jobject (dający dostęp do obiektu this) oraz dwa jint
(dwa parametry funkcji natywnej). Wartość zwracana przez funkcję jest typu jdouble:
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint)
Typy JNI jint i jdouble odpowiadają typom Javowym int i double odpowiednio.
Definicje typedef określające mapowanie pomiędzy tymi typami znajdują się w plikach nagłówkowych
jni.h i linux/jni_mh.h (który zależy od platformy).
Plik jni.h zawiera dodatkowo definicję typu jsize.
// jni_md.h
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;
// jni.h
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;
Implementacja pliku TestJNIPrimitive.c wygląda następująco:
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitive.h"
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject thisObj, jint n1, jint n2) {
jdouble result;
printf("W C, przekazane liczby to: %d i %d\n", n1, n2);
result = ((jdouble)n1 + n2) / 2.0;
return result;
}
Skompiluj program w C przy pomocy następujących poleceń:
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
gcc -o libmyjni.so TestJNIPrimitive.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -fPIC
Alternatywna implementacja pliku TestJNIPrimitive.cpp w C++ wygląda następująco:
#include <jni.h>
#include <iostream>
#include "TestJNIPrimitive.h"
using namespace std;
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject thisObj, jint n1, jint n2) {
jdouble result;
cout << "W C++, przekazane liczby to: " << n1 << n2 << endl;
result = ((jdouble)n1 + n2) / 2.0;
return result;
}
Skompiluj program w C++ przy pomocy następujących poleceń:
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
g++ -o libmyjni.so TestJNIPrimitive.cpp -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -fPIC
Program Javowy: TestJNIString.java
public class TestJNIString {
static {
System.loadLibrary("myjni"); // libmyjni.so (Linux)
}
// Metoda natywna, która jako paametr pobiera łańcuch znaków i zwraca również łańcuch znaków
private native String sayHello(String msg);
public static void main(String args[]) {
String result = new TestJNIString().sayHello("Witaj świecie - łańcuch pochodzący z programu Java");
System.out.println("W Javie, zwrócony łańcuch przez program JNI to: " + result);
}
}
Powyższy program deklaruje metodę natywną sayHello, która pobiera łańcuch znaków i zwraca również
łańcuch znaków. Metoda main testuje działanie tej metody natywnej.
Skompiluj program Javy i wygeneruj plik nagłówkowy C/C++ "TestJNIString.h":
javac TestJNIString.java
javah TestJNIString
Implementacja w C - TestJNIString.c
Wygenerowany plik nagłówkowy TestJNIString.h zawiera następującą deklarację:
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring);
Jak widzisz, typ Javowy String jest reprezentowany przez typ jstring w programie
natywnym.
Przekazywanie łańcuchów znaków jest nieco bardziej skomplikowane niż przekazywanie podstawowych typów danych. Powodem
jest to, że typ String w Javie jest obiektem (typem referencyjnym), natomiast łańcuch znaków
w C jest tablicą znaków (pojedyńczych bajtów) zakończonych znakiem (bajtem) NULL. Konieczna jest więc konwersja
między tymi dwoma typami danych.
Metody konwersji między tymi typami dostarcza środowisko JNIEnv *:
const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)
jstring NewStringUTF(JNIEnv*, char*)
Implementacja TestJNIString.c wygląda zatem następująco:
#include <jni.h>
#include <stdio.h>
#include "TestJNIString.h"
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
// krok 1: Konwersja z JNI String (jstring) do C-String (char*)
const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
if (NULL == inCStr) return NULL;
// Krok 2: Wykonaj zamierzone operacje
printf("W C, przekazany w parametrze łańcuch to: %s\n", inCStr);
(*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr); // zwolnij zasoby
// Poproś użytkownika o podanie łańcucha znaków C-string
char outCStr[128];
printf("Podaj łańcuch znaków: ");
scanf("%s", outCStr); // nie więcej niż 127 znaków
// Krok 3: Skonwertuj C-string (char*) na JNI String (jstring) i zwróć rezultat
return (*env)->NewStringUTF(env, outCStr);
}
Jak zaznaczono w komentarzach program wykonuje następujące operacje:
Skompiluj program w C przy pomocy następujących poleceń:
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
Teraz uruchom program poleceniem:
gcc -o libmyjni.so TestJNIString.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -fPIC
> java TestJNIString
W C, przekazany w parametrze łańcuch to: Witaj świecie - łańcuch pochodzący z programu Java
Podaj łańcuch znaków: Witaj!
W Javie, zwrócony łańcuch przez program JNI to: Witaj!
Natywne funkcje JNI operujące na łańcuchach znaków
JNI umożliwia również konwersje dla kodowań łańcuchów Unicode (16-bitowe znaki) oraz
UTF-8 (złożone z 1-3 bajtów). Łańcuchy Unicode i UTF-8
w C również są reprezentowane jako tablice znaków (char *), którymi można się posługiwać
w C/C++.
Dostępne funkcje operujące na łańcuchach, to:
// Łańcuchy UTF-8 (poszczególne znaki reprezentowane są przez 1-3 bajty, kompatybilne wstecznie z 7-bitowym ASCII).
// Mogą być zmapowane do tablic znaków C-string (char *)
// Zwraca wskaźnik na tablicę znaków w kodowaniu UTF-8.
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
// Informuje VM (Java Virtual Machine), że natywny kod nie potrzebuje już łańcucha znaków char* utf.
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
// Tworzy nowy obiekt java.lang.String z tablicy znaków (char*) w kodowaniu UTF-8.
jstring NewStringUTF(JNIEnv *env, const char *bytes);
// Zwraca długość w bajtach łańcucha jstring kodowanego w UTF-8.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
// Wypełnia podany bufor (char *buf) podciągiem znaków ciągu str w kodowaniu UTF-8 zaczynając
// od bajtu start mającego długość length bajtów. Poszczególne znaki 2-3 bajtowe
// nie są traktowane jako jeden znak tylko jako 2-3 niezależne bajty.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
// Łańcuchy Unicode (16-bitowe znaki)
// Zwraca wskaźnik na tablicę znaków w kodowaniu Unicode.
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
// Informuje VM (Java Virtual Machine), że natywny kod nie potrzebuje już łańcucha znaków char* unicode.
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
// Tworzy nowy obiekt java.lang.String z tablicy znaków (char*) w kodowaniu Unicode.
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);
// Zwraca długość w bajtach łańcucha jstring kodowanego w Unicode.
jsize GetStringLength(JNIEnv *env, jstring string);
// Wypełnia podany bufor (char *buf) podciągiem znaków ciągu str w kodowaniu Unicode zaczynając
// od bajtu start mającego długość length bajtów. Poszczególne znaki 2-bajtowe
// nie są traktowane jako jeden znak tylko jako 2 niezależne bajty.
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf);
Łańcuchy UTF-8 a łańcuchy C char*
Funkcja GetStringUTFChars() służy do tworzenia nowego łańcucha znaków języka C - char * z danego łańcuch znaków języka Java - jstring. Zwraca ona wartość NULL jeśli jest za mało pamięci, żeby taki łańcuch utworzyć. Dobrą praktyką jest zawsze sprawdzać czy utworzenie łańcucha się powiodło.
Trzeci parametr tej funkcji isCopy (typu jboolean *), który jest parametrem "in-out" (czyli podaje dane na wejściu do funkcji oraz zwraca dane przy wyjściu z funkcji), powinien być ustawiony na JNI_TRUE jeśli zwracany łańcuch ma być nową kopią oryginalnej instancji java.lang.String (jstring). Jeśli natomiast w parametrze tym przekażemy wartość JNI_FALSE, to zwrócony łańcuch znaków będzie bezpośrednim wskaźnikiem na łańcuch przechowywany w parametrze (jstring). W tym przypadku kod natywny nie powinien zmieniać zawartości zwróconego łańccha znaków. Domyślnie środowisko JNI próbuje zwrócić bezpośredni wskaźnik. Jeśli się to nie uda, to próbuje zwrócić kopię przekazanego łańcucha java.lang.String. Zazwyczaj rzadko jesteśmy zainteresowani modyfikowaniem zwróconego łańcucha znaków i dlatego kjako trzeci parametr przekazujemy wartość NULL.
Należy pamiętać, żeby zawsze wywołać funkcję ReleaseStringUTFChars(), gdy już nie potrzebujemy łańcucha zwróconego przez GetStringUTFChars(). Dzięki temu zwalniamy zaalokowaną pamięć lub referencję na oryginalny łańcuch znaków, pozwalając tym samym na usunięcie jej przez garbage-collectora.
Funkcja NewStringUTF() zwraca nowy łańuch JNI (jstring) z podanego łańcucha C-string (char *).
JDK 1.2 wprowadziło funkcję GetStringUTFRegion(), która kopiuje wartość jstring (lub jej fragment o długości length zaczynając od pozycji start) do zadeklarowanej wcześniej tablicy znaków char[]. Funkcja ta może być użyta w miejsce funkcji GetStringUTFChars(). W tym przypadku wartość isCopy nie jest potrzebna, ponieważ łańcuch jest kopiowany do tablicy, która została już wcześniej zaalokowana.
JDK 1.2 wprowadziło także parę funkcji GetStringCritical() oraz ReleaseStringCritical(). Podobnie jak funkcja GetStringUTFChars(), zwraca ona bezpośredni wskaźnik jeśli jest to możliwe lub kopię oryginalnego łańcucha jeśli nie. Natywna metoda nie powinna się blokować (np. w oczekiwaniu na operacje wejścia/wyjścia - I/O) pomiędzy wywołaniami tych dwóch funkcji.
Więcej szczegółów na temat powyższych funkcji można znaleźć w oficjalnej dokumentacji: http://docs.oracle.com/javase/7/docs/technotes/guides/jni/index.html.
Łańcuchy znaków Unicode
Łańcuchy znaków Unicode przechowywane są w zmiennych typu jchar * zamiast char *.
Implementacja TestJNIString.cpp wygląda następująco:
#include <jni.h>
#include <iostream>
#include <string>
#include "TestJNIString.h"
using namespace std;
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
// krok 1: Konwersja z JNI String (jstring) do C-String (char*)
const char *inCStr = env->GetStringUTFChars(inJNIStr, NULL);
if (NULL == inCStr) return NULL;
// Krok 2: Wykonaj zamierzone operacje
cout << "W C++, przekazany w parametrze łańcuch to: " << inCStr << endl;
env->ReleaseStringUTFChars(inJNIStr, inCStr); // zwolnij zasoby
// Poproś użytkownika o podanie łańcucha znaków C-string
string outCppStr;
cout << "Podaj łańcuch znaków: ";
cin >> outCppStr;
// Krok 3: Skonwertuj C++ string na C-string (char*) a następnie na JNI String (jstring) i zwróć rezultat
return env->NewStringUTF(outCppStr.c_str());
}
Skompiluj program poleceniem:
g++ -o libmyjni.so TestJNIString.cpp -I"/usr/lib/jvm/java-8-openjdk-amd64/include" -I"/usr/lib/jvm/java-8-openjdk-amd64/include/linux" -shared -fPIC
Zwróć uwagę na to, że funkcje natywne w C++ mają nieco inną składnię niż funkcje w C.
W C++ można używać "env->" zamiast "(*env)->".
Ponadto w C++ w wywołaniach funkcji nie ma potrzeby przekazywania argumentu JNIEnv*
jako pierwszego argumentu tych funkcji.
Ponadto używamy tu klasy string zamiast typu C-string: char*.
JNI Program - TestJNIPrimitiveArray.java
public class TestJNIPrimitiveArray {
static {
System.loadLibrary("myjni"); // libmyjni.so
}
// Deklaracja natywnej metody sumAndAverage(), która jako argument pobiera tablicę int[] i
// zwraca tablicę double[2], która w komórce [0] przekazuje sumę a w komórce [1] zwraca średnią
// liczb przekazanych w parametrze
private native double[] sumAndAverage(int[] numbers);
// Test funkcji natywnej
public static void main(String args[]) {
int[] numbers = {22, 33, 33};
double[] results = new TestJNIPrimitiveArray().sumAndAverage(numbers);
System.out.println("In Java, the sum is " + results[0]);
System.out.println("In Java, the average is " + results[1]);
}
}
Implementaja w C - TestJNIPrimitiveArray.c
Plik nagłówkowy TestJNIPrimitiveArray.h zawiera następującą deklarację funkcji:
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage(JNIEnv *, jobject, jintArray);
W Javie typ tablicowy jest typem referencyjnym, podobnym do klasy. Istnieje w niej 9 typów tablicowych -
8 tablic typów podstawowych oraz jedna tablica typu java.lang.Object (każda stworzona przez nas klasa wywodzi
się właśnie od tego typu). JNI definiuje dla każdej z wymienionych tablic oddzielny typ. Zatem mamy tu typy
takie jak: jintArray, jbyteArray, jshortArray,
jlongArray, jfloatArray, jdoubleArray,
jcharArray, jbooleanArray oraz jobjectArray (omówiona
w dalszej części tego tutorialu).
Ponownie, żeby używać typów tablicowych przekazywanych do i z programu natywnego w C konieczna jest konwersja typów, np. jintArray na C jint[] i odwrotnie, czy jdoubleArray na C jdouble[] i odwrotnie. Środowisko JNI oczywiście zapewnia zestaw funkcji do wykonywania tych konwersji:
Istnieje 8 odmian powyższych trzech funkcji, dla każdego z Javowych typów podstawowych.
Tak więc nasz program natywny w C powinien wykonać następujące czynności:
Implementacja TestJNIPrimitiveArray.c wygląda następująco:
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitiveArray.h"
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage
(JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
// Krok 1: Konwersja tablicy JNI jintarray do typu C jint[]
jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
if (NULL == inCArray) return NULL;
jsize length = (*env)->GetArrayLength(env, inJNIArray);
// Krok 2: wykonanie oczekiwanych operacji
jint sum = 0;
int i;
for (i = 0; i < length; i++) {
sum += inCArray[i];
}
jdouble average = (jdouble)sum / length;
(*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0); // zwolnienie pamięci
jdouble outCArray[] = {sum, average};
// Krok 3: Konwersja tablicy C jdouble[] do typu JNI jdoubleArray
jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2); // Przydziel pamięć
if (NULL == outJNIArray) return NULL;
(*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2, outCArray); // Skopiuj zawartość tablicy jint[]
return outJNIArray;
}
Poniżej znajduje się zestaw funkcji operujcych na tablicach dla każdego typu podstawowego Javy:
// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
Funkcje Get<PrimitiveType>ArrayElements i Release<PrimitiveType>ArrayElements
służą odpowiednio do konwersji tablicy natywnej Javy na tablicę C oraz do zwolnienia pamięci zajmowanej przez
tablicę C.
Funkcje Get<PrimitiveType>ArrayRegion i Set<PrimitiveType>ArrayRegion
służą odpowiednio do skopiowania tablicy Javowej JNI do tablicy C oraz odwrotnie.
Funkcja New<PrimitiveType>Array alokuje pamięć dla tablicy Javowej o podanym rozmiarze.
Funkcje GetPrimitiveArrayCritical i ReleasePrimitiveArrayCritical nie pozwalają
wywoływać funkcji blokujących (np. na operacje I/O) pomiędzy wywołaniem pierwszej a drugiej.