Programování

Tip pro Javu 17: Integrace Javy s C ++

V tomto článku budu diskutovat o některých problémech spojených s integrací kódu C ++ s aplikací Java. Po slovu o tom, proč by to člověk chtěl dělat a jaké jsou překážky, vytvořím funkční program Java, který používá objekty napsané v C ++. Po cestě budu diskutovat o některých důsledcích toho (například interakci s odvozem odpadu) a představím letmý pohled na to, co můžeme v této oblasti v budoucnu očekávat.

Proč integrovat C ++ a Javu?

Proč byste vůbec chtěli integrovat kód C ++ do programu Java? Koneckonců, jazyk Java byl zčásti vytvořen, aby vyřešil některé nedostatky C ++. Ve skutečnosti existuje několik důvodů, proč budete chtít integrovat C ++ s Javou:

  • Výkon. I když vyvíjíte pro platformu s kompilátorem just-in-time (JIT), je pravděpodobné, že kód generovaný modulem runtime JIT je výrazně pomalejší než ekvivalentní kód C ++. Jak se technologie JIT zlepšuje, mělo by se to stát méně faktorem. (Ve skutečnosti v blízké budoucnosti může dobrá technologie JIT dobře znamenat, že Java běží rychlejší než ekvivalentní C ++ kód.)
  • Pro opětovné použití původního kódu a integraci do starších systémů.
  • Přímý přístup k hardwaru nebo jiné činnosti na nízké úrovni.
  • Využití nástrojů, které pro Javu dosud nejsou k dispozici (starší OODBMSes, ANTLR atd.).

Pokud se rozhodnete pro integraci Java a C ++, vzdáte se některých důležitých výhod aplikace pouze pro Java. Zde jsou nevýhody:

  • Kombinovaná aplikace C ++ / Java nemůže běžet jako applet.
  • Vzdáváte se bezpečnosti ukazatele. Váš kód C ++ může chybně vložit objekty, přistupovat k odstraněnému objektu nebo poškodit paměť jakýmkoli dalším způsobem, který je v C ++ tak snadný.
  • Váš kód nemusí být přenosný.
  • Vaše vytvořené prostředí rozhodně nebude přenosné - budete muset přijít na to, jak umístit kód C ++ do sdílené knihovny na všech platformách, které vás zajímají.
  • Rozhraní API pro integraci jazyka C a Javy probíhají a velmi pravděpodobně se změní s přechodem z JDK 1.0.2 na JDK 1.1.

Jak vidíte, integrace Java a C ++ není pro slabé povahy! Pokud však chcete pokračovat, čtěte dále.

Začneme jednoduchým příkladem, který ukazuje, jak volat metody C ++ z Javy. Poté tento příklad rozšíříme, abychom ukázali, jak podporovat vzor pozorovatele. Vzor pozorovatele, kromě toho, že je jedním ze základních kamenů objektově orientovaného programování, slouží jako pěkný příklad více zapojených aspektů integrace kódu C ++ a Java. Poté sestavíme malý program, který otestuje náš objekt C ++ zabalený v Javě, a skončíme diskusí o budoucích směrech pro Javu.

Volání C ++ z Javy

Co je tak těžké na integraci Java a C ++, ptáte se? Koneckonců, SunSoft Výukový program Java obsahuje část „Integrace nativních metod do programů Java“ (viz Zdroje). Jak uvidíme, je to adekvátní pro volání metod C ++ z Javy, ale nedává nám to dostatek k volání metod Java z C ++. K tomu budeme muset udělat trochu více práce.

Jako příklad si vezmeme jednoduchou třídu C ++, kterou bychom chtěli použít z prostředí Java. Budeme předpokládat, že tato třída již existuje a že ji nesmíme měnit. Tato třída se nazývá „C ++ :: NumberList“ (pro přehlednost předponu všech názvů tříd C ++ vložím „C ++ ::“). Tato třída implementuje jednoduchý seznam čísel, s metodami přidání čísla do seznamu, dotazování na velikost seznamu a získání prvku ze seznamu. Vytvoříme třídu Java, jejímž úkolem je reprezentovat třídu C ++. Tato třída Java, kterou budeme nazývat NumberListProxy, bude mít stejné tři metody, ale implementací těchto metod bude volání ekvivalentů C ++. To je zobrazeno v následujícím diagramu techniky modelování objektů (OMT):

Instance Java NumberListProxy musí držet odkaz na odpovídající instanci C ++ NumberList. To je dost snadné, pokud je lehce nepřenosné: Pokud jsme na platformě s 32bitovými ukazateli, můžeme tento ukazatel jednoduše uložit do int; pokud jsme na platformě, která používá 64bitové ukazatele (nebo si myslíme, že bychom mohli být v blízké budoucnosti), můžeme ji uložit za dlouhou dobu. Skutečný kód pro NumberListProxy je přímočarý, i když poněkud chaotický. Využívá mechanismy z části „Integrace nativních metod do programů Java“ příručky Java SunSoft.

První řez ve třídě Java vypadá takto:

 veřejná třída NumberListProxy {static {System.loadLibrary ("NumberList"); } NumberListProxy () {initCppSide (); } public native void addNumber (int n); public native int size (); public native int getNumber (int i); private native void initCppSide (); soukromé int čísloListPtr_; // NumberList *} 

Statická část se spustí, když je třída načtena. System.loadLibrary () načte pojmenovanou sdílenou knihovnu, která v našem případě obsahuje kompilovanou verzi C ++ :: NumberList. V systému Solaris se očekává, že najde sdílenou knihovnu „libNumberList.so“ někde v $ LD_LIBRARY_PATH. Konvence pojmenování sdílené knihovny se mohou v jiných operačních systémech lišit.

Většina metod v této třídě je deklarována jako „nativní“. To znamená, že k jejich implementaci poskytneme funkci C. Abychom mohli psát funkce C, spustíme javah dvakrát, nejprve jako „javah NumberListProxy“, poté jako „javah -stubs NumberListProxy“. Tím se automaticky vygeneruje nějaký „lepicí“ kód potřebný pro běh prostředí Java (který vloží do NumberListProxy.c) a vygeneruje se deklarace pro funkce C, které máme implementovat (v NumberListProxy.h).

Rozhodl jsem se implementovat tyto funkce do souboru s názvem NumberListProxyImpl.cc. Začíná to několika typickými směrnicemi #include:

 // // NumberListProxyImpl.cc // // // // Tento soubor obsahuje kód C ++, který implementuje útržky generované // pomocí „javah -stubs NumberListProxy“. srov. NumberListProxy.c. #include #include "NumberListProxy.h" #include "NumberList.h" 

je součástí JDK a obsahuje řadu důležitých systémových prohlášení. NumberListProxy.h pro nás vygeneroval javah a obsahuje deklarace funkcí C, které se chystáme napsat. NumberList.h obsahuje deklaraci třídy Number ++ třídy C ++.

V konstruktoru NumberListProxy voláme nativní metodu initCppSide (). Tato metoda musí najít nebo vytvořit objekt C ++, který chceme reprezentovat. Pro účely tohoto článku pouze přidělám haldu nový objekt C ++, ačkoli obecně bychom místo toho mohli chtít propojit náš proxy s objektem C ++, který byl vytvořen jinde. Implementace naší nativní metody vypadá takto:

 void NumberListProxy_initCppSide (struct HNumberListProxy * javaObj) {NumberList * list = new NumberList (); unfand (javaObj) -> numberListPtr_ = (dlouhý) seznam; } 

Jak je popsáno v Výukový program Java, předali jsme „popisovač“ objektu Java NumberListProxy. Naše metoda vytvoří nový objekt C ++ a poté jej připojí k datovému členu numberListPtr_ objektu Java.

Nyní k zajímavým metodám. Tyto metody obnoví ukazatel na objekt C ++ (z datového člena numberListPtr_), poté vyvolají požadovanou funkci C ++:

 void NumberListProxy_addNumber (struktura HNumberListProxy * javaObj, dlouhé v) {NumberList * list = (NumberList *) unfand (javaObj) -> numberListPtr_; list-> addNumber (v); } dlouhá NumberListProxy_size (struktura HNumberListProxy * javaObj) {NumberList * list = (NumberList *) unfand (javaObj) -> numberListPtr_; návratový seznam-> size (); } dlouhý NumberListProxy_getNumber (struktura HNumberListProxy * javaObj, dlouhý i) {NumberList * list = (NumberList *) nezpracovaný (javaObj) -> numberListPtr_; návratový seznam-> getNumber (i); } 

Názvy funkcí (NumberListProxy_addNumber a zbytek) pro nás určuje javah. Další informace o tomto, typy argumentů odeslaných funkci, makro unfand () a další podrobnosti podpory jazyka Java pro nativní funkce C najdete v dokumentu Výukový program Java.

I když je toto „lepidlo“ při psaní poněkud zdlouhavé, je poměrně jednoduché a funguje dobře. Co se ale stane, když chceme volat Javu z C ++?

Volání Javy z C ++

Než se ponoříme do jak volat metody Java z C ++, dovolte mi to vysvětlit proč to může být nutné. V diagramu, který jsem ukázal dříve, jsem nepředložil celý příběh třídy C ++. Kompletnější obrázek třídy C ++ je uveden níže:

Jak vidíte, máme co do činění s pozorovatelným číselným seznamem. Tento číselný seznam může být upraven z mnoha míst (z NumberListProxy nebo z libovolného objektu C ++, který má odkaz na náš objekt C ++ :: NumberList). NumberListProxy má věrně reprezentovat Všechno chování C ++ :: NumberList; to by mělo zahrnovat upozornění Java pozorovatelů, když se seznam čísel změní. Jinými slovy, NumberListProxy musí být podtřídou java.util.Observable, jak je znázorněno zde:

Je snadné udělat z NumberListProxy podtřídu java.util.Observable, ale jak to dostává oznámení? Kdo zavolá setChanged () a notifyObservers () při změně C ++ :: NumberList? K tomu budeme potřebovat pomocnou třídu na straně C ++. Naštěstí bude tato pomocná třída fungovat s jakoukoli pozorovatelnou Javou. Tato pomocná třída musí být podtřídou C ++ :: Observer, aby se mohla zaregistrovat pomocí C ++ :: NumberList. Když se seznam čísel změní, zavolá se metoda naší třídy pomocníka 'update (). Implementací naší metody update () bude volání setChanged () a notifyObservers () na objekt proxy Java. Toto je zobrazeno v OMT:

Než se pustíme do implementace C ++ :: JavaObservableProxy, dovolte mi zmínit některé další změny.

NumberListProxy má nového datového člena: javaProxyPtr_. Toto je ukazatel na instanci C ++ JavaObservableProxy. To budeme potřebovat později, až budeme diskutovat o zničení objektu. Jedinou další změnou našeho stávajícího kódu je změna naší funkce C NumberListProxy_initCppSide (). Nyní to vypadá takto:

 void NumberListProxy_initCppSide (struct HNumberListProxy * javaObj) {NumberList * list = new NumberList (); struct HObservable * pozorovatelný = (struct HObservable *) javaObj; JavaObservableProxy * proxy = nová JavaObservableProxy (pozorovatelná, seznam); unfand (javaObj) -> numberListPtr_ = (dlouhý) seznam; unfand (javaObj) -> javaProxyPtr_ = (dlouhý) proxy; } 

Všimněte si, že vrháme javaObj na ukazatel na HObservable. To je v pořádku, protože víme, že NumberListProxy je podtřídou Observable. Jedinou další změnou je, že nyní vytvoříme instanci C ++ :: JavaObservableProxy a udržujeme na ni odkaz. C ++ :: JavaObservableProxy bude napsáno tak, aby upozornilo všechny pozorovatelné Java, když zjistí aktualizaci, což je důvod, proč jsme potřebovali vložit HNumberListProxy * do HObservable *.

Vzhledem k dosavadnímu pozadí se může zdát, že stačí implementovat C ++ :: JavaObservableProxy: update () tak, aby oznamoval pozorovatelnou Javu. Toto řešení se jeví koncepčně jednoduché, ale je tu zádrhel: Jak se můžeme držet odkazu na objekt Java zevnitř objektu C ++?

Údržba reference Java v objektu C ++

Mohlo by se zdát, že bychom mohli jednoduše uložit popisovač objektu Java v objektu C ++. Pokud by tomu tak bylo, mohli bychom kódovat C ++ :: JavaObservableProxy takto:

 třída JavaObservableProxy public Observer {public: JavaObservableProxy (struct HObservable * javaObj, Observable * obs) {javaObj_ = javaObj; ObservOne_ = obs; ObservOne _-> addObserver (this); } ~ JavaObservableProxy () {ObservOne _-> deleteObserver (this); } void update () {execute_java_dynamic_method (0, javaObj_, "setChanged", "() V"); } soukromé: struct HObservable * javaObj_; Pozorovatelné * ObservedOne_; }; 

Řešení našeho dilematu bohužel není tak jednoduché. Když vám Java předá popisovač objektu Java, popisovač] zůstane platný po dobu trvání hovoru. Pokud jej uložíte na hromadu a pokusíte se jej použít později, nemusí nutně zůstat v platnosti. Proč je to tak? Kvůli sběru odpadu v Javě.

Nejprve se snažíme udržovat odkaz na objekt Java, ale jak runtime Java ví, že tento odkaz udržujeme? Není. Pokud žádný objekt Java nemá odkaz na objekt, může jej odstranit sběrač odpadků. V tomto případě by náš objekt C ++ měl visící odkaz na oblast paměti, která dříve obsahovala platný objekt Java, ale nyní může obsahovat něco zcela jiného.

I když jsme si jistí, že náš objekt Java nebude shromažďovat odpadky, nemůžeme po nějaké době důvěřovat popisovači objektu Java. Sběratel odpadků nemusí objekt Java odstranit, ale mohl by jej velmi dobře přesunout na jiné místo v paměti! Specifikace Java neobsahuje žádnou záruku proti tomuto výskytu. Sun JDK 1.0.2 (alespoň v systému Solaris) nebude přesouvat objekty Java tímto způsobem, ale neexistují žádné záruky pro ostatní runtime.

To, co opravdu potřebujeme, je způsob informování sběrače odpadků, že plánujeme udržovat odkaz na objekt Java, a požádat o nějaký „globální odkaz“ na objekt Java, který zaručeně zůstane platný. Je smutné, že JDK 1.0.2 takový mechanismus nemá. (Jeden bude pravděpodobně k dispozici v JDK 1.1; další informace o budoucích směrech najdete na konci tohoto článku.) Zatímco čekáme, můžeme tento problém vyřešit.