Programjąc w Javie czasami niezbędne jest użycie kodu natywnego (C/C++), aby obejść ograniczenia wydajności kodu Java czy też mechanizmu zarządzania pamięcią. W tym właśnie celu Java udostępnia interfejs JNI (Java Native Interface).
JNI jest trudniejsze do opanowania niż standardowy kod Javy, gdyż wymaga poznania dwóch języków programowania oraz ich środowiska uruchomieniowego.
W tutorialu tym zakładam, że znasz:
public class HelloJNI {
static {
System.loadLibrary("hello"); // Załadowanie biblioteki natywnej w pliku uruchomieniowym libhello.so
}
// Deklaracja metody natywnej sayHello(), która nie pobiera żadnych parametrów i nic nie zwraca
private native void sayHello();
// Test metody natywnej - wywołanie jej w funkcji startowej programu Java
public static void main(String[] args) {
new HelloJNI().sayHello();
}
}
Podczas ładowania programu statyczny inicjalizator wywołuje funkcję System.loadLibrary(), aby załadować natywną bibliotekę "hello", zawierającą natywną metodę sayHello(). W systemie Linux
plik tej biblioteki powinien się nazywać libhello.so i powinien się znajdować w ścieżce wyszukiwania bibliotek Javy. Przeszukiwane ścieżki są dostępne w zmiennej systemowej java.library.path.
Jeśli biblioteka nie zostanie znaleziona, to nastąpi rzucenie wyjątku UnsatisfiedLinkError. Aby dodać dowolną ścieżkę do przeszukiwanych przez Javę lokalizacji, należy przy uruchomieniu programu użyć argumentu
-Djava.library.path=sciezka_do_katalogu_zawierajacego_biblioteke.
Następnie deklarujemy metodę natywną sayHello() z wykorzystaniem słowa kluczowego native. Instruuje ono kompilator Javy, że metoda ta została zaimplementowana w innym języku programowania.
Metoda natywna oczywiście nie zawiera ciała.
W metodzie main() tworzymy instancję klasy HelloJNI i wywołujemy na niej naszą metodę natywną.
Kompilujemy plik HelloJNI.java do pliku HelloJNI.class:
javac HelloJNI.java
Teraz uruchamiamy polecenie javah, aby wygenerować plik nagłówkowy w C/C++:
javah HelloJNI
Zostanie wygenerowany plik HelloJNI.h o następującej zawartości:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Jak widzimy, plik nagłówkowy zawiera deklarację funkcji Java_HelloJNI_sayHello:
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
Konwencja nazewnicza funkcji natywnych w C jest następująca: Java_{paczka_i_NazwaKlasy}_{nazwaFunkcji}(JNI argumenty) (kropka w nazwie paczki powinna zostać zastąpiona znakiem podkreślenia '_').
Argumenty powyższej funkcji, to:
W tym momencie nie używamy argumentów w powyższej funkcji, ale zrobimy to nieco później. Na razie także, dla zachowania jak największej prostoty, pominiemy wyjaśnienie makrodefinicji JNIEXPORT
i JNICALL.
Składnia
extern "C"
ma zastosowanie wyłącznie w języku C++. Instruuje ona kompilator tego języka, żeby wykorzystywał protokół nazewniczy funkcji języka C a nie C++. Oba protokoły różnią się między sobą, jako że C++ wspiera tzw. przeciążanie
funkcji, czyli tworzenie funkcji o tej samej nazwie, ale innych argumentach.
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
// Implementacja natwywnej metody sayHello() klasy Javy HelloJNI
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Witaj świecie!\n");
return;
}
Plik nagłówkowy jni.h znajduje się w katalogu: <JAVA_HOME>/include, gdzie <JAVA_HOME> jest katalogiem, w którym zainstalowano JDK, np. /usr/lib/jvm/java-8-openjdk-amd64. W powyższym przykładzie funkcja natywna wyświetla na ekranie tekst "Witaj świecie!" i kończy działanie. Teraz skompilujmy ten kod za pomocą polecenia:
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
gcc -o libhello.so HelloJNI.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -fPIC
W poleceniu tym użyliśmy następujących flag kompilatora:
Po pomyślnie zakończonej kompilacji spróbujmy wywołać polecenie nm (które wyświetla listę symboli w pliku biblioteki) na otrzymanej bibliotece i przefiltrujmy grepem wyniki w poszukiwaniu naszej funkcji:
nm libhello.so | grep say
00000000000006b0 T Java_HelloJNI_sayHello
java -Djava.library.path=. HelloJNI
public class HelloJNICpp {
static {
System.loadLibrary("hello");
}
// Deklaracja metody natywnej
private native void sayHello();
// Test działania metody natywnej
public static void main(String[] args) {
new HelloJNICpp().sayHello();
}
}
Jak widzimy klasa ta jest identyczna z klasą dla kodu w C. Kompilujemy ją wydając polecenie:
javac HelloJNICpp.java
javah HelloJNICpp
Wygenerowany plik wygląda identycznie jak ten dla języka C (i prawidłowo bo jak na razie jedyną zmianą w stosunku do poprzedniego punktu była zmiana nazw utworzonych plików źródłowych).
Właściwy program obsługujący wywołanie JNI implementujemy w języku C++ - są to pliki: HelloJNICppImpl.h i HelloJNICppImpl.cpp. Natomiast kod wywołujący zaimplementowaną przed chwilą funkcjonalność i wywoływany z poziomu Javy implementujemy w C w pliku HelloJNICpp.c.
Nagłówek pliku w C++: HelloJNICppImpl.h
#ifndef _HELLO_JNI_CPP_IMPL_H
#define _HELLO_JNI_CPP_IMPL_H
#ifdef __cplusplus
extern "C" {
#endif
void sayHello ();
#ifdef __cplusplus
}
#endif
#endif
Implementacja funkcji sayHello w pliku w C++: HelloJNICppImpl.cpp
#include "HelloJNICppImpl.h"
#include <iostream>
using namespace std;
void sayHello () {
cout << "Witaj świecie" << endl;
return;
}
Implementacja programu pomostu między kodem w C++ a Javą: HelloJNICpp.c
#include <jni.h>
#include "HelloJNICpp.h"
#include "HelloJNICppImpl.h"
JNIEXPORT void JNICALL Java_HelloJNICpp_sayHello (JNIEnv *env, jobject thisObj) {
sayHello(); // wywołanie funkcji zaimplementowanej w C++
return;
}
Kod utorzony w plikach powyżej kompilujemy poleceniem:
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
g++ -o libhello.so HelloJNICpp.c HelloJNICppImpl.cpp -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -fPIC
java -Djava.library.path=. HelloJNICpp
Wszystkie duże programy Javowe powinny być podzielone na pakiety zamiast być umieszczone w domyślnym pakiecie bez nazwy. Pokażemy tutaj jak uwzględnić nazwy pakietów w bibliotekach JNI.
package pkgjni;
public class HelloJNI {
static {
System.loadLibrary("hello");
}
private native void sayHello();
public static void main(String[] args) {
new HelloJNI().sayHello();
}
}
Plik ten zachowujemy w katalogu "pkgjni/HelloJNI.java" i kompilujemy poleceniem
javac pkgjni/HelloJNI.java
javah -d include pkgjni.HelloJNI
Powyższe polecenie utworzy katalog include w bieżącym katalogu a następnie wygeneruje w nim plik pkgjni_HelloJNI.h. W pliku tym zadeklarowana zostanie natywna funkcja:
JNIEXPORT void JNICALL Java_pkgjni_HelloJNI_sayHello(JNIEnv *, jobject);. Zauważmy, że w nazwie tej funkcji występuje nazwa pakietu (pkgjni). Gdyby pakiet składał się z kilku członów,
wówczas kropki rozdzielające poszczególne jego części zostałyby zastąpione znakami podkreślenia w powyższej nazwie.
#include <jni.h>
#include <stdio.h>
#include "include/pkgjni_HelloJNI.h"
JNIEXPORT void JNICALL Java_naszjni_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Witaj świecie z pakietu pkgjni!\n");
return;
}
Kompilujemy program poleceniem:
gcc -o libhello.so pkgjni_HelloJNI.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -fPIC
I na koniec uruchamiamy program Javowy:
java -Djava.library.path=. pkgjni.HelloJNI
Jeśli nie masz już zainstalowanego Eclipse IDE for Java Developers, pobierz go ze strony Eclipse Downloads a następnie zainstaluj (rozpakuj w dowolnym katalogu i ustaw zmienną PATH, tak by wskazywała na katalog z binarką tego pakietu).
Upewnij się, że masz zainstalowany plugin CDT dla programistów w C/C++. W tym celu wejdź w menu "Help ⇒ About Eclipse".
W okienku, które się pojawi kliknij przycisk "Installation Details". Jeśli masz zainstalowany CDT, to powinien on
figurować na liście oprogramoweania w zakładce "Installed Software" (Dokładnie na tej liście powinny się znajdować
pozycje "C/C++ Development Tools" i "C/C++ Development Tools SDK").
Jeśli nie jest on obecny na tej liście, to powinieneś go zainstalować w następujący sposób:
Uruchom Eclipsa i utwórz w nim nowy projekt Javy np. "HelloJNI". Następnie dodaj nową klasę: HelloJNI.java o następującej zawartości:
public class HelloJNI {
static {
System.loadLibrary("hello"); // libhello.so (Linux)
}
// Declare native method
private native void sayHello();
public static void main(String[] args) {
new HelloJNI().sayHello(); // invoke the native method
}
}
Kliknij prawym przyciskiem myszki na projekcie "HelloJNI"i z menu wybierz pozycję: New ⇒ Other... ⇒ C/C++ ⇒ "Convert to a C/C++ Project (Adds C/C++ Nature)" Wciśnij przycisk "Next".
Pojawi się okienko "Convert to a C/C++ Project". W sekcji "Project type" wybierz pozycję "Makefile Project"
a w sekcji "Toolchains" wybierz "Linux GCC". Kliknij przycisk "Finish".
Od teraz możesz uruchamiać projekt zarówno jako aplikację Javy jak i aplikację C/C++.
Utwórz katalog "jni" w hierarchii projektu. Będzie on przechowywał pliki programu w C. Utwórz plik makefile w katalogu jni. W tym celu kliknij ppm (prawym przyciskiem myszki) folder jni i wybierz menu: "New" ⇒ "File", w okienku które się pojawi wpisz makefile i kliknij przycisk "Finish". Otwórz ten plik i wpisz do niego następujący kod:
# Definiuje zmienną przechowującą ścieżkę classpath
CLASS_PATH = ../bin
# Definiuje wirtualną ścieżkę dla plików .class w katalogu bin
vpath %.class $(CLASS_PATH)
# $* - zmienna, która przechowuje nazwę docelowego pliku bez rozszerzenia
HelloJNI.h : HelloJNI.class
javah -classpath $(CLASS_PATH) $*
UWAGA! Upewnij się, że wcięcia w powyższym pliku robione są znakiem pojedyńczej tabulacji a nie spacji, gdyż taki jest wymóg składni pliku makefile.
Powyższy makefile definiuje zależność celu HelloJNI.h od pliku HelloJNI.class. Dlatego też zawsze kiedy plik skompilowanej klasy Javowej HelloJNI.class będzie nowszy od pliku nagłówkowego HelloJNI.h, zostanie uruchomione polecenie javah, które zaktualizuje ten plik nagłówkowy.
Teraz utwórz nowy Eclipsowy "Build Target" dla utworzonego pliku makefile. W tym celu: ppm na folderze jni, z menu wybierz opcję "Build Targets" ⇒ "Create...". W okienku "Create Build Target", które się pojawi w polu "Target name" wpisz cel "HelloJNI.h". Kliknij przycisk OK. Pod folderem jni pojawi się nowa pozycja "Build Targets". Rozwiń ją a zobaczysz, że figuruje tam utworzony target HelloJNI.h. Kliknij go dwukrotnie lewym przyciskiem myszy (lpm). Wywołany zostanie nasz plik makefile i za pomocą polecenia javah zostanie wygenerowany plik "HelloJNI.h". Aby to sprawdzić, wystarczy, że przełączysz się do zakładki Console. Powinna ona zawartość podobną do poniższej:
14:58:59 **** Build of configuration Build (GNU) for project HelloJNI ****
make HelloJNI.h
javah -classpath ../bin HelloJNI
14:58:59 Build Finished (took 439ms)
Alternatywnie kompilację możesz wykonać w konsoli systemowej. Wystarczy, że przejdziesz w niej do katalogu jni
w naszym projekcie i wywołasz polecenie:
make HelloJNI.h
Utwórz program HelloJNI.c klikając ppm na folderze jni a następnie New ⇒ Source file. W okienku "Source file", które się pojawi wpisz "HelloJNI.c" i kliknij "Finish".
W nowo utworzonym pliku wpisz następujący kod:
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Witaj świecie!\n");
return;
}
Zmodyfikuj plik makefile następująco:
# Definiuje zmienną przechowującą ścieżkę classpath
CLASS_PATH = ../bin
JAVA_HOME = /usr/lib/jvm/java-8-openjdk-amd64
# Definiuje wirtualną ścieżkę dla plików .class w katalogu bin
vpath %.class $(CLASS_PATH)
all : libhello.so
# $@ - zmienna przechowująca plik docelowy razem z rozszerzeniem, $< - zmienna przechowująca pierwszy plik zależności
libhello.so : HelloJNI.o
gcc -o $@ $< -I"$(JAVA_HOME)/include" -I"$(JAVA_HOME)/include/linux" -shared
# $@ - zmienna przechowująca plik docelowy razem z rozszerzeniem, $< - zmienna przechowująca pierwszy plik zależności
HelloJNI.o : HelloJNI.c HelloJNI.h
gcc -c $< -o $@ -I"$(JAVA_HOME)/include" -I"$(JAVA_HOME)/include/linux" -fPIC
# $* - zmienna, która przechowuje nazwę docelowego pliku bez rozszerzenia
HelloJNI.h : HelloJNI.class
javah -classpath $(CLASS_PATH) $*
clean :
rm HelloJNI.h HelloJNI.o libhello.so
UWAGA! Upewnij się, że wcięcia w powyższym pliku robione są znakiem pojedyńczej tabulacji a nie spacji, gdyż taki jest wymóg składni pliku makefile.
W sposób opisany tutaj utwórz cele "all" i "clean". Następnie kliknij lpm w cel "all" znajdujący się w drzewie projektu pod pozycją "Build Targets". Program zostanie skompilowany i powstanie plik biblioteki dynamicznej libhello.so. Zawartość konsoli powinna wyglądać podobnie jak poniżej:
16:23:02 **** Build of configuration Build (GNU) for project HelloJNI ****
make all
javah -classpath ../bin HelloJNI
gcc -c HelloJNI.c -o HelloJNI.o -I"/usr/lib/jvm/java-8-openjdk-amd64/include" -I"/usr/lib/jvm/java-8-openjdk-amd64/include/linux" -fPIC
gcc -o libhello.so HelloJNI.o -I"/usr/lib/jvm/java-8-openjdk-amd64/include" -I"/usr/lib/jvm/java-8-openjdk-amd64/include/linux" -shared
16:23:02 Build Finished (took 502ms)
Teraz prawie możesz już uruchomić program Javowy. Zanim jednak to zrobisz musisz jeszcze podać linkerowi gdzie ma szukać pliku biblioteki natywnej libhello.so. W tym celu kliknij ppm na projekcie i z menu wybierz: "Run As" ⇒ "Run Configurations...". W okienku "Run Configurations" kliknij na liście z lewej strony pozycję "HelloJNI" znajdującą się w węźle "Java Application". Upewnij się, że w zakładce "Main", w centralnej części okna, pole "Main class" zawiera wpis "HelloJNI". Następnie przełącz się do zakładki "Arguments" i w polu "VM arguments" wpisz "-Djava.library.path=jni" - czyli ścieżkę do naszej biblioteki. Kliknij przycisk "Run".
W konsoli (zakładka "Console") zobaczysz nasz napis: Witaj świecie!.