Programmering

Java Tip 17: Integrering af Java med C ++

I denne artikel vil jeg diskutere nogle af de problemer, der er involveret i at integrere C ++ - kode med en Java-applikation. Efter et ord om hvorfor man ønsker at gøre dette, og hvad nogle af forhindringerne er, opbygger jeg et fungerende Java-program, der bruger objekter skrevet i C ++. Undervejs vil jeg diskutere nogle af konsekvenserne af at gøre dette (såsom interaktion med affaldsindsamling), og jeg vil præsentere et glimt af, hvad vi kan forvente i dette område i fremtiden.

Hvorfor integrere C ++ og Java?

Hvorfor ønsker du i første omgang at integrere C ++ - kode i et Java-program? Når alt kommer til alt blev Java-sproget til dels oprettet for at løse nogle af manglerne ved C ++. Der er faktisk flere grunde til, at du måske vil integrere C ++ med Java:

  • Ydeevne. Selvom du udvikler til en platform med en just-in-time (JIT) -kompilator, er oddsene, at den kode, der genereres af JIT-runtime, er betydeligt langsommere end den tilsvarende C ++ -kode. Efterhånden som JIT-teknologien forbedres, bør dette blive mindre af en faktor. (Faktisk kan god JIT-teknologi i den nærmeste fremtid betyde, at Java kører hurtigere end den tilsvarende C ++ - kode.)
  • Til genbrug af ældre kode og integration i ældre systemer.
  • For direkte at få adgang til hardware eller udføre andre aktiviteter på lavt niveau.
  • At udnytte værktøjer, der endnu ikke er tilgængelige for Java (modne OODBMSer, ANTLR osv.).

Hvis du tager springet og beslutter at integrere Java og C ++, opgiver du nogle af de vigtige fordele ved et Java-eneste program. Her er ulemperne:

  • En blandet C ++ / Java-applikation kan ikke køre som en applet.
  • Du opgiver markørens sikkerhed. Din C ++ - kode er fri til at miscaste objekter, få adgang til et slettet objekt eller ødelægge hukommelsen på en af ​​de andre måder, der er så lette i C ++.
  • Din kode er muligvis ikke bærbar.
  • Dit byggede miljø vil bestemt ikke være bærbart - du bliver nødt til at finde ud af, hvordan du placerer C ++ - kode i et delt bibliotek på alle platforme af interesse.
  • API'erne til integration af C og Java er i gang og vil sandsynligvis ændre sig med flytningen fra JDK 1.0.2 til JDK 1.1.

Som du kan se, er integrering af Java og C ++ ikke for svag af hjertet! Men hvis du ønsker at fortsætte, skal du læse videre.

Vi starter med et simpelt eksempel, der viser, hvordan man kalder C ++ - metoder fra Java. Vi udvider derefter dette eksempel for at vise, hvordan man understøtter observatørmønsteret. Observatørmønsteret, ud over at være en af ​​hjørnestenene i objektorienteret programmering, fungerer som et godt eksempel på de mere involverede aspekter ved integration af C ++ og Java-kode. Vi bygger derefter et lille program til at teste vores Java-indpakket C ++ - objekt, og vi slutter med en diskussion af fremtidige retninger til Java.

Opkald til C ++ fra Java

Hvad er så svært ved at integrere Java og C ++, spørger du? Når alt kommer til alt, SunSofts Java-vejledning har et afsnit om "Integrering af indfødte metoder i Java-programmer" (se Ressourcer). Som vi vil se, er dette tilstrækkeligt til at kalde C ++ -metoder fra Java, men det giver os ikke nok til at kalde Java-metoder fra C ++. For at gøre det skal vi gøre lidt mere arbejde.

Som et eksempel tager vi en simpel C ++ - klasse, som vi gerne vil bruge inden for Java. Vi antager, at denne klasse allerede eksisterer, og at vi ikke har lov til at ændre den. Denne klasse kaldes "C ++ :: NumberList" (for klarhedens skyld vil jeg foran alle C ++ -klassenavne have "C ++ ::"). Denne klasse implementerer en simpel liste med numre med metoder til at føje et nummer til listen, forespørge på størrelsen på listen og få et element fra listen. Vi opretter en Java-klasse, hvis opgave det er at repræsentere C ++ -klassen. Denne Java-klasse, som vi kalder NumberListProxy, har de samme tre metoder, men implementeringen af ​​disse metoder vil være at kalde C ++ ækvivalenter. Dette er afbilledet i følgende diagram for objektmodellering (OMT):

En Java-forekomst af NumberListProxy skal holde på en henvisning til den tilsvarende C ++ -forekomst af NumberList. Dette er let nok, hvis det ikke er bærbart: Hvis vi er på en platform med 32-bit-markører, kan vi simpelthen gemme denne markør i en int; hvis vi er på en platform, der bruger 64-bit-pegepinde (eller vi tror, ​​vi måske er i den nærmeste fremtid), kan vi gemme den i lang tid. Den aktuelle kode for NumberListProxy er ligetil, hvis den er noget rodet. Det bruger mekanismerne fra afsnittet "Integrering af indfødte metoder i Java-programmer" i SunSofts Java-vejledning.

Et første snit i Java-klassen ser sådan ud:

 offentlig klasse NumberListProxy {statisk {System.loadLibrary ("NumberList"); } NumberListProxy () {initCppSide (); } offentlig native void addNumber (int n); offentlig indfødt int størrelse (); offentlig indfødt int getNumber (int i); privat indfødt ugyldigt initCppSide (); privat int nummerListPtr_; // Nummerliste *} 

Den statiske sektion køres, når klassen indlæses. System.loadLibrary () indlæser det navngivne delte bibliotek, som i vores tilfælde indeholder den kompilerede version af C ++ :: NumberList. Under Solaris forventer den at finde det delte bibliotek "libNumberList.so" et eller andet sted i $ LD_LIBRARY_PATH. Konventioner om navngivning af delt bibliotek kan variere i andre operativsystemer.

De fleste af metoderne i denne klasse er erklæret som "native". Dette betyder, at vi vil give en C-funktion til at implementere dem. For at skrive C-funktionerne kører vi javah to gange, først som "javah NumberListProxy" og derefter som "javah -stubs NumberListProxy." Dette genererer automatisk noget "lim" -kode, der er nødvendig til Java-runtime (som den placerer i NumberListProxy.c) og genererer erklæringer til de C-funktioner, som vi skal implementere (i NumberListProxy.h).

Jeg valgte at implementere disse funktioner i en fil kaldet NumberListProxyImpl.cc. Det begynder med nogle typiske #include-direktiver:

 // // NumberListProxyImpl.cc // // // Denne fil indeholder C ++ -koden, der implementerer de stubs, der er genereret // af "javah -stubs NumberListProxy". jf. NumberListProxy.c. #include #include "NumberListProxy.h" #include "NumberList.h" 

er en del af JDK og inkluderer en række vigtige systemerklæringer. NumberListProxy.h blev genereret til os af javah og inkluderer erklæringer om de C-funktioner, vi er ved at skrive. NumberList.h indeholder erklæringen fra C ++ klasse NumberList.

I NumberListProxy-konstruktøren kalder vi den oprindelige metode initCppSide (). Denne metode skal finde eller oprette det C ++ - objekt, vi vil repræsentere. I forbindelse med denne artikel vil jeg bare tildele et nyt C ++ - objekt, selvom vi generelt måske i stedet ønsker at linke vores proxy til et C ++ - objekt, der blev oprettet et andet sted. Implementeringen af ​​vores oprindelige metode ser sådan ud:

 ugyldig NumberListProxy_initCppSide (struct HNumberListProxy * javaObj) {NumberList * list = new NumberList (); unhand (javaObj) -> numberListPtr_ = (lang) liste; } 

Som beskrevet i Java-vejledning, vi får et "håndtag" til Java NumberListProxy-objektet. Vores metode opretter et nyt C ++ - objekt, og vedhæfter det derefter til nummerListPtr_-datamedlemet til Java-objektet.

Nu videre til de interessante metoder. Disse metoder genopretter en markør til C ++ -objektet (fra numberListPtr_-datalementet), og påkald derefter den ønskede C ++ -funktion:

 ugyldigt NumberListProxy_addNumber (struct HNumberListProxy * javaObj, long v) {NumberList * list = (NumberList *) unhand (javaObj) -> numberListPtr_; liste-> addNumber (v); } lang NumberListProxy_size (struct HNumberListProxy * javaObj) {NumberList * list = (NumberList *) unhand (javaObj) -> numberListPtr_; returliste-> størrelse (); } lang NumberListProxy_getNumber (struct HNumberListProxy * javaObj, lang i) {NumberList * list = (NumberList *) unhand (javaObj) -> numberListPtr_; returliste-> getNumber (i); } 

Funktionsnavne (NumberListProxy_addNumber og resten) bestemmes for os af javah. For mere information om dette, de typer argumenter, der sendes til funktionen, makroen unhand () og andre detaljer om Java's understøttelse af native C-funktioner, henvises til Java-vejledning.

Mens denne "lim" er lidt kedelig at skrive, er den ret ligetil og fungerer godt. Men hvad sker der, når vi vil kalde Java fra C ++?

Opkald til Java fra C ++

Før du går ned i hvordan at kalde Java-metoder fra C ++, lad mig forklare hvorfor dette kan være nødvendigt. I diagrammet jeg viste tidligere præsenterede jeg ikke hele historien om C ++ klassen. Et mere komplet billede af C ++ klassen vises nedenfor:

Som du kan se, har vi at gøre med en observerbar nummerliste. Denne nummerliste kan ændres fra mange steder (fra NumberListProxy eller fra ethvert C ++ - objekt, der har en henvisning til vores C ++ :: NumberList-objekt). NumberListProxy skal trofast repræsentere alle af C ++ :: NumberList's opførsel; dette bør omfatte underretning af Java-observatører, når nummerlisten ændres. Med andre ord skal NumberListProxy være en underklasse af java.util.Observable, som vist her:

Det er let nok at gøre NumberListProxy til en underklasse af java.util.Observable, men hvordan får den besked? Hvem kalder setChanged () og notifyObservers () når C ++ :: NumberList ændres? For at gøre dette skal vi bruge en hjælperklasse på C ++ - siden. Heldigvis fungerer denne ene hjælperklasse med enhver Java, der kan observeres. Denne hjælperklasse skal være en underklasse af C ++ :: Observer, så den kan registrere sig med C ++ :: NumberList. Når nummerlisten ændres, kaldes vores hjælperklasses opdateringsmetode (). Implementeringen af ​​vores opdateringsmetode () er at kalde setChanged () og notifyObservers () på Java proxy-objektet. Dette er afbilledet i OMT:

Før jeg går ind i implementeringen af ​​C ++ :: JavaObservableProxy, lad mig nævne nogle af de andre ændringer.

NumberListProxy har et nyt datamedlem: javaProxyPtr_. Dette er en markør til forekomsten af ​​C ++ JavaObservableProxy. Vi har brug for dette senere, når vi diskuterer ødelæggelse af objekter. Den eneste anden ændring af vores eksisterende kode er en ændring af vores C-funktion NumberListProxy_initCppSide (). Det ser nu sådan ud:

 ugyldig NumberListProxy_initCppSide (struct HNumberListProxy * javaObj) {NumberList * list = new NumberList (); struct HObservable * observerbar = (struct HObservable *) javaObj; JavaObservableProxy * proxy = ny JavaObservableProxy (observerbar, liste); unhand (javaObj) -> numberListPtr_ = (lang) liste; unhand (javaObj) -> javaProxyPtr_ = (lang) proxy; } 

Bemærk, at vi caster javaObj til en markør til en HObservable. Dette er OK, fordi vi ved, at NumberListProxy er en underklasse af Observable. Den eneste anden ændring er, at vi nu opretter en C ++ :: JavaObservableProxy-forekomst og opretholder en henvisning til den. C ++ :: JavaObservableProxy vil blive skrevet, så den underretter enhver Java Observable, når den registrerer en opdatering, hvorfor vi havde brug for at caste HNumberListProxy * til HObservable *.

I lyset af den hidtidige baggrund kan det synes, at vi bare skal implementere C ++ :: JavaObservableProxy: opdatering () sådan, at den underretter en Java, der kan observeres. Denne løsning virker konceptuelt enkel, men der er en hakke: Hvordan holder vi fast på en henvisning til et Java-objekt fra et C ++ - objekt?

Vedligeholdelse af en Java-reference i et C ++ - objekt

Det kan virke som om vi simpelthen kunne gemme et håndtag til et Java-objekt i et C ++ - objekt. Hvis dette var tilfældet, kunne vi muligvis kode C ++ :: JavaObservableProxy sådan:

 klasse JavaObservableProxy public Observer {public: JavaObservableProxy (struct HObservable * javaObj, Observable * obs) {javaObj_ = javaObj; observeretOne_ = obs; observeretOne _-> addObserver (dette); } ~ JavaObservableProxy () {observeretOne _-> deleteObserver (dette); } ugyldig opdatering () {execute_java_dynamic_method (0, javaObj_, "setChanged", "() V"); } privat: struct HObservable * javaObj_; Observerbar * observeretOne_; }; 

Desværre er løsningen på vores dilemma ikke så enkel. Når Java sender et håndtag til et Java-objekt, forbliver håndtaget] gyldigt i hele opkaldets varighed. Det forbliver ikke nødvendigvis gyldigt, hvis du gemmer det på bunken og prøver at bruge det senere. Hvorfor er det sådan? På grund af Java's affaldssamling.

Først og fremmest forsøger vi at opretholde en henvisning til et Java-objekt, men hvordan ved Java-runtime, at vi opretholder denne reference? Det gør det ikke. Hvis intet Java-objekt har en henvisning til objektet, kan affaldssamleren ødelægge det. I dette tilfælde ville vores C ++ - objekt have en dinglende henvisning til et hukommelsesområde, der tidligere indeholdt et gyldigt Java-objekt, men nu måske indeholder noget helt andet.

Selvom vi er sikre på, at vores Java-objekt ikke samler skrald, kan vi stadig ikke stole på et Java-objekt efter et stykke tid. Affaldssamleren fjerner muligvis ikke Java-objektet, men det kan meget vel flytte det til et andet sted i hukommelsen! Java-specifikationen indeholder ingen garanti mod denne forekomst. Suns JDK 1.0.2 (i det mindste under Solaris) flytter ikke Java-objekter på denne måde, men der er ingen garantier for andre driftstider.

Det, vi virkelig har brug for, er en måde at informere affaldssamleren om, at vi planlægger at opretholde en henvisning til et Java-objekt, og bede om en slags "global reference" til Java-objektet, der garanteret forbliver gyldigt. Desværre har JDK 1.0.2 ingen sådan mekanisme. (En vil sandsynligvis være tilgængelig i JDK 1.1; se slutningen af ​​denne artikel for at få flere oplysninger om fremtidige retninger.) Mens vi venter, kan vi slå os rundt om dette problem.