Program JNI - TestJNIInstanceVariable.java:
public class TestJNIInstanceVariable {
static {
System.loadLibrary("myjni"); // libmyjni.so
}
// Atrybuty obiektu klasy
private int number = 88;
private String message = "Hello from Java";
// Deklaracja metody natywnej, która modyfikuje atrybuty obiektu
private native void modifyInstanceVariable();
// Test działania metody natywnej
public static void main(String args[]) {
TestJNIInstanceVariable test = new TestJNIInstanceVariable();
test.modifyInstanceVariable();
System.out.println("In Java, int is " + test.number);
System.out.println("In Java, String is " + test.message);
}
}
Powyższa klasa posiada dwa atrybuty: number typu podstawowego int oraz message typu obiektowego String. Klasa ta deklaruje również metodę natywną modifyInstanceVariable(), która modyfikuje zawartość każdego z tych atrybutów.
Implementacja w C - TestJNIInstanceVariable.c:
#include <jni.h>
#include <stdio.h>
#include "TestJNIInstanceVariable.h"
JNIEXPORT void JNICALL Java_TestJNIInstanceVariable_modifyInstanceVariable
(JNIEnv *env, jobject thisObj) {
// Pobierz referencję do klasy obiektu thisObj
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
// int
// Pobierz identyfikator atrybutu "number" w klasie thisClass
jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
if (NULL == fidNumber) return;
// Pobierz wartość atrybutu "number" w obiekcie thisObj
jint number = (*env)->GetIntField(env, thisObj, fidNumber);
printf("In C, the int is %d\n", number);
// Zmień wartość atrybutu "number"
number = 99;
(*env)->SetIntField(env, thisObj, fidNumber, number);
// Pobierz identyfikator atrybutu "message" w klasie thisClass
jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
if (NULL == fidMessage) return;
// String
// Pobierz wartość atrybutu "message" w obiekcie thisObj
jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);
// Utwórz łańcuch znaków języka C ze zmiennej JNI typu String
const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
if (NULL == cStr) return;
printf("In C, the string is %s\n", cStr);
(*env)->ReleaseStringUTFChars(env, message, cStr);
// Przypisz do zmiennej JNI typu String nową wartość określoną przez łańcuch znaków C
message = (*env)->NewStringUTF(env, "Hello from C");
if (NULL == message) return;
// Zmień wartość atrybutu "message"
(*env)->SetObjectField(env, thisObj, fidMessage, message);
}
Aby uzyskać dostęp do atrybutu obiektu należy:
Podsumowując, metodami pozwalającymi na dostęp do atrybutów obiektów klas są:
// Zwraca klasę dla poddanego obiektu
jclass GetObjectClass(JNIEnv *env, jobject obj);
// Zwraca identyfikator (field ID) dla podanego atrybutu obiektu klasy
jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Pobiera/Ustawia wartość atrybutu obiektu
// <typ> reprezentuje nazwę każdego z ośmiu typów podstawowych plus typ obiektowy
TypNatywny Get<typ>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
void Set<typ>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
Uzyskiwanie dostępu do statycznych zmiennych klas jest podobne do uzyskiwania dostępu do atrybutów obiektów z tą różnicą, że tutaj wykorzystujemy GetStaticFieldID(), Get|SetStaticObjectField(), Get|SetStatic<Typ-Podstawowy>Field().
Program JNI - TestJNIStaticVariable.java:
public class TestJNIStaticVariable {
static {
System.loadLibrary("myjni"); // libmyjni.so
}
// Zmienna statyczna
private static double number = 55.66;
// Deklaracja metody natywnej, która modyfikuje zmienną statyczną klasy
private native void modifyStaticVariable();
// Test metody natywnej
public static void main(String args[]) {
TestJNIStaticVariable test = new TestJNIStaticVariable();
test.modifyStaticVariable();
System.out.println("W Javie, wartość typu double wynosi: " + number);
}
}
Implementacja w C - TestJNIStaticVariable.c:
#include <jni.h>
#include <stdio.h>
#include "TestJNIStaticVariable.h"
JNIEXPORT void JNICALL Java_TestJNIStaticVariable_modifyStaticVariable
(JNIEnv *env, jobject thisObj) {
// Pobierz referencję do klasy obiektu thisObj
jclass cls = (*env)->GetObjectClass(env, thisObj);
// Pobierz identyfikator zmiennej statycznej number
jfieldID fidNumber = (*env)->GetStaticFieldID(env, cls, "number", "D");
if (NULL == fidNumber) return;
// Odczytaj wartość zmiennej statycznej number
jdouble number = (*env)->GetStaticDoubleField(env, cls, fidNumber);
printf("W kodzie C, wartość zmiennej typu double wynosi %f\n", number);
number = 77.88;
// Ustaw nową wartość zmiennej statycznej number
(*env)->SetStaticDoubleField(env, cls, fidNumber, number);
}
Metodami pozwalającymi na dostęp do zmiennych statycznych klas są:
// Zwraca identyfikator (field ID) zmiennej statycznej klasy.
jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Pobiera/Ustawia wartość zmiennej statycznej klasy
// <typ> reprezentuje nazwę każdego z ośmiu typów podstawowych plus typ obiektowy
NativeType GetStatic<typ>Field(JNIEnv *env, jclass cls, jfieldID fieldID);
void SetStatic<typ>Field(JNIEnv *env, jclass cls, jfieldID fieldID, NativeType value);
Program JNI - TestJNICallBackMethod.java:
public class TestJNICallBackMethod {
static {
System.loadLibrary("myjni"); // libmyjni.so
}
// Deklaracja metody natywnej, która wywołuje poniższe metody Javy
private native void nativeMethod();
// Metody wywoływane przez kod natywny
private void callback() {
System.out.println("W Javie");
}
private void callback(String message) {
System.out.println("W Javie z parametrem message: " + message);
}
private double callbackAverage(int n1, int n2) {
return ((double)n1 + n2) / 2.0;
}
// Statyczna metoda wywoływana przez kod natywny
private static String callbackStatic() {
return "Łańcuch znaków zwrócony przez metodę Javy";
}
// Test metody natywnej
public static void main(String args[]) {
new TestJNICallBackMethod().nativeMethod();
}
}
Klasa ta deklaruje natywną metodę nativeMethod() i wywołuje ją w metodzie main. Z kolei metoda nativeMethod() wywołuje metody instancji: callback(), callback(String), callbackAverage(int,int) oraz metodę statyczną klasy: callbackStatic().
Implementacja w C - TestJNICallBackMethod.c:
#include <jni.h>
#include <stdio.h>
#include "TestJNICallBackMethod.h"
JNIEXPORT void JNICALL Java_TestJNICallBackMethod_nativeMethod
(JNIEnv *env, jobject thisObj) {
// Pobierz referencję do klasy obiektu thisObj
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
// Pobierz identyfikator (Method ID) metody "callback", która nie pobiera żadnych argumentów i nic nie zwraca
jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V");
if (NULL == midCallBack) return;
printf("W C, wywołanie Javowej metody callback()\n");
// Wywołaj metodę "callback"
(*env)->CallVoidMethod(env, thisObj, midCallBack);
jmethodID midCallBackStr = (*env)->GetMethodID(env, thisClass, "callback", "(Ljava/lang/String;)V");
if (NULL == midCallBackStr) return;
printf("W C, wywołanie Javowej metody callback(String)\n");
jstring message = (*env)->NewStringUTF(env, "Witaj w C");
(*env)->CallVoidMethod(env, thisObj, midCallBackStr, message);
jmethodID midCallBackAverage = (*env)->GetMethodID(env, thisClass, "callbackAverage", "(II)D");
if (NULL == midCallBackAverage) return;
jdouble average = (*env)->CallDoubleMethod(env, thisObj, midCallBackAverage, 2, 3);
printf("W C, średnia wynosi %f\n", average);
jmethodID midCallBackStatic = (*env)->GetStaticMethodID(env, thisClass, "callbackStatic", "()Ljava/lang/String;");
if (NULL == midCallBackStatic) return;
jstring resultJNIStr = (*env)->CallStaticObjectMethod(env, thisClass, midCallBackStatic);
const char *resultCStr = (*env)->GetStringUTFChars(env, resultJNIStr, NULL);
if (NULL == resultCStr) return;
printf("W C, zwrócony łańcuch znaków to: %s\n", resultCStr);
(*env)->ReleaseStringUTFChars(env, resultJNIStr, resultCStr);
}
Aby wywołać metodę instancji w kodzie natywnym, należy:
javap -s -p TestJNICallBackMethod
.......
private void callback();
Signature: ()V
private void callback(java.lang.String);
Signature: (Ljava/lang/String;)V
private double callbackAverage(int, int);
Signature: (II)D
private static java.lang.String callbackStatic();
Signature: ()Ljava/lang/String;
.......
Poniżej umieszczone są wszystkie funkcje, których używamy do wywoływania metod instancyjnych oraz metod statycznych:
// Zwraca identyfikator metody instancyjnej klasy lub interfejsu
jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Wywołuje metodę obiektu (instancji).
// The <typ> to jeden z typów podstawowych lub typ obiektowy (Object).
NativeType Call<typ>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<typ>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<typ>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
// Zwraca identyfikator metody statycznej klasy lub interfejsu
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Wywołuje metodę statyczną klasy
// <typ> to jeden z typów podstawowych lub typ obiektowy (Object).
NativeType CallStatic<typ>Method(JNIEnv *env, jclass cls, jmethodID methodID, ...);
NativeType CallStatic<typ>MethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallStatic<typ>MethodV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
JNI udostępnia zestaw funkcji CallNonvirtual<Typ>Method() służących do wywoływania metod wirtualnych zdefiniowanych w klasie z której odziedziczono (coś w stylu super.methodName()). Aby to zrobić, należy:
Zestaw funkcji Nonwirtual zaprezentowano poniżej:
NativeType CallNonvirtual<typ>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtual<typ>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<typ>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);
Możesz utworzyć nowe zmienne typów jobject i jobjectArray w kodzie natywnym stosując wywołania funkcji NewObject() i NewObjectArray(). Następnie utworzone tak zmienne możesz przekazać z powrotem do programu w Javie.
Wywołanie konstruktora w kodzie natywnym jest podobne do wywołania dowolnej metody. Najpierw pobieramy identyfikator konstruktora przekazując przekazując łańcuch "<init>" jako nazwę metody oraz "V" jako zwracaną wartość. Następnie możesz wywołać jedną z metod postaci NewObject(), żeby wywołać konstruktor i utworzyć nowy obiekt Javy.
Program JNI - TestJavaConstructor.java:
public class TestJavaConstructor {
static {
System.loadLibrary("myjni"); // libmyjni.so
}
// Natywna metoda, która wywołuje konstrutor klasy Integer i zwraca utworzony obekt.
// Zwraca utworzony obiekt typu Integer, który przechowuje podaną wartość number.
private native Integer getIntegerObject(int number);
public static void main(String args[]) {
TestJavaConstructor test = new TestJavaConstructor();
System.out.println("In Java, the number is :" + test.getIntegerObject(9999));
}
}
Powyższa klasa deklaruje metodę natywną getIntegerObject(). Kod natywny tej metody tworzy na podstawie przekazanego argumentu number obiekt klasy Integer i zwraca go do programu w Javie.
Implementacja w C - TestJavaConstructor.c:
#include <jni.h>
#include <stdio.h>
#include "TestJavaConstructor.h"
JNIEXPORT jobject JNICALL Java_TestJavaConstructor_getIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
// Pobierz referencję do klasy java.lang.Integer
jclass cls = (*env)->FindClass(env, "java/lang/Integer");
// Pobierz identyfikator konstruktora w powyższej klasie, który przyjmuje wartość int
jmethodID midInit = (*env)->GetMethodID(env, cls, "<init>", "(I)V");
if (NULL == midInit) return NULL;
// Wywołaj konstruktor, który utworzy instancję obiektu powyższej klasy
jobject newObj = (*env)->NewObject(env, cls, midInit, number);
// Pobierz identyfikator metody toString a następnie go wywołaj w celu przetestowania
// poprawności utworzonego obiektu
jmethodID midToString = (*env)->GetMethodID(env, cls, "toString", "()Ljava/lang/String;");
if (NULL == midToString) return NULL;
jstring resultStr = (*env)->CallObjectMethod(env, newObj, midToString);
const char *resultCStr = (*env)->GetStringUTFChars(env, resultStr, NULL);
printf("In C: the number is %s\n", resultCStr);
return newObj;
}
Funkcje JNI wykorzystywane w procesie tworzenia obiektów Javy znajdują się na poniższej liście:
jclass FindClass(JNIEnv *env, const char *name);
// Tworzy nowy obiect Javy. Argument methodID określa, który konstruktor klasy cls wywołać
jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
// Alokuje pamięć na nowy obiekt Javy bez wywoływania żadnego konstruktora
jobject AllocObject(JNIEnv *env, jclass cls);
Program JNI - TestJNIObjectArray.java:
public class TestJNIObjectArray {
static {
System.loadLibrary("myjni"); // libmyjni.so
}
// Metoda natywna, która pobiera tablicę Integer[] i zwraca
// tablicę Double[2], której element [0] przechowuje sumę a [1] średnią podanych argumentów
private native Double[] sumAndAverage(Integer[] numbers);
public static void main(String args[]) {
Integer[] numbers = {11, 22, 32}; // tablica elementów testowych
Double[] results = new TestJNIObjectArray().sumAndAverage(numbers);
System.out.println("In Java, the sum is " + results[0]);
System.out.println("In Java, the average is " + results[1]);
}
}
Powyższy kod deklaruje metodę natywną, która pobiera tablicę obiektów typu Integer, wylicza ich sumę i średnią a następnie zwraca wyliczone wartości w tablicy obiektów typu Double. Na uwagę zasługuje fakt, że metoda ta zarówno pobiera tablicę obiektów jak i zwraca tablicę obiektów.
Implementacja w C - TestJNIObjectArray.c:
#include <jni.h>
#include <stdio.h>
#include "TestJNIObjectArray.h"
JNIEXPORT jobjectArray JNICALL Java_TestJNIObjectArray_sumAndAverage
(JNIEnv *env, jobject thisObj, jobjectArray inJNIArray) {
// Pobierz referencję do klasy java.lang.Integer
jclass classInteger = (*env)->FindClass(env, "java/lang/Integer");
// Pobierz identyfikator metody Integer.intValue() zwracającej wartość int obiektu klasy Integer
jmethodID midIntValue = (*env)->GetMethodID(env, classInteger, "intValue", "()I");
if (NULL == midIntValue) return NULL;
//Pobierz ilość elementów tablicy przekazanej w parametrze
jsize length = (*env)->GetArrayLength(env, inJNIArray);
jint sum = 0;
int i;
for (i = 0; i < length; i++) {
// Pobierz i-ty obiekt z tablicy
jobject objInteger = (*env)->GetObjectArrayElement(env, inJNIArray, i);
if (NULL == objInteger) return NULL;
// Wywołaj metodę intValue na pobranym obiekcie
jint value = (*env)->CallIntMethod(env, objInteger, midIntValue);
// zaktualizuj sumę
sum += value;
}
// oblicz średnią
double average = (double)sum / length;
printf("In C, the sum is %d\n", sum);
printf("In C, the average is %f\n", average);
//Pobierz referencję na klasę java.lang.Double
jclass classDouble = (*env)->FindClass(env, "java/lang/Double");
// Utwórz tablicę 2 elementów klasy java.lang.Double
jobjectArray outJNIArray = (*env)->NewObjectArray(env, 2, classDouble, NULL);
// Pobierz identyfikator konstruktora klasy Double
jmethodID midDoubleInit = (*env)->GetMethodID(env, classDouble, "<init>", "(D)V");
if (NULL == midDoubleInit) return NULL;
// Utwórz dwa obiekty klasy Double przechowujące sumę i średnią odpowiednio
jobject objSum = (*env)->NewObject(env, classDouble, midDoubleInit, (double)sum);
jobject objAve = (*env)->NewObject(env, classDouble, midDoubleInit, average);
// Ustaw elementy tablicy
(*env)->SetObjectArrayElement(env, outJNIArray, 0, objSum);
(*env)->SetObjectArrayElement(env, outJNIArray, 1, objAve);
// i zwróć tablicę do programu Javy
return outJNIArray;
}
W przeciwieństwie do tablic typów podstawowych, które mogą być pobierane i/lub ustawiane bezpośrednio, dostęp do elementów tablic obiektowych wymaga użycia funkcji Get|SetObjectArrayElement().
Funkcje potrzebne do przetwarzania tablic obiektów wymieniono poniżej:
// Tworzy nową tablicę przechowującą length elementów klasy elementClass.
// Wszystkie elementy są zainicjalizowane domyślnie wartością initialElement.
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
// Pobiera element klasy array znajdujący się na pozycji index
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
// Przypisuje elementowi klasy array znajdującemu się na pozycji index
// wartość value
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
Zarządzanie referencjami ma kluczowe znaczenie przy pisaniu wydajnego kodu natywnego. Na przykład często używamy funkcji FindClass(), GetMethodID(), GetFieldID(), aby uzyskać odpowiednio referencję do klasy jclass, identyfikator metody jmethodID oraz identyfiaktor atrybutu jfieldID w kodzie natywnym. Zamiast wywoływać powyższe metody wiele razy, za każdym razem kiedy są potrzebne, powinniśmy wywołać je raz i skeszować zwrócone przez nie wyniki w celu poprawienia wydajności programu.
JNI dzieli referencje na obiekty/klasy, wykorzystywane w kodzie natywnym, na referencje lokalne i globalne:
Przykładowy program JNI - TestJNIReference.java:
public class TestJNIReference {
static {
System.loadLibrary("myjni"); // libmyjni.so
}
// Metoda natywna zwracająca obiekt java.lang.Integer przechowujący podaną wartość int
private native Integer getIntegerObject(int number);
// Inna metoda natywna zwracająca obiekt java.lang.Integer przechowujący podaną wartość int
private native Integer anotherGetIntegerObject(int number);
public static void main(String args[]) {
TestJNIReference test = new TestJNIReference();
System.out.println(test.getIntegerObject(1));
System.out.println(test.getIntegerObject(2));
System.out.println(test.anotherGetIntegerObject(11));
System.out.println(test.anotherGetIntegerObject(12));
System.out.println(test.getIntegerObject(3));
System.out.println(test.anotherGetIntegerObject(13));
}
}
Powyższy kod deklaruje dwie metody natywne, które pobierają wartość typu int i tworzą przechowujący ją obiekt typu java.lang.Integer.
W implementacji w C potrzebujemy pobrać referencję na klasę java.lang.Integer przy pomocy funkcji FindClass(). Następnie musimy pobrać identyfikator konstruktora tej klasy, żeby móc utworzyć odpowiednią zmienną obiektową. Zamiast robić to za każdym razem, gdy wywoływana jest dowolna z dwóch powyższych metod natywnych możemy to zrobić raz i przechować otrzymane wartości w referencjach globalnych. Następnie za każdym wywołaniem tych metod natywnych pobieramy odpowiednie wartości z tych globalnych referencji, przyspieszając działanie programu.
Implementacja w C - TestJNIReference.c:
#include <jni.h>
#include <stdio.h>
#include "TestJNIReference.h"
// Global Reference to the Java class "java.lang.Integer"
static jclass classInteger;
static jmethodID midIntegerInit;
jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {
// Get a class reference for java.lang.Integer if missing
if (NULL == classInteger) {
printf("Uzyskanie reerencji na klasę java.lang.Integer\n");
// FindClass returns a local reference
jclass classIntegerLocal = (*env)->FindClass(env, "java/lang/Integer");
// Create a global reference from the local reference
classInteger = (*env)->NewGlobalRef(env, classIntegerLocal);
// No longer need the local reference, free it!
(*env)->DeleteLocalRef(env, classIntegerLocal);
}
if (NULL == classInteger) return NULL;
// Get the Method ID of the Integer's constructor if missing
if (NULL == midIntegerInit) {
printf("Pobierz identyfikator konstruktora klasy java.lang.Integer's\n");
midIntegerInit = (*env)->GetMethodID(env, classInteger, "<init>", "(I)V");
}
if (NULL == midIntegerInit) return NULL;
// Call back constructor to allocate a new instance, with an int argument
jobject newObj = (*env)->NewObject(env, classInteger, midIntegerInit, number);
printf("W C, utworzono obiekt klasy java.lang.Integer zawierający wartość %d\n", number);
return newObj;
}
JNIEXPORT jobject JNICALL Java_TestJNIReference_getIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
return getInteger(env, thisObj, number);
}
JNIEXPORT jobject JNICALL Java_TestJNIReference_anotherGetIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
return getInteger(env, thisObj, number);
}
W powyższym programie wywołujemy funkcję FindClass(), żeby uzyskać lokalną referencję na klasę java.lang.Integer. Następnie tak otrzymaną wartość konwertujemy przy pomocy funkcji NewGlobalRef() na referencję globalną i zapisujemy zwróconą przez nią wartość w statycznej zmiennej globalnej. Po tej operacji możemy zwolnić referencję lokalną przy pomocy funkcji DeleteLocalRef, gdyż nie jest ona nam już potrzebna.
Należy tu zwrócić uwagę na to, że zmienne jmethodID i jfieldID nie są obiektami jobject, tylko zwykłymi strukturami języka C i nie można z nich tworzyć referencji globalnych. Można je bezpośrednio zachować w statycznych zmiennych globalnych.