Brug af JNA til at få adgang til indfødte dynamiske biblioteker

1. Oversigt

I denne vejledning vil vi se, hvordan du bruger Java Native Access-biblioteket (forkortet JNA) til at få adgang til indfødte biblioteker uden at skrive nogen JNI-kode (Java Native Interface).

2. Hvorfor JNA?

I mange år har Java og andre JVM-baserede sprog i høj grad opfyldt sit motto "skriv en gang, kør overalt". Imidlertid er vi undertiden nødt til at bruge native kode til at implementere nogle funktioner:

  • Genbrug af ældre kode skrevet i C / C ++ eller ethvert andet sprog, der er i stand til at oprette oprindelig kode
  • Adgang til systemspecifik funktionalitet er ikke tilgængelig i den almindelige Java-runtime
  • Optimering af hastighed og / eller hukommelsesforbrug til bestemte sektioner af en given applikation.

Oprindeligt betød denne form for krav, at vi skulle bruge JNI - Java Native Interface. Selvom den er effektiv, har denne tilgang sine ulemper og blev generelt undgået på grund af et par problemer:

  • Kræver udviklere at skrive C / C ++ “limkode” for at bygge bro mellem Java og native kode
  • Kræver en komplet kompilering og linkværktøjskæde tilgængelig for hvert målsystem
  • Marshaling og unmarshalling af værdier til og fra JVM er en kedelig og fejlbehæftet opgave
  • Juridiske og supportproblemer ved blanding af Java og indfødte biblioteker

JNA kom til at løse det meste af kompleksiteten forbundet med brug af JNI. Især er der ikke behov for at oprette nogen JNI-kode for at bruge native-kode, der er placeret i dynamiske biblioteker, hvilket gør hele processen meget lettere.

Selvfølgelig er der nogle kompromiser:

  • Vi kan ikke direkte bruge statiske biblioteker
  • Langsommere sammenlignet med håndlavet JNI-kode

For de fleste applikationer opvejer JNAs enkelhedsfordele imidlertid langt de ulemper. Som sådan er det rimeligt at sige, at medmindre vi har meget specifikke krav, er JNA i dag sandsynligvis det bedste tilgængelige valg for at få adgang til native-kode fra Java - eller ethvert andet JVM-baseret sprog, forresten.

3. JNA-projektopsætning

Den første ting, vi skal gøre for at bruge JNA, er at tilføje dens afhængigheder til vores projekts pom.xml:

 net.java.dev.jna jna-platform 5.6.0 

Den seneste version af jna-platform kan downloades fra Maven Central.

4. Brug af JNA

Brug af JNA er en totrins proces:

  • Først opretter vi en Java-grænseflade, der udvider JNA'er Bibliotek interface til at beskrive de metoder og typer, der bruges, når der kaldes til den oprindelige målkode
  • Dernæst sender vi denne grænseflade til JNA, som returnerer en konkret implementering af denne grænseflade, som vi bruger til at påberåbe sig indfødte metoder

4.1. Opkaldsmetoder fra C Standard-biblioteket

For vores første eksempel, lad os bruge JNA til at kalde koselig funktion fra standard C-biblioteket, som er tilgængeligt i de fleste systemer. Denne metode tager en dobbelt argument og beregner dets hyperbolske cosinus. AC-program kan bruge denne funktion bare ved at inkludere header-fil:

#include #include int main (int argc, char ** argv) {double v = cosh (0.0); printf ("Resultat:% f \ n", v); }

Lad os oprette den Java-grænseflade, der er nødvendig for at kalde denne metode:

offentlig grænseflade CMath udvider biblioteket {dobbelt cosh (dobbelt værdi); } 

Dernæst bruger vi JNA'er Hjemmehørende klasse for at skabe en konkret implementering af denne grænseflade, så vi kan kalde vores API:

CMath lib = Native.load (Platform.isWindows ()? "Msvcrt": "c", CMath.class); dobbelt resultat = lib.cosh (0); 

Den virkelig interessante del her er opfordringen til belastning() metode. Det kræver to argumenter: det dynamiske biblioteksnavn og en Java-grænseflade, der beskriver de metoder, vi bruger. Det returnerer en konkret implementering af denne grænseflade, så vi kan kalde en hvilken som helst af dens metoder.

Nu er dynamiske biblioteksnavne normalt systemafhængige, og C-standardbibliotek er ingen undtagelse: libc.so i de fleste Linux-baserede systemer, men msvcrt.dll i Windows. Dette er grunden til, at vi har brugt Platform hjælperklasse, inkluderet i JNA, for at kontrollere, hvilken platform vi kører på, og vælg det korrekte biblioteksnavn.

Bemærk, at vi ikke behøver at tilføje .så eller .dll udvidelse, som de er underforstået. For Linux-baserede systemer behøver vi heller ikke angive det “lib” -præfiks, der er standard for delte biblioteker.

Da dynamiske biblioteker opfører sig som Singletons fra et Java-perspektiv, er en almindelig praksis at erklære en INSTANS felt som en del af interface-erklæringen:

offentlig grænseflade CMath udvider biblioteket {CMath INSTANCE = Native.load (Platform.isWindows ()? "msvcrt": "c", CMath.class); dobbelt cosh (dobbelt værdi); } 

4.2. Grundlæggende typer kortlægning

I vores oprindelige eksempel brugte den kaldte metode kun primitive typer som både argument og returværdi. JNA håndterer disse sager automatisk, normalt ved hjælp af deres naturlige Java-modstykker, når de kortlægges fra C-typer:

  • char => byte
  • kort => kort
  • wchar_t => char
  • int => int
  • lang => com.sun.jna.NativeLong
  • lang lang => lang
  • flyde => flyde
  • dobbelt => dobbelt
  • char * => String

En kortlægning, der kan se mærkelig ud, er den, der bruges til den indfødte lang type. Dette skyldes, at i C / C ++ lang type kan repræsentere en 32- eller 64-bit værdi, afhængigt af om vi kører på et 32- eller 64-bit system.

For at løse dette problem leverer JNA NativeLong type, der bruger den rigtige type afhængigt af systemets arkitektur.

4.3. Strukturer og fagforeninger

Et andet almindeligt scenario handler om native-kode-API'er, der forventer en markør til nogle struct eller Union type. Når du opretter Java-grænsefladen for at få adgang til den, skal det tilsvarende argument eller returværdien være en Java-type, der udvides Struktur eller Union, henholdsvis.

For eksempel givet denne C-struktur:

struct foo_t {int felt1; int felt2; char * felt3; };

Dens Java-peer-klasse ville være:

@FieldOrder ({"field1", "field2", "field3"}) offentlig klasse FooType udvider struktur {int field1; int felt2; Strengfelt3; };

JNA kræver @FeltOrder kommentar, så den korrekt kan serieisere data i en hukommelsesbuffer, inden den bruges som et argument til målmetoden.

Alternativt kan vi tilsidesætte getFieldOrder () metode til samme effekt. Når man målretter mod en enkelt arkitektur / platform, er den tidligere metode generelt god nok. Vi kan bruge sidstnævnte til at håndtere tilpasningsproblemer på tværs af platforme, der nogle gange kræver tilføjelse af nogle ekstra polstringsfelter.

Fagforeninger arbejde på samme måde, bortset fra nogle få punkter:

  • Ingen grund til at bruge en @FeltOrder kommentar eller implementering getFieldOrder ()
  • Vi er nødt til at ringe setType () inden du kalder den oprindelige metode

Lad os se, hvordan man gør det med et simpelt eksempel:

offentlig klasse MyUnion udvider Union {public String foo; offentlig dobbeltbjælke; }; 

Lad os nu bruge det MyUnion med et hypotetisk bibliotek:

MyUnion u = ny MyUnion (); u.foo = "test"; u.setType (String.class); lib.some_method (u); 

Hvis begge dele foo og bar hvor af samme type, skulle vi i stedet bruge feltets navn:

u.foo = "test"; u.setType ("foo"); lib.some_method (u);

4.4. Brug af markører

JNA tilbyder en Markør abstraktion, der hjælper med at håndtere API'er, der er erklæret med utypet markør - typisk en ugyldig *. Denne klasse tilbyder metoder, der giver læse- og skriveadgang til den underliggende native-hukommelsesbuffer, hvilket har åbenlyse risici.

Før vi begynder at bruge denne klasse, skal vi være sikre på, at vi klart forstår, hvem der "ejer" den refererede hukommelse hver gang. Hvis du ikke gør det, vil det sandsynligvis medføre fejl at fejle fejl relateret til hukommelseslækage og / eller ugyldig adgang.

Forudsat at vi ved hvad vi laver (som altid), lad os se, hvordan vi kan bruge det velkendte malloc () og ledig() fungerer med JNA, der bruges til at allokere og frigive en hukommelsesbuffer. Lad os først oprette vores wrapper-interface:

offentlig grænseflade StdC udvider biblioteket {StdC INSTANCE = // ... oprettelse af instans udeladt Pointer malloc (lang n); ugyldig (Pointer p); } 

Lad os nu bruge den til at tildele en buffer og lege med den:

StdC lib = StdC.INSTANCE; Markør p = lib.malloc (1024); p.setMemory (0l, 1024l, (byte) 0); lib.free (p); 

Det setMemory () metode udfylder bare den underliggende buffer med en konstant byteværdi (nul, i dette tilfælde). Bemærk, at Markør eksempel har ingen anelse om, hvad det peger på, meget mindre dets størrelse. Dette betyder, at vi ganske let kan ødelægge vores bunke ved hjælp af dens metoder.

Vi ser senere, hvordan vi kan afbøde sådanne fejl ved hjælp af JNAs funktion til nedbrudssikring.

4.5. Håndtering af fejl

Gamle versioner af standard C-biblioteket brugte det globale errno variabel for at gemme årsagen til, at et bestemt opkald mislykkedes. For eksempel er det sådan en typisk åben() opkald ville bruge denne globale variabel i C:

int fd = åben ("noget sti", O_RDONLY); hvis (fd <0) {printf ("Åben mislykkedes: errno =% d \ n", errno); udgang (1); }

Selvfølgelig fungerer denne kode i moderne multitrådede programmer ikke, ikke? Takket være C's forprocessor kan udviklere stadig skrive kode som denne, og den fungerer fint. Det viser sig, at i dag, errno er en makro, der udvides til et funktionsopkald:

// ... uddrag fra bits / errno.h på Linux #definer errno (* __ errno_location ()) // ... uddrag fra Visual Studio #define errno (* _errno ())

Nu fungerer denne tilgang fint, når du kompilerer kildekode, men der er ikke sådan noget, når du bruger JNA. Vi kunne erklære den udvidede funktion i vores wrapper-interface og kalde den eksplicit, men JNA tilbyder et bedre alternativ: LastErrorException.

Enhver metode, der er angivet i indpakningsgrænseflader med kaster LastErrorException vil automatisk inkludere en kontrol af en fejl efter et oprindeligt opkald. Hvis det rapporterer en fejl, vil JNA kaste et LastErrorException, som inkluderer den originale fejlkode.

Lad os tilføje et par metoder til StdC indpakningsgrænseflade, vi har brugt før til at vise denne funktion i aktion:

offentlig grænseflade StdC udvider biblioteket {// ... andre metoder udeladt int åben (streng sti, int flag) kaster LastErrorException; int tæt (int fd) kaster LastErrorException; } 

Nu kan vi bruge åben() i en prøve / fangst-klausul:

StdC lib = StdC.INSTANCE; int fd = 0; prøv {fd = lib.open ("/ some / path", 0); // ... brug fd} catch (LastErrorException err) {// ... fejlhåndtering} endelig {if (fd> 0) {lib.close (fd); }} 

I fangst blokere, vi kan bruge LastErrorException.getErrorCode () for at få originalen errno værdi og brug den som en del af fejlhåndteringslogikken.

4.6. Håndtering af adgangsbrud

Som nævnt tidligere beskytter JNA os ikke mod misbrug af en given API, især når det drejer sig om hukommelsesbuffere, der er sendt frem og tilbage native kode. I normale situationer resulterer sådanne fejl i en overtrædelse af adgangen og afslutter JVM.

JNA understøtter til en vis grad en metode, der gør det muligt for Java-kode at håndtere adgangsfejl. Der er to måder at aktivere det på:

  • Indstilling af jna.beskyttet systemegenskab til rigtigt
  • Ringer Native.setProtected (true)

Når vi har aktiveret denne beskyttede tilstand, vil JNA fange adgangsfejl på overtrædelse, der normalt ville resultere i et nedbrud og kaste et java.lang.Error undtagelse. Vi kan kontrollere, at dette fungerer ved hjælp af en Markør initialiseret med en ugyldig adresse og forsøger at skrive nogle data til den:

Native.setProtected (true); Markør p = ny markør (0l); prøv {p.setMemory (0, 100 * 1024, (byte) 0); } catch (Error err) {// ... fejlhåndtering udeladt} 

Som dokumentationen angiver, skal denne funktion imidlertid kun bruges til fejlfinding / udviklingsformål.

5. Konklusion

I denne artikel har vi vist, hvordan man nemt bruger JNA til at få adgang til indfødt kode sammenlignet med JNI.

Som sædvanlig er al kode tilgængelig på GitHub.