A JNA használata a natív dinamikus könyvtárak eléréséhez

1. Áttekintés

Ebben az oktatóanyagban megtudhatjuk, hogyan lehet a Java Native Access könyvtárat (röviden JNA-t) használni a natív könyvtárak eléréséhez JNI (Java Native Interface) kód megírása nélkül.

2. Miért pont a JNA?

Hosszú évek óta a Java és más JVM-alapú nyelvek nagyrészt teljesítik „egyszer írj, fuss mindenhova” mottót. Néha azonban natív kódot kell használnunk bizonyos funkciók megvalósításához:

  • A C / C ++ nyelven vagy bármely más, natív kód létrehozására alkalmas nyelven írt régi kód újrafelhasználása
  • Rendszer-specifikus funkciók elérése nem elérhető a normál Java futtatás során
  • Optimalizálja a sebességet és / vagy a memória használatát egy adott alkalmazás egyes szakaszain.

Eleinte ez a fajta követelmény azt jelentette, hogy a JNI - Java Native Interface-t kell igénybe vennünk. Bár hatékony, ennek a megközelítésnek vannak hátrányai, és néhány kérdés miatt általában kerülik:

  • Megköveteli a fejlesztőktől, hogy írjanak C / C ++ „ragasztókódot” a Java és a natív kód összekapcsolásához
  • Minden célrendszerhez teljes fordításra és linkelésre van szükség
  • Az értékek elosztása és feloldása a JVM-be és onnan unalmas és hibára hajlamos feladat
  • Jogi és támogatási aggályok a Java és a natív könyvtárak keverésekor

A JNA megoldotta a JNI használatával járó komplexitás nagy részét. Különösen nincs szükség JNI-kód létrehozására a dinamikus könyvtárakban található natív kódok használatához, ami sokkal könnyebbé teszi az egész folyamatot.

Természetesen van néhány kompromisszum:

  • Nem használhatunk közvetlenül statikus könyvtárakat
  • Lassabb, mint a kézműves JNI-kód

A legtöbb alkalmazás esetében azonban a JNA egyszerűségének előnyei messze felülmúlják ezeket a hátrányokat. Mint ilyen, méltányos azt mondani, hogy hacsak nincsenek nagyon konkrét követelményeink, a JNA ma valószínűleg a legjobb elérhető választás a natív kód eléréséhez a Java-ból - vagy mellesleg bármely más JVM-alapú nyelvből.

3. JNA projekt beállítása

Az első dolog, amit meg kell tennünk a JNA használatával, az, hogy hozzáadjuk függőségeit a projektünkhöz pom.xml:

 net.java.dev.jna jna-platform 5.6.0 

A legfrissebb verziója jna-platform letölthető a Maven Central oldalról.

4. A JNA használata

A JNA használata kétlépcsős folyamat:

  • Először létrehozunk egy Java felületet, amely kiterjeszti a JNA-kat Könyvtár interfész a cél natív kód meghívásakor használt módszerek és típusok leírására
  • Ezután átadjuk ezt a felületet a JNA-nak, amely visszaadja ennek az interfésznek a konkrét megvalósítását, amelyet natív módszerek meghívására használunk

4.1. Hívási módszerek a C szabványos könyvtárból

Első példaként használjuk a JNA-t a kényelmes funkció a standard C könyvtárból, amely a legtöbb rendszerben elérhető. Ez a módszer a kettős érv és kiszámítja annak hiperbolikus koszinuszát. Az A-C program ezt a funkciót csak a fejlécfájl:

#include #include int main (int argc, char ** argv) {kettős v = cosh (0,0); printf ("Eredmény:% f \ n", v); }

Hozzuk létre a Java felületet, amely a módszer meghívásához szükséges:

nyilvános felület A CMath kiterjeszti a Library {double cosh (double value) könyvtárat; } 

Ezután a JNA-kat használjuk Anyanyelvi osztályban hozza létre az interfész konkrét megvalósítását, hogy felhívhassuk az API-t:

CMath lib = Native.load (Platform.isWindows ()? "Msvcrt": "c", CMath.class); kettős eredmény = lib.cosh (0); 

Az igazán érdekes rész itt a Betöltés() módszer. Két érvre van szükség: a dinamikus könyvtárnévre és egy Java felületre, amely leírja a használni kívánt módszereket. Ennek az interfésznek a konkrét megvalósítását adja vissza, lehetővé téve számunkra annak bármelyik módszerének meghívását.

A dinamikus könyvtárnevek általában rendszerfüggők, és ez alól a C standard könyvtár sem kivétel: libc.so a legtöbb Linux-alapú rendszerben, de msvcrt.dll a Windows rendszerben. Ezért használtuk a Felület segítő osztály a JNA-ban, hogy ellenőrizze, melyik platformon futunk, és válassza ki a megfelelő könyvtár nevet.

Figyelje meg, hogy nem kell hozzáadnunk a .így vagy .dll kiterjesztés, amint arra utalnak. Továbbá, Linux-alapú rendszereknél nem kell megadnunk a megosztott könyvtáraknál szokásos „lib” előtagot.

Mivel a dinamikus könyvtárak Java nézőpontból Singletonként viselkednek, általános gyakorlat az, hogy deklarálunk egy PÉLDA mező az interfész deklaráció részeként:

nyilvános felület A CMath kiterjeszti a {CMath INSTANCE = Native.load könyvtárat (Platform.isWindows ()? "msvcrt": "c", CMath.class); kettős cosh (kettős érték); } 

4.2. Alaptípusok leképezése

Első példánkban a meghívott módszer csak primitív típusokat használt argumentumként és visszatérési értékként. A JNA ezeket az eseteket automatikusan kezeli, általában természetes Java-társaikat használva, amikor a C-típusokról térképeznek fel:

  • char => bájt
  • rövid => rövid
  • wchar_t => char
  • int => int
  • hosszú => com.sun.jna.NativeLong
  • hosszú hosszú => hosszú
  • lebeg => lebeg
  • kettős => kettős
  • char * => Karakterlánc

Az őslakos számára egy furcsának tűnő leképezést használnak hosszú típus. Ennek oka, hogy C / C ++ nyelven a hosszú típus 32 vagy 64 bites értéket jelenthet, attól függően, hogy 32 vagy 64 bites rendszeren futunk.

A probléma megoldására a JNA biztosítja a NativeLong típus, amely a rendszer architektúrájától függően a megfelelő típust használja.

4.3. Szerkezetek és szakszervezetek

Egy másik gyakori forgatókönyv olyan natív kódú API-kkal foglalkozik, amelyek elvárják a mutatót egyesek számára strukturált vagy unió típus. Amikor létrehozza a Java felületet az eléréséhez, a megfelelő argumentumnak vagy visszatérési értéknek olyan Java típusúnak kell lennie, amely kiterjed Struktúra vagy Unióill.

Például, ha figyelembe vesszük ezt a C struktúrát:

struct foo_t {int mező1; int mező2; char * mező3; };

Java társosztálya a következő lenne:

@FieldOrder ({"mező1", "mező2", "mező3"}) a FooType nyilvános osztály kiterjeszti a struktúrát {int mező1; int mező2; Karaktermező3; };

A JNA megköveteli a @FieldOrder annotációval, így megfelelően sorosíthatja az adatokat egy memória pufferbe, mielőtt argumentumként használná a cél módszerrel.

Alternatív megoldásként felülírhatjuk a getFieldOrder () módszer ugyanarra a hatásra. Egyetlen architektúra / platform megcélzásakor az előbbi módszer általában elég jó. Ez utóbbit használhatjuk a platformokon keresztüli igazítási problémák kezelésére, amelyekhez néha extra kitöltési mezőket kell megadni.

Szakszervezetek hasonlóan működik, néhány pont kivételével:

  • Nem kell használni a @FieldOrder kommentár vagy megvalósítás getFieldOrder ()
  • Fel kell hívnunk setType () mielőtt meghívná a natív módszert

Nézzük meg, hogyan kell csinálni egy egyszerű példával:

public class MyUnion kiterjeszti Union {public String foo; nyilvános kettős sáv; }; 

Most használjuk MyUnion hipotetikus könyvtárral:

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

Ha mindkettő foo és rúd ahol ugyanolyan típusú, akkor inkább a mező nevét kell használnunk:

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

4.4. Mutatók használata

A JNA felajánlja a Mutató absztrakció, amely segít a beíratlan mutatóval deklarált API-k kezelésében - általában a érvénytelen *. Ez az osztály olyan módszereket kínál, amelyek lehetővé teszik az olvasás és az írás hozzáférését az alapul szolgáló natív memória pufferhez, ami nyilvánvaló kockázatokkal jár.

Mielőtt elkezdenénk használni ezt az osztályt, biztosnak kell lennünk abban, hogy világosan megértjük, hogy a hivatkozott memória kinek a tulajdonosa. Ennek elmulasztása valószínűleg nehezen hibakeresési hibákat eredményez, amelyek a memóriaszivárgásokhoz és / vagy érvénytelen hozzáférésekhez kapcsolódnak.

Feltéve, hogy tudjuk, mit csinálunk (mint mindig), nézzük meg, hogyan használhatnánk a jól ismerteket malloc () és ingyenes() a JNA-val működik, memóriapuffer lefoglalására és felszabadítására szolgál. Először hozzuk létre újra a csomagoló felületünket:

nyilvános felület Az StdC kiterjeszti a {StdC INSTANCE = // ... példány létrehozását kihagyva Pointer malloc (hosszú n); ürességmentes (Pointer p); } 

Most használjuk puffer kiosztására és játsszunk vele:

StdC lib = StdC.INSTANCE; Mutató p = lib.malloc (1024); p.setMemory (0l, 1024l, (bájt) 0); lib.free (p); 

A setMemory () A metódus az állandó puffert állandó bájtértékkel tölti fel (ebben az esetben nulla). Figyeljük meg, hogy a Mutató példánynak fogalma sincs mire mutat, még kevésbé a méretére. Ez azt jelenti, hogy a maga módszereivel könnyen megronthatjuk a kupacunkat.

Később meglátjuk, hogyan csökkenthetjük az ilyen hibákat a JNA ütközésvédelmi funkciójával.

4.5. A hibák kezelése

A standard C könyvtár régi verziói a globális verziót használták errno változó az adott hívás sikertelenségének tárolásához. Például ez egy tipikus nyisd ki() A call ezt a globális változót használja C-ben:

int fd = nyitott ("valamilyen út", O_RDONLY); if (fd <0) {printf ("Megnyitás nem sikerült: errno =% d \ n", errno); kilépés (1); }

Természetesen a modern, többszálú programokban ez a kód nem működne, igaz? Nos, a C előfeldolgozójának köszönhetően a fejlesztők továbbra is írhatnak ilyen kódot, és ez remekül fog működni. Kiderült, hogy manapság errno egy makró, amely kibővül függvényhívássá:

// ... kivonat a bitek / errno.h fájlból Linuxon #define errno (* __ errno_location ()) // ... kivonat a Visual Studio-ból #define errno (* _errno ())

Ez a megközelítés jól működik a forráskód összeállításakor, de a JNA használatakor nincs ilyen. Kijelenthetjük a kibővített függvényt a burkoló felületünkön és kifejezetten hívhatjuk, de a JNA jobb alternatívát kínál: LastErrorException.

A burkolóban deklarált bármely módszer kapcsolódik a dobja a LastErrorException-t automatikusan tartalmaz egy hibaellenőrzést egy natív hívás után. Ha hibát jelent, a JNA a-t dobja LastErrorException, amely tartalmazza az eredeti hibakódot.

Adjunk hozzá néhány módszert a StdC burkoló felület, amelyet korábban használtunk a funkció működéséhez:

nyilvános felület Az StdC kiterjeszti a könyvtárat {// ... egyéb módszerek kihagyva int open (String path, int flags) dob LastErrorException; int bezár (int fd) dob LastErrorException; } 

Most már használhatjuk nyisd ki() try / catch záradékban:

StdC lib = StdC.INSTANCE; int fd = 0; próbáld ki a {fd = lib.open ("/ some / path", 0) parancsot; // ... használja az fd} catch (LastErrorException err) {// ... hibakezelést} végül {if (fd> 0) {lib.close (fd); }} 

Ban,-ben fogás blokk, használhatjuk LastErrorException.getErrorCode () hogy megkapja az eredetit errno értéket, és használja a hibakezelési logika részeként.

4.6. A hozzáférési jogsértések kezelése

Mint korábban említettük, a JNA nem véd meg minket egy adott API-val való visszaéléstől, különösen akkor, ha oda-vissza natív kódot továbbított memóriapufferekkel foglalkozunk. Normális helyzetekben az ilyen hibák hozzáférési megsértést eredményeznek, és megszüntetik a JVM-et.

A JNA bizonyos mértékben támogatja azt a módszert, amely lehetővé teszi a Java kód számára a hozzáférés megsértésének hibáinak kezelését. Két módon lehet aktiválni:

  • A jna.védett rendszer tulajdonság a igaz
  • Hívás Native.setProtected (true)

Miután aktiváltuk ezt a védett módot, a JNA elkapja a hozzáférés megsértésének hibáit, amelyek általában összeomlást és a dobást eredményeznek java.lang.Hiba kivétel. A a használatával ellenőrizhetjük, hogy ez működik-e Mutató érvénytelen címmel inicializálva próbálunk néhány adatot írni:

Native.setProtected (true); Mutató p = új mutató (0l); próbáld ki a {p.setMemory (0, 100 * 1024, (byte) 0); } catch (Error err) {// ... hiba kezelése elhagyva} 

Amint azonban a dokumentáció kimondja, ezt a funkciót csak hibakereséshez / fejlesztési célokra szabad használni.

5. Következtetés

Ebben a cikkben bemutattuk, hogyan lehet JNA-t használni a natív kód egyszerű eléréséhez, összehasonlítva a JNI-vel.

Szokás szerint az összes kód elérhető a GitHubon.