Zenedith's dev blog

Dałem się namówić.., szatanowi chyba..

JNI #1 – Eclipse + CDT + MinGW

Pisałem już o różnych językach skryptowych – postaram się opisać przynajmniej podstawy współpracy z cpp z każdym z nich. Dziś przedstawię jak wygląda współpraca Javy i cpp przez interfejs JNI (Java Native Interface).

Tak jak w temacie, pokażę jak napisać prosty pomost między Javą a cpp przy wykorzystaniu IDE Eclipse z biblioteką CDT5.0 oraz wykorzystując toolchain’a MinGW. O sposobie konfiguracji takiego środowiska wspomniałem już wcześniej, a więc zaczynamy.

Naszym zadaniem będzie napisanie kodu w Javie, który będzie wykorzystywał dll’ke, którą przygotujemy wykorzystując język cpp i toolchain’a MinGW – będziemy przesyłać i odbierać dane z dll’a.

Lista wymagań jest następująca:

  1. Eclipse + CDT + MinGW,
  2. Java JDK (nie musi być koniecznie nowe), JRE nie wystarczy.

Ze wspomnianego JDK wykorzystamy następujące rzeczy:

  • bin\javah.exe – pozwoli automatycznie wygenerować nagłówek języka cpp(.h) dla danej klasy Javy,
  • include\jni.h – dostarcza „funkcje” do konwersji typów Java <-> cpp oraz wiele innych,
  • include\$platform$ – $platform$ w moim przypadku to win32.

Kolejne kroki które będziemy wykonywać przedstawiają się następująco:

  1. napisanie kodu klas(y) w Javie z użyciem słowa kluczowego native,
  2. kompilacja klas(y) – otrzymujemy pliki .class,
  3. wygenerowanie nagłówka dla języka cpp (.h) za pomocą narzędzia javah.exe,
  4. implementacja wygenerowanego pliku nagłówkowego w projekcie w języku cpp,
  5. skompilowanie kodu do postaci pliku biblioteki dynamicznej (dll/so),
  6. uruchomienie aplikacji Javy.

Zaczynamy – punkt 1 i napisanie kodu klasy w Javie.
Jak już wspomniałem, użyjemy słowa kluczowego native do oznaczenia metod, które chcielibyśmy zaimplementować w języku cpp.

Niech za przykład takiego kodu w Javie posłuży nam następująca klasa:

public class SomeData {

native String GetVersion();
native void SetVersion(String sNewVersion);
native void DoSomething();

static{

System.loadLibrary("libjni_test");

}

public String GetSomeDataFromJava(){

return "dane z javy";

}

public static void main(String[] args) {

SomeData cData = new SomeData();
System.out.println("Version: "+cData.GetVersion());

}

}

Parę słów komentarza: na początku mamy metody natywne, które będziemy implementować w cpp – podajemy więc ich definicję i na tym kończymy. Następnie widoczny jest blok static{} w którym podajemy informację do JVM, z jakiej dokładnie biblioteki dll/so będziemy korzystać.
Ważne są dwie rzeczy:

  1. nie podajemy tam rozszerzenia pliku (bez dll/so) ponieważ w zależności od systemu JVM zrobi to za nas,
  2. nie musimy ładowania biblioteki zewnętrznej umieszczać w bloku static{} – równie dobrze można to zrobić zaraz na początku metody main(String[] args).

Ostatecznie w metodzie main(String[] args) wywołujemy naszą natywną metodę w celu sprawdzenia efektu.

Czas na punkt 2 z listy czyli kompilację – tak naprawdę dzieje się ona z automatu jeśli mamy w Eclipse ustawioną opcję automatycznego kompilowania przy zapisywaniu pliku. Jeśli więc utworzyliśmy prosty projekt Javy, to plik źródłowy (.java) znajduje się w katalogu /src/ a skompilowana klasa w katalogu /bin/.

W punkcie następnym użyjemy generatora nagłowka (.h) – narzędzia javah.exe. Jeśli jest on w naszej zmiennej środowiskowej PATH to wydanie polecania:

javah -jni $nasza_klasa$

w katalogu \src\ powinno zakończyć się sukcesem i wygenerowanym nagłówkiem, lecz może być jeszcze konieczne przekopiowanie pliku skompilowanego (.class) z katalogu \bin\ do \src\.
Natomiast jeśli wydanie powyższego polecania zgłasza błąd nieznanego polecenia, należy edytować zmienną PATH (np. z poziomu właściwości mój komputer, na zakładce Zaawansowane, przycisk zmienne środowiskowe, dodając dodatkowy wpis do zmiennej PATH, który będzie zawierał ścieżkę do pliku wykonywalnego java, javac, javah, itp. – np. D:\Program Files\Java\jdk1.6.0_06\bin\należy uważać żeby nie nadpisać jej całkowicie a tylko dopisać dodając średnik jeśli to konieczne do oddzielenia ścieżek).

W tym momencie mamy już wygenerowany plik nagłówkowy więc możemy w Eclipse opcjonalnie przełączyć perspektywę na C/C++ i utworzyć projekt C++ (New C++ Project) – również pisałem już o tym wcześniej, najważniejsze żeby dostępny był toolchain MinGW oraz rodzaj projektu wybrać Empty Project (.exe) ewentualnie docelowy Shared Library.

Po utworzeniu pustego projektu, należy dokonać na starcie paru ustawień – przechodzimy do jego właściwości:

  • na zakładce C/C++ Bulid przechodzimy do Settings/Tool Settings,
  • przechodzimy do GCC C++ Compiler i dalej do Directories,
  • klikając przycisk dodajemy katalogi \include i \include\win32 – (w moim przypadku są to D:\Program Files\Java\jdk1.6.0_06\include oraz D:\Program Files\Java\jdk1.6.0_06\include\win32)
  • zatwierdzamy wprowadzone zmiany,
  • następnie odnajdujemy zakładkę MinGW C++ Linker, która jest na poziomie C/C++ Bulid,
  • przechodzimy do zakładki Miscellaneous i w Linker Flags wpisujemy:
    -Wl,–add-stdcall-alias (przed add są dwa minusy)
    Bardzo istotna opcja, ponieważ dzięki niej nie dojdzie do „udziwnienie” metod w wynikowym pliku biblioteki dynamicznej. Gdybyśmy o tej opcji zapomnieli, to pomimo że nasza biblioteka byłaby widoczna, to JVM nie odnalazłaby w niej określonych nazw, właśnie przez te „udziwnienia”, tworzone przez linker z toolchain’a MinGW.
  • sprawdzamy jeszcze, czy na zakładce poniżej Miscellaneous-Shared Library Settings zaznaczona jest pierwsza opcja od góry Shared (jeśli tworzony był Empty Project trzeba ją zaznaczyć),
  • opcjonalnie, jeśli korzystaliśmy z Empty Project musimy zmienić ustawienia generowania pliku wykonywalnego na generowania dll/so – uczynimy to zmieniając zakładkę Tool Settings na Build Artifact i ustawiając pole Artifact Type na Shared Library. Resztę opcji możemy tak pozostawić chyba że pragniemy dla przykładu nadać jakąś specyficzną nazwę dla biblioteki dynamicznej.
  • zatwierdzamy wprowadzone zmiany i powracamy do ekranu głównego.

Teraz możemy zaimportować wygenerowany przez javah plik nagłówkowy (import/File System/podajemy scieżkę do wygenerowanego pliku nagłówkowego). Gdy spróbujemy ten plik podejrzeć,  include jni.h nie powinien zgłaszać błędów (w p.p sprawdź include directory), lecz syntax error’y pojawią się przy wszystkich liniach rozpoczynających się np. tak JNIEXPORT jstring JNICALL, czyli wszystkich eksportowanych metodach z/do Javy – nie musimy się tym w ogóle przejmować.

Teraz należy utworzyć plik implementacji zaimportowanego pliku nagłówkowego czyli wkraczamy w punkt 4 – dodajemy w nim include’y dla pliku nagłówkowego i jni.h oraz kopiujemy definicje metod z pliku nagłówkowego i uzupełniamy je o nazwy zmiennych w metodach(są tylko typy) oraz ciało metod.

Dla przykładu podam ciało metody zwracającej z dll’ki napis typu String:

JNIEXPORT jstring JNICALL Java_SomeData_GetVersion(JNIEnv * env, jobject o){
const char *str = "1.1 MinGW compiled";
return env->NewStringUTF(str);
}

Jak widać w powyższym, nie ma bezpośredniej konwersji typów między Javą i cpp, dlatego musimy się posługiwać metodami konwersji, które dostępne są przez odwołanie do JNIEnv, która jest tablicą metod pomocniczych.

Kompilujemy kod (Bulid Project) i jeśli wszystko dobrze wcześniej ustawiliśmy, otrzymamy w wyniku plik biblioteki dynamicznej o nazwie projektu z przedrostkiem lib lub podaną przy ustawieniach Arifact name, co kończy punkt 5.

Przed wykonaniem ostatniego punktu, czyli odpalenia aplikacji Javy, musimy w odpowiednim miejscu umieścić nasz plik wynikowy biblioteki dynamicznej. Możemy go umieścić w scieżkę wskazywanej przez java.library.path lub umieścić w wybranym miejscu i w ustawieniach projektu Javy – Java Build Path, rozwinąć zakładkę Source i edytować ostatnią pozycję o nazwie Native Library Location, wskazując folder gdzie znajduje się skompilowany dll/so.

Czas na wykonanie ostatniego kroku i uruchomienie (najszybciej w Eclipse z opcji Run As/Java application) kodu. W wyniku nie powinniśmy otrzymać błędu java.lang.UnsatisfiedLinkError, a jeśli się to zdarzy, to należy uważnie przejrzeć opisane wcześniej czynności.

W ten sposób kończę ten najdłuższy do tej pory wpis na blogu.
Z takimi podstawami można już szybko ruszyć w dalszą drogę i poznawać możliwości JNI.

Reklamy

23 Lipiec, 2008 - Posted by | cpp, java, scripts | , , , , , ,

4 komentarze »

  1. Bardzo bardzo dobry tutorial. Szczerze mowiac, dla mnie kluczowa byla jedna linijka, ktorej bardzo dlugo nigdzie nie moglam znalezc, byc moze dlatego ze zaczelam poszukiwania od portali anglojezycznych – a mianowicie: -Wl,–add-stdcall-alias.
    Dzieki serdeczne!

    Komentarz - autor: aigilas | 31 Lipiec, 2008 | Odpowiedz

  2. Dzięki – Zapraszam do drugiej części:)

    Komentarz - autor: zenedith | 31 Lipiec, 2008 | Odpowiedz

  3. Witam,

    na szczescie natknalem sie na ten bardzo uzyteczny tutek przed zabawa z JNI i CDT. Dzieki!

    Jedna istotna sprawa: dwie krechy przed slowkien „add”.
    -Wl,–-add-stdcall-alias.

    Tu jest to troche opisane
    http://www.inonit.com/cygwin/jni/helloWorld/c.html

    Pozdrawiam

    Komentarz - autor: korzen | 18 Listopad, 2008 | Odpowiedz

  4. Zgadzam się – jak można było jednak zauważyć, były tam dwa minusy,które jednak wordpress scalił w jeden długi.

    Komentarz - autor: zenedith | 19 Listopad, 2008 | Odpowiedz


Skomentuj

Proszę zalogować się jedną z tych metod aby dodawać swoje komentarze:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s

%d blogerów lubi to: