Bruke JNA for å få tilgang til innfødte dynamiske biblioteker

1. Oversikt

I denne veiledningen vil vi se hvordan du bruker Java Native Access-biblioteket (forkortet JNA) for å få tilgang til innfødte biblioteker uten å skrive noen JNI-kode (Java Native Interface).

2. Hvorfor JNA?

I mange år har Java og andre JVM-baserte språk i stor grad oppfylt sitt motto "skriv en gang, løp overalt". Noen ganger må vi imidlertid bruke innfødt kode for å implementere noe funksjonalitet:

  • Gjenbruk av eldre koder skrevet i C / C ++ eller et hvilket som helst annet språk som kan lage opprinnelig kode
  • Å få tilgang til systemspesifikk funksjonalitet som ikke er tilgjengelig i standard Java-kjøretid
  • Optimalisering av hastighet og / eller minnebruk for bestemte deler av et gitt program.

Opprinnelig betydde denne typen krav at vi måtte ty til JNI - Java Native Interface. Selv om denne tilnærmingen er effektiv, har den sine ulemper og ble generelt unngått på grunn av noen få problemer:

  • Krever utviklere å skrive C / C ++ "limkode" for å bygge bro over Java og innfødt kode
  • Krever en komplett kompilerings- og lenkeverktøykjede tilgjengelig for hvert målsystem
  • Marshaling og unmarshalling verdier til og fra JVM er en kjedelig og feilutsatt oppgave
  • Juridiske og støttehensyn når du blander Java og innfødte biblioteker

JNA kom til å løse det meste av kompleksiteten knyttet til bruk av JNI. Spesielt er det ikke nødvendig å lage noen JNI-kode for å bruke innfødt kode i dynamiske biblioteker, noe som gjør hele prosessen mye enklere.

Selvfølgelig er det noen kompromisser:

  • Vi kan ikke bruke statiske biblioteker direkte
  • Tregere sammenlignet med håndlaget JNI-kode

For de fleste applikasjoner oppveier JNAs enkelhetsfordeler langt de ulempene. Som sådan er det rimelig å si at, med mindre vi har veldig spesifikke krav, er JNA i dag sannsynligvis det beste tilgjengelige valget for å få tilgang til innfødt kode fra Java - eller et hvilket som helst annet JVM-basert språk, forresten.

3. JNA Prosjektoppsett

Det første vi må gjøre for å bruke JNA er å legge til avhengighet til prosjektets pom.xml:

 net.java.dev.jna jna-plattform 5.6.0 

Den siste versjonen av jna-plattform kan lastes ned fra Maven Central.

4. Bruke JNA

Å bruke JNA er en totrinnsprosess:

  • Først oppretter vi et Java-grensesnitt som utvider JNA Bibliotek grensesnitt for å beskrive metodene og typene som brukes når du ringer til den opprinnelige målkoden
  • Deretter sender vi dette grensesnittet til JNA som returnerer en konkret implementering av dette grensesnittet som vi bruker for å påkalle innfødte metoder

4.1. Anropsmetoder fra C Standard-biblioteket

For vårt første eksempel, la oss bruke JNA til å ringe koselig funksjon fra standard C-biblioteket, som er tilgjengelig i de fleste systemer. Denne metoden tar en dobbelt argument og beregner sin hyperbolske cosinus. AC-programmet kan bruke denne funksjonen bare ved å inkludere overskriftsfil:

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

La oss lage Java-grensesnittet som trengs for å kalle denne metoden:

offentlig grensesnitt CMath utvider biblioteket {double cosh (dobbel verdi); } 

Deretter bruker vi JNA-er Innfødt klasse for å lage en konkret implementering av dette grensesnittet slik at vi kan ringe API-en vår:

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

Den virkelig interessante delen her er samtalen til laste() metode. Det tar to argumenter: det dynamiske biblioteksnavnet og et Java-grensesnitt som beskriver metodene vi skal bruke. Det returnerer en konkret implementering av dette grensesnittet, slik at vi kan kalle noen av metodene.

Nå er dynamiske biblioteksnavn vanligvis systemavhengige, og C-standardbibliotek er ikke noe unntak: libc.so i de fleste Linux-baserte systemer, men msvcrt.dll i Windows. Dette er grunnen til at vi har brukt Plattform hjelperklasse, inkludert i JNA, for å sjekke hvilken plattform vi kjører i og velge riktig biblioteksnavn.

Legg merke til at vi ikke trenger å legge til .så eller .dll utvidelse, slik de er underforstått. Også for Linux-baserte systemer trenger vi ikke å spesifisere "lib" -prefikset som er standard for delte biblioteker.

Siden dynamiske biblioteker oppfører seg som Singletons fra et Java-perspektiv, er en vanlig praksis å erklære et FOREKOMST felt som en del av grensesnitterklæringen:

offentlig grensesnitt CMath utvider biblioteket {CMath INSTANCE = Native.load (Platform.isWindows ()? "msvcrt": "c", CMath.class); dobbel cosh (dobbel verdi); } 

4.2. Grunnleggende typer kartlegging

I vårt første eksempel brukte den kalte metoden bare primitive typer som både argument og returverdi. JNA håndterer disse sakene automatisk, vanligvis ved hjelp av deres naturlige Java-kolleger når de kartlegges fra C-typer:

  • char => byte
  • kort => kort
  • wchar_t => røye
  • int => int
  • lang => com.sun.jna.NativeLong
  • lang lang => lang
  • flyte => flyte
  • dobbelt => dobbelt
  • char * => String

En kartlegging som kan se rart ut, er den som brukes for innfødte lang type. Dette er fordi, i C / C ++, lang typen kan representere en 32- eller 64-biters verdi, avhengig av om vi kjører på et 32- eller 64-biters system.

For å løse dette problemet tilbyr JNA NativeLong type, som bruker riktig type avhengig av systemets arkitektur.

4.3. Strukturer og fagforeninger

Et annet vanlig scenario er å håndtere native code APIer som forventer en peker til noen struct eller fagforening type. Når du oppretter Java-grensesnittet for å få tilgang til det, må det tilsvarende argumentet eller returverdien være en Java-type som utvides Struktur eller Union, henholdsvis.

For eksempel gitt denne C-strukturen:

struct foo_t {int felt1; int felt2; røye * felt3; };

Java-jevnaldrende klassen vil være:

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

JNA krever @FeltOrder kommentar slik at den kan ordentlig serieisere data til en minnebuffer før den brukes som et argument for målmetoden.

Alternativt kan vi overstyre getFieldOrder () metode for samme effekt. Når du målretter mot en enkelt arkitektur / plattform, er den tidligere metoden generelt god nok. Vi kan bruke sistnevnte til å håndtere justeringsproblemer på tvers av plattformer, som noen ganger krever å legge til noen ekstra polstringsfelt.

Fagforeninger fungerer på samme måte, bortsett fra noen få poeng:

  • Ingen grunn til å bruke en @FeltOrder kommentar eller implementering getFieldOrder ()
  • Vi må ringe setType () før du kaller den opprinnelige metoden

La oss se hvordan du gjør det med et enkelt eksempel:

offentlig klasse MyUnion utvider Union {public String foo; offentlig dobbel bar; }; 

La oss nå bruke det MyUnion med et hypotetisk bibliotek:

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

Hvis begge deler foo og bar hvor av samme type, må vi bruke feltnavnet i stedet:

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

4.4. Bruke pekere

JNA tilbyr en Peker abstraksjon som hjelper til med å håndtere API-er erklært med utypet peker - vanligvis en tomrom *. Denne klassen tilbyr metoder som tillater lese- og skrivetilgang til den underliggende innebygde minnebufferen, som har åpenbare risikoer.

Før vi begynner å bruke denne klassen, må vi være sikre på at vi tydelig forstår hvem som "eier" det refererte minnet til hver gang. Unnlatelse av å gjøre det vil sannsynligvis gi vanskelige feilsøkingsfeil relatert til minnelekkasjer og / eller ugyldige tilganger.

Forutsatt at vi vet hva vi gjør (som alltid), la oss se hvordan vi kan bruke det velkjente malloc () og gratis() funksjoner med JNA, brukes til å tildele og frigjøre en minnebuffer. Først, la oss igjen opprette vårt grensesnittgrensesnitt:

offentlig grensesnitt StdC utvider biblioteket {StdC INSTANCE = // ... forekomstoppretting utelatt Pointer malloc (lang n); tomrom (peker p); } 

La oss nå bruke den til å tildele en buffer og spille med den:

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

De setMemory () metoden fyller bare den underliggende bufferen med en konstant byteverdi (null, i dette tilfellet). Legg merke til at Peker eksempel har ingen anelse om hva det peker på, og enda mindre størrelsen. Dette betyr at vi ganske enkelt kan ødelegge haugen vår ved hjelp av metodene.

Vi får se senere hvordan vi kan redusere slike feil ved hjelp av JNAs kollisjonsbeskyttelsesfunksjon.

4.5. Håndteringsfeil

Gamle versjoner av standard C-biblioteket brukte det globale errno variabel for å lagre årsaken til at en bestemt samtale mislyktes. For eksempel er dette hvordan en typisk åpen() call ville bruke denne globale variabelen i C:

int fd = open ("noen sti", O_RDONLY); hvis (fd <0) {printf ("Åpen mislyktes: errno =% d \ n", errno); utgang (1); }

Selvfølgelig, i moderne flertrådede programmer, ville denne koden ikke fungere, ikke sant? Vel, takket være Cs forprosessor, kan utviklere fortsatt skrive kode som dette, og det vil fungere bra. Det viser seg at i dag, errno er en makro som utvides til en funksjonsanrop:

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

Nå fungerer denne tilnærmingen bra når du kompilerer kildekode, men det er ikke noe slikt når du bruker JNA. Vi kan erklære den utvidede funksjonen i wrapper-grensesnittet og kalle det eksplisitt, men JNA tilbyr et bedre alternativ: LastErrorException.

Enhver metode som er erklært i innpakningsgrensesnitt med kaster LastErrorException vil automatisk inkludere en sjekk for en feil etter en innfødt samtale. Hvis det rapporterer en feil, vil JNA kaste et LastErrorException, som inkluderer den opprinnelige feilkoden.

La oss legge til et par metoder til StdC wrapper-grensesnitt vi har brukt før for å vise denne funksjonen i aksjon:

offentlig grensesnitt StdC utvider biblioteket {// ... andre metoder utelatt int åpen (strengbane, int-flagg) kaster LastErrorException; int close (int fd) kaster LastErrorException; } 

Nå kan vi bruke åpen() i en prøve / fangst-klausul:

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

I å fange blokkere, kan vi bruke LastErrorException.getErrorCode () for å få originalen errno verdi og bruk den som en del av feilhåndteringslogikken.

4.6. Håndtering av adgangsbrudd

Som nevnt tidligere, beskytter JNA oss ikke mot misbruk av en gitt API, spesielt når det gjelder minnebuffere som sendes frem og tilbake innfødt kode. I normale situasjoner resulterer slike feil i tilgangsbrudd og avslutter JVM.

JNA støtter til en viss grad en metode som lar Java-kode håndtere feil med tilgangsbrudd. Det er to måter å aktivere det på:

  • Sette inn jna.beskyttet systemegenskap til ekte
  • Ringer Native.setProtected (true)

Når vi har aktivert denne beskyttede modusen, vil JNA fange tilgangsbruddfeil som normalt vil føre til et krasj og kaste et java.lang.Error unntak. Vi kan bekrefte at dette fungerer ved hjelp av en Peker initialisert med en ugyldig adresse og prøver å skrive noen data til den:

Native.setProtected (true); Peker p = ny peker (0l); prøv {p.setMemory (0, 100 * 1024, (byte) 0); } catch (Error err) {// ... feilhåndtering utelatt} 

Som dokumentasjonen sier, bør denne funksjonen imidlertid bare brukes til feilsøking / utvikling.

5. Konklusjon

I denne artikkelen har vi vist hvordan du bruker JNA for å få tilgang til innfødt kode enkelt sammenlignet med JNI.

Som vanlig er all kode tilgjengelig på GitHub.