Propojení Javy s C++

Jako Java programátor se čas od času potýkám s následující situací – mám k dispozici program, který bych chtěl použít v rámci svého vlastního projektu, ale brání mi v tom jedna malá drobnost – tento program je napsaný v C++. Varianty použít pouze jediný programovací jazyk (tj. celé to napsat v Javě nebo C++) jsou mimo diskusi, obě části už jsou dosti rozsáhlé a znamenalo by to začít v novém jazyce od nuly. Přitom jediné co by stačilo, je spuštění jedné nebo dvou funkcí napsaných v C++ programu z mého Javovského kódu.

Java umožňuje pomocí JNI (Java Native Interface) volání nativních metod ze sdílených knihoven. V tomto článku je popsán jednoduchý způsob použití prográmku SWIG (Simplified Wrapper and Interface Generator) pro automatické vygenerování všech potřebných komponent pro vytvoření sdílené knihovny a jejího použití v Javě.

java-logo

SWIG

Předtím, než je možné začít se samotným generováním, je nejprve nutné instalovat samotný SWIG. Pro uživatele linuxových systémů je tato akce jednoduchá – stačí nainstalovat potřebný balíček z repozitářů. Uživatelé systému Windows mají situaci o něco složitější. Na stránkách projektu SWIG je k dispozici ke stažení verze pro windows. Zkušenosti s touto verzí ale nemám a nadále budu popisovat použití v linuxovém prostředí.

Jaký je tedy vlastně účel tohoto prográmku? Tento program umožňuje generování wrapperu pro C++ a interfacových JNI tříd pro Javu. Wrapper je zapotřebí pro vytvoření sdílené knihovny nad kterou je možné volat potřebné funkce. Interfacové třídy pak provádějí samotné volání z kódu Javy. Jediným vstupem pro swig je interfacový soubor s příponou .i, ve kterém jsou definována jména funkcí použitých v rozhraní a jejich vstupní a výstupní parametry. V následujících několika odstavcích bude popsán obecný postup pro vytvoření rozhraní pro projekt z Javy do C++.

Generování wrapperu pomocí programu SWIG

Interfacový soubor na vstupu aplikace swig má svojí definovanou strukturu, v níž lze zadat funkce a proměnné, které budou následně přístupné pomocí JNI rozhraní z Javy. Je třeba v něm jako externí definovat hlavičky funkcí a proměnné, které chceme číst. Takto může vypadat obsah souboru example.i definujícího přístup k jedné proměnné a jedné funkci se dvěma vstupními parametry:

%module example
%{ 

extern int my_variable;
extern int my_function(int x,int y);

%}

extern int my_variable;
extern int my_function(int x,int y);

Může se zdát, že některé řádky jsou v souboru zakomentované, a proto zbytečné. Nenechte se ale zmást, znaky ‚%‘ na začátcích řádků patří do syntaxe interfacového souboru.

Za předpokladu, že už máme úspěšně nainstalovaný SWIG, pak můžeme použít následující příkaz pro vygenerování wrapperu a JNI tříd:

$ swig -java -c++ -package pcg.example -outdir pcg/example example.i

Výstupem tohoto příkazu bude soubor example_wrap.cxx, což je již avizovaný wrapper pro C++ a soubory example.java a exampleJNI.java, což jsou interfacové třídy, které budou použity pro volání z Javy.

SWIG umožňuje propojovat programy v C a C++ s mnoha dalšími programovacími jazyky. Proto je pomocí přepínačů nutné určit jakou variantu propojení se chystáme realizovat. Přepínače -java a -c++ určují, že vytváříme wrapper pro kód z C++ směrem do Javy. Další přepínače jsou určeny pro generované java třídy a určují adresář, kam budou třídy uloženy (-outdir) a javovský balík, který budou mít nadefinován v hlavičce (-package). Tím je možno okamžitě je zařadit do svého projektu v Javě a dále se jimi není třeba zabývat.

Kompilace sdílené knihovny

Prvním krokem pro kompilaci C++ projektu do sdílené knihovny je zkompilování samotných souborů projektu – můj projekt v C++ je v souboru example_code.cpp a k jeho kompliaci použiju kompilátor g++ takto:

$ g++ -c -fPIC example_code.cpp

Použitím přepínače -c jsme dosáhli toho, že jednotlivé kompilované soubory se nelinkují dohromady. Před kompletací projektu musíme totiž ještě přidat soubor wrapperu. Ten je potřeba zkompilovat stejným způsobem:

$ g++ -c -fPIC example_wrap.cxx \
-I/usr/java/default/include \
-I/usr/java/default/include/linux

Přepínač -fPIC v obou případech umožňuje kompilaci kódu nezávislého na pozici (například relativní adresování skoků) a je téměř nutností, je-li je naším cílem vytvoření sdílené knihovny. Pomocí přepínačů -I jsou při kompilaci wrapperu použity javovské knihovny jni.h a jni_md.h. Tím je zajištěna kompatibilita s interfacovými JNI třídami vytvořenými v předchozím bodě. Konkrétní cesta k těmto knihovnám se může lišit v závislosti na systému a verzi nainstalované Javy.

Nyní máme v aktuálním adresáři vytvořen objektový soubor .o pro každý zdrojový soubor projektu a wrapperu. Dalším krokem je tedy vytvoření sdílené knihovny .so z těchto souborů.

 $ g++ -shared *.o -o libexample.so

Všechny názvy sdílených knihoven by měly začínat předponou lib-. Díky ní pak systém rozeznává, že se jedná o sdílenou knihovnu a ulehčuje se tak její použití.

Použití v kódu Javy

Když je vytvořena sdílená knihovna, je třeba jí přesunout do defaultního umístění knihoven pro Javu, nebo spustit náš Java program s následujícím parametrem

-Djava.library.path="/cesta/ke/slozce/kde/lezi/sdilena/knihovna"

tím je zaručeno, že běžící JVM uvidí sdílenou knihovnu. V konkrétní třídě, kde bude sdílená knihovna použita, je pak knihovna načtena příkazem

System.loadLibrary(„example“);

Za povšimnutí stojí, že při načtení knihovny se název píše jak bez přípony .so, tak bez předpony lib.

Nyní už stačí importovat třídu example.java, kterou nám vytvořil program SWIG a kterou jsme zatím nepoužili. Nad touto třídou nyní můžeme volat statické metody, které jsme si na začátku definovali v interfacovém souboru example.i. Tím jsou přímo spouštěny knihovní funkce v nativním kódu C++.

Závěr

Pokud bylo vše provedeno správně, nyní naše Java vesele volá funkce napsané v C++ s minimem námahy a bez ohledu na složitost programu v C++. Doufám, že pro vás byly předchozí řádky přínosné a v případě zájmu či dotazů mne neváhejte kontaktovat na adrese tomas.blovsky@bcvsolutions.eu.