Korištenje JNA za pristup matičnim dinamičkim knjižnicama

1. Pregled

U ovom ćemo uputstvu vidjeti kako koristiti knjižnicu Java Native Access (skraćeno JNA) za pristup matičnim knjižnicama bez pisanja bilo kakvog koda JNI (Java Native Interface).

2. Zašto JNA?

Mnogo su godina Java i drugi jezici temeljeni na JVM-u u velikoj mjeri ispunili svoju krilaticu "napiši jednom, trči svugdje". Međutim, ponekad moramo koristiti izvorni kôd za implementaciju neke funkcionalnosti:

  • Ponovna upotreba naslijeđenog koda napisanog na C / C ++ ili bilo kojem drugom jeziku koji može stvoriti izvorni kôd
  • Pristup funkcionalnosti specifičnoj za sustav koji nije dostupan u standardnom Java izvođenju
  • Optimiziranje brzine i / ili upotrebe memorije za određene odjeljke dane aplikacije.

U početku je ova vrsta zahtjeva značila da bismo morali pribjeći JNI - Java Native Interfaceu. Iako je učinkovit, ovaj pristup ima nedostataka i uglavnom je izbjegnut zbog nekoliko problema:

  • Zahtijeva od programera da napišu C / C ++ "ljepljivi kôd" za premošćivanje Jave i izvornog koda
  • Zahtijeva cjelovit alat za prevođenje i povezivanje dostupan za svaki ciljni sustav
  • Marširanje i uklanjanje marširanosti vrijednosti iz i iz JVM-a dosadan je zadatak koji podliježe pogreškama
  • Pitanja pravne i podrške pri miješanju Jave i matičnih knjižnica

JNA je došla riješiti većinu složenosti povezane s korištenjem JNI. Konkretno, nema potrebe za izradom bilo kojeg JNI koda za upotrebu nativnog koda smještenog u dinamičkim knjižnicama, što puno olakšava cijeli postupak.

Naravno, postoje neki kompromisi:

  • Ne možemo izravno koristiti statičke knjižnice
  • Sporije u usporedbi s ručno izrađenim JNI kodom

Ipak, za većinu aplikacija JNA-ini znakovi jednostavnosti daleko nadilaze te nedostatke. Kao takvo, pošteno je reći da je, osim ako nemamo vrlo specifične zahtjeve, JNA danas vjerojatno najbolji dostupni izbor za pristup izvornom kodu s Jave - ili bilo kojeg drugog jezika temeljenog na JVM-u.

3. Postavljanje projekta JNA

Prvo što moramo učiniti da bismo koristili JNA je dodavanje njezinih ovisnosti našem projektu pom.xml:

 net.java.dev.jna jna-platforma 5.6.0 

Najnovija verzija jna-platforma može se preuzeti s Maven Central.

4. Korištenje JNA

Korištenje JNA postupak je u dva koraka:

  • Prvo, kreiramo Java sučelje koje proširuje JNA Knjižnica sučelje za opisivanje metoda i tipova koji se koriste prilikom pozivanja ciljnog izvornog koda
  • Dalje, prosljeđujemo ovo sučelje JNA-u koja vraća konkretnu implementaciju ovog sučelja koje koristimo za pozivanje izvornih metoda

4.1. Metode pozivanja iz C standardne biblioteke

Za naš prvi primjer, upotrijebimo JNA da pozovemo cosh funkcija iz standardne C knjižnice koja je dostupna u većini sustava. Ova metoda zahtijeva dvostruko argument i izračunava njegov hiperbolički kosinus. A-C program može koristiti ovu funkciju samo uključivanjem datoteka zaglavlja:

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

Stvorimo Java sučelje potrebno za pozivanje ove metode:

javno sučelje CMath proširuje Library {double cosh (dvostruka vrijednost); } 

Dalje, koristimo JNA Native klase za stvaranje konkretne implementacije ovog sučelja kako bismo mogli nazvati naš API:

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

Stvarno zanimljiv dio ovdje je poziv na opterećenje() metoda. Potrebna su dva argumenta: naziv dinamičke knjižnice i Java sučelje koje opisuje metode koje ćemo koristiti. Vraća konkretnu implementaciju ovog sučelja, omogućujući nam da pozovemo bilo koju od njegovih metoda.

Sada su imena dinamičnih knjižnica obično ovisna o sustavu, a C standardna knjižnica nije iznimka: libc.tako u većini sustava zasnovanih na Linuxu, ali msvcrt.dll u sustavu Windows. Zbog toga smo koristili Platforma pomoćna klasa, uključena u JNA, da provjeri na kojoj platformi radimo i odabere pravilno ime knjižnice.

Primijetite da ne moramo dodavati .tako ili .dll produženje, kako se podrazumijevaju. Također, za sustave zasnovane na Linuxu ne trebamo navesti prefiks "lib" koji je standardni za dijeljene knjižnice.

Budući da se dinamičke knjižnice iz Java perspektive ponašaju poput Singletona, uobičajena je praksa deklarirati PRIMJER polje kao dio deklaracije sučelja:

javno sučelje CMath proširuje knjižnicu {CMath INSTANCE = Native.load (Platform.isWindows ()? "msvcrt": "c", CMath.class); dvostruki koš (dvostruka vrijednost); } 

4.2. Mapiranje osnovnih tipova

U našem početnom primjeru, pozvana metoda koristila je samo primitivne tipove kao svoj argument i povratnu vrijednost. JNA rješava te slučajeve automatski, obično koristeći svoje prirodne Java kolege prilikom mapiranja iz tipova C:

  • char => bajt
  • kratko => kratko
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • dugo dugo => dugo
  • plutati => plutati
  • dvostruko => dvostruko
  • char * => Niz

Mapiranje koje može izgledati neobično je ono koje se koristi za izvorne dugo tip. To je zato što u C / C ++ dugo type može predstavljati 32- ili 64-bitnu vrijednost, ovisno o tome radimo li na 32- ili 64-bitnom sustavu.

Kako bi riješila ovaj problem, JNA daje NativeLong type, koji koristi odgovarajući tip, ovisno o arhitekturi sustava.

4.3. Strukture i sindikati

Sljedeći uobičajeni scenarij je rješavanje API-ja izvornog koda koji očekuju pokazivač na neke strukt ili unija tip. Prilikom stvaranja Java sučelja za pristup, odgovarajući argument ili povratna vrijednost mora biti vrsta Java koja se proteže Struktura ili Unija, odnosno.

Na primjer, s obzirom na ovu strukturu C:

struct foo_t {int polje1; int polje2; char * polje3; };

Njegova klasa Java vršnjaka bila bi:

@FieldOrder ({"field1", "field2", "field3"}) javna klasa FooType proširuje Struktura {int field1; int polje2; Niz polja3; };

JNA zahtijeva @FieldOrder napomena kako bi mogao pravilno serializirati podatke u memorijski međuspremnik prije nego što ih koristi kao argument ciljnoj metodi.

Alternativno, možemo poništiti getFieldOrder () metoda za isti učinak. Kada ciljate jednu arhitekturu / platformu, prva metoda je općenito dovoljno dobra. Potonje možemo koristiti za rješavanje problema s poravnavanjem na različitim platformama, koji ponekad zahtijevaju dodavanje dodatnih polja za popunjavanje.

Sindikati radite slično, osim nekoliko točaka:

  • Nije potrebno koristiti a @FieldOrder napomena ili provedba getFieldOrder ()
  • Moramo nazvati setType () prije pozivanja nativne metode

Pogledajmo kako to učiniti jednostavnim primjerom:

javna klasa MyUnion proširuje Union {public String foo; javna dvostruka traka; }; 

Sada, iskoristimo MyUnion s hipotetičkom knjižnicom:

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

Ako oboje foo i bar gdje je istog tipa, umjesto toga morali bismo koristiti ime polja:

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

4.4. Korištenje pokazivača

JNA nudi a Pokazivač apstrakcija koja pomaže u rješavanju API-ja deklariranih netipiziranim pokazivačem - obično a praznina *. Ova klasa nudi metode koje omogućuju pristup čitanju i pisanju u osnovni međuspremnik matične memorije, što ima očite rizike.

Prije početka korištenja ove klase, moramo biti sigurni da jasno razumijemo tko "posjeduje" referenciranu memoriju u svakom trenutku. Ako to ne učine, vjerojatno će doći do pogrešaka koje je teško ispraviti u vezi s curenjem memorije i / ili nevaljanim pristupima.

Pod pretpostavkom da znamo što radimo (kao i uvijek), pogledajmo kako možemo koristiti dobro poznato malloc () i besplatno() funkcije s JNA, koriste se za dodjelu i oslobađanje memorijskog međuspremnika. Prvo, izradimo ponovno naše sučelje omotača:

javno sučelje StdC proširuje Biblioteku {StdC INSTANCE = // ... izostavljeno stvaranje instance Pointer malloc (long n); praznina slobodna (pokazivač p); } 

Sada, iskoristimo ga za dodjelu međuspremnika i poigrajmo se s njim:

StdC lib = StdC.INSTANCE; Pokazivač p = lib.malloc (1024); p.setMemory (0l, 1024l, (bajt) 0); lib.free (p); 

The setMemory () metoda samo ispunjava temeljni međuspremnik konstantnom vrijednošću bajta (u ovom slučaju nulom). Primijetite da Pokazivač instanca nema pojma na što upućuje, a još manje svoju veličinu. To znači da možemo prilično lako pokvariti našu hrpu koristeći se njezinim metodama.

Kasnije ćemo vidjeti kako možemo ublažiti takve pogreške pomoću značajke zaštite JNA od pada.

4.5. Rukovanje pogreškama

Stare verzije standardne C knjižnice koristile su globalnu errno varijabla za pohranu razloga zbog kojeg određeni poziv nije uspio. Na primjer, ovo je tipično otvorena() poziv koristi ovu globalnu varijablu u C:

int fd = otvoren ("neki put", O_RDONLY); if (fd <0) {printf ("Otvaranje nije uspjelo: errno =% d \ n", pogrešno); izlaz (1); }

Naravno, u modernim višenitnim programima ovaj kod ne bi radio, zar ne? Pa, zahvaljujući C-ovom pretprocesoru, programeri i dalje mogu pisati ovakav kôd i on će raditi sasvim u redu. Ispada da u današnje vrijeme, errno je makronaredba koja se proširuje na poziv funkcije:

// ... izvadak iz bits / errno.h na Linuxu #define errno (* __ errno_location ()) // ... izvadak iz Visual Studija #define errno (* _errno ())

Ovaj pristup sada dobro funkcionira pri kompajliranju izvornog koda, ali kod upotrebe JNA toga nema. Mogli bismo proglasiti proširenu funkciju u našem omotu omotača i izričito je nazvati, ali JNA nudi bolju alternativu: LastErrorException.

Bilo koja metoda deklarirana u omotu sučelja s baca LastErrorException automatski će uključiti provjeru pogreške nakon izvornog poziva. Ako prijavi pogrešku, JNA će baciti znak LastErrorException, koji uključuje izvorni kôd pogreške.

Dodajmo nekoliko metoda u StdC omotno sučelje koje smo prije koristili za prikaz ove značajke na djelu:

javno sučelje StdC proširuje knjižnicu {// ... izostavljene su druge metode int open (String path, int flags) baca LastErrorException; int close (int fd) baca LastErrorException; } 

Sada možemo koristiti otvorena() u klauzuli try / catch:

StdC lib = StdC.INSTANCE; int fd = 0; pokušajte {fd = lib.open ("/ some / path", 0); // ... koristi fd} catch (greška LastErrorException) {// ... rukovanje pogreškama} konačno {if (fd> 0) {lib.close (fd); }} 

U ulov blok, možemo koristiti LastErrorException.getErrorCode () da biste dobili original errno vrijednost i upotrijebite ga kao dio logike rukovanja pogreškama.

4.6. Rukovanje povredama pristupa

Kao što je već spomenuto, JNA nas ne štiti od zlouporabe određenog API-ja, posebno kada se radi o memorijskim međuspremnicima koji se prosljeđuju naprijed-natrag izvornim kodom. U normalnim situacijama takve pogreške rezultiraju kršenjem pristupa i ukidaju JVM.

JNA u određenoj mjeri podržava metodu koja omogućava Java kodu da obrađuje pogreške povrede pristupa. Postoje dva načina da ga aktivirate:

  • Postavljanje jna.zaštićeno svojstvo sustava na pravi
  • Pozivanje Native.setProtected (true)

Jednom kada aktiviramo ovaj zaštićeni način, JNA će uhvatiti pogreške zbog kršenja pristupa koje bi obično rezultirale padom i bacile java.lang.Pogreška iznimka. Možemo li provjeriti radi li to pomoću a Pokazivač inicijalizirano s neispravnom adresom i pokušavajući na nju upisati neke podatke:

Native.setProtected (true); Pokazivač p = novi pokazivač (0l); pokušajte {p.setMemory (0, 100 * 1024, (bajt) 0); } catch (pogreška pogreške) {// ... izostavljena obrada pogreške} 

Međutim, kako navodi dokumentacija, ova bi se značajka trebala koristiti samo u svrhu uklanjanja pogrešaka / razvoja.

5. Zaključak

U ovom smo članku pokazali kako koristiti JNA za lak pristup izvornom kodu u usporedbi s JNI.

Kao i obično, sav je kôd dostupan na GitHub-u.