Používanie JNA na prístup k natívnym dynamickým knižniciam

1. Prehľad

V tomto tutoriáli sa dozvieme, ako používať knižnicu Java Native Access (skrátene JNA) na prístup k natívnym knižniciam bez písania kódu JNI (Java Native Interface).

2. Prečo JNA?

Po mnoho rokov Java a ďalšie jazyky založené na JVM vo veľkej miere plnili svoje heslo „napísať raz, bežať všade“. Niekedy však na implementáciu niektorých funkcií musíme použiť natívny kód:

  • Opätovné použitie pôvodného kódu napísaného v jazyku C / C ++ alebo inom jazyku schopnom vytvoriť natívny kód
  • Prístup k špecifickým funkciám systému nie je k dispozícii v štandardnom prostredí runtime Java
  • Optimalizácia rýchlosti a / alebo využitia pamäte pre konkrétne sekcie danej aplikácie.

Spočiatku tento druh požiadavky znamenal, že sa budeme musieť uchýliť k JNI - natívnemu rozhraniu Java. Aj keď je tento prístup efektívny, má svoje nevýhody a bol všeobecne vylúčený z dôvodu niekoľkých problémov:

  • Vyžaduje, aby vývojári napísali „lepiaci kód“ C / C ++ na premostenie Javy a natívneho kódu
  • Vyžaduje úplnú kompiláciu a prepojenie nástrojov, ktoré sú k dispozícii pre každý cieľový systém
  • Zaradenie a zrušenie rozdelenia hodnôt do a z JVM je únavná a náchylná na chyby
  • Právne otázky a podpora pri zmiešaní Javy a natívnych knižníc

Spoločnosť JNA prišla vyriešiť väčšinu zložitostí spojených s používaním JNI. Najmä nie je potrebné vytvárať žiadny kód JNI, aby ste mohli používať natívny kód umiestnený v dynamických knižniciach, čo celý proces výrazne uľahčuje.

Samozrejme, existujú určité kompromisy:

  • Nemôžeme priamo používať statické knižnice
  • Pomalšie v porovnaní s ručne vyrobeným kódom JNI

Pre väčšinu aplikácií však výhody jednoduchosti JNA tieto nevýhody vysoko prevyšujú. Preto je spravodlivé povedať, že pokiaľ nemáme veľmi konkrétne požiadavky, dnes je JNA pravdepodobne najlepšia dostupná voľba na prístup k natívnemu kódu z Javy - alebo mimochodom z iného jazyka založeného na JVM.

3. Nastavenie projektu JNA

Prvá vec, ktorú musíme urobiť, aby sme mohli používať JNA, je pridať jej závislosti do nášho projektu pom.xml:

 net.java.dev.jna jna-platforma 5.6.0 

Najnovšia verzia servera platforma jna je možné stiahnuť z Maven Central.

4. Používanie JNA

Používanie JNA je proces pozostávajúci z dvoch krokov:

  • Najskôr vytvoríme rozhranie Java, ktoré rozširuje JNA Knižnica rozhranie na opis metód a typov použitých pri volaní cieľového natívneho kódu
  • Ďalej odovzdáme toto rozhranie JNA, ktorá vráti konkrétnu implementáciu tohto rozhrania, ktorú používame na vyvolanie natívnych metód

4.1. Volanie metód zo štandardnej knižnice C.

Pre náš prvý príklad použijeme JNA na volanie keš funkcia zo štandardnej knižnice C, ktorá je k dispozícii vo väčšine systémov. Táto metóda vyžaduje a dvojitý argument a počíta jeho hyperbolický kosínus. Program A-C môže používať túto funkciu iba zahrnutím hlavičkový súbor:

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

Vytvorme rozhranie Java potrebné na volanie tejto metódy:

verejné rozhranie CMath rozširuje Library {double cosh (double value); } 

Ďalej použijeme JNA Nativní triedy na vytvorenie konkrétnej implementácie tohto rozhrania, aby sme mohli zavolať naše API:

CMath lib = Native.load (Platform.isWindows ()? "Msvcrt": "c", CMath.class); dvojitý výsledok = lib.cosh (0); 

Skutočne zaujímavou časťou je volanie na server naložiť() metóda. Vyžaduje si dva argumenty: názov dynamickej knižnice a rozhranie Java popisujúce metódy, ktoré použijeme. Vracia konkrétnu implementáciu tohto rozhrania a umožňuje nám zavolať ktorúkoľvek z jeho metód.

Teraz sú názvy dynamických knižníc zvyčajne závislé od systému a štandardná knižnica C nie je výnimkou: libc.so vo väčšine systémov založených na systéme Linux, ale msvcrt.dll vo Windows. Preto sme použili Plošina pomocná trieda zahrnutá v JNA, aby sme skontrolovali, na ktorej platforme bežíme, a vyberte správny názov knižnice.

Všimnite si, že nemusíme pridávať .takže alebo .dll ako je naznačené. V prípade systémov založených na systéme Linux tiež nemusíme špecifikovať predponu „lib“, ktorá je štandardom pre zdieľané knižnice.

Pretože dynamické knižnice sa z pohľadu Java správajú ako Singletons, bežnou praxou je deklarovať INSTANCE ako súčasť deklarácie rozhrania:

verejné rozhranie CMath rozširuje Library {CMath INSTANCE = Native.load (Platform.isWindows ()? "msvcrt": "c", CMath.class); double cosh (dvojitá hodnota); } 

4.2. Mapovanie základných typov

V našom počiatočnom príklade volaná metóda používala ako svoj argument aj návratovú hodnotu iba primitívne typy. JNA tieto prípady rieši automaticky, zvyčajne pri mapovaní z typov C používa svoje prirodzené náprotivky Java:

  • char => bajt
  • krátke => krátke
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • dlho dlho => dlho
  • float => float
  • double => double
  • char * => Reťazec

Mapovanie, ktoré by mohlo vyzerať zvláštne, je to, ktoré sa používa pre natívnu reklamu dlho typu. Je to tak preto, lebo v C / C ++ dlho type môže predstavovať 32- alebo 64-bitovú hodnotu, v závislosti od toho, či bežíme na 32- alebo 64-bitovom systéme.

Na riešenie tohto problému poskytuje spoločnosť JNA NativeLong typu, ktorý používa správny typ v závislosti od architektúry systému.

4.3. Štruktúry a odbory

Ďalším bežným scenárom je práca s API natívneho kódu, ktoré očakávajú smerník na niektoré štruktúr alebo únie typu. Pri vytváraní rozhrania Java na prístup k nemu musí byť zodpovedajúcim argumentom alebo návratovou hodnotou typ Java, ktorý sa rozširuje Štruktúra alebo únia, resp.

Napríklad vzhľadom na túto štruktúru C:

struct foo_t {int pole1; int pole2; char * pole3; };

Jeho rovnocenná trieda Java by bola:

@FieldOrder ({"pole1", "pole2", "pole3"}) verejná trieda FooType rozširuje štruktúru {int pole1; int pole2; Reťazcové pole3; };

JNA vyžaduje @ FieldOrder anotáciu, aby mohla správne serializovať údaje do medzipamäte pamäte predtým, ako ich použije ako argument cieľovej metódy.

Prípadne môžeme prepísať getFieldOrder () metóda s rovnakým účinkom. Pri zacielení na jednu architektúru / platformu je prvá metóda vo všeobecnosti dosť dobrá. Poslednú z nich môžeme použiť na riešenie problémov so zarovnaním naprieč platformami, ktoré niekedy vyžadujú pridanie ďalších polôh výplne.

Odbory fungujú podobne, až na pár bodov:

  • Nie je potrebné používať a @ FieldOrder anotáciu alebo implementovať getFieldOrder ()
  • Musíme zavolať setType () pred volaním natívnej metódy

Pozrime sa, ako to urobiť, na jednoduchom príklade:

public class MyUnion rozširuje Union {public String foo; verejný dvojitý bar; }; 

Teraz poďme MyUnion s hypotetickou knižnicou:

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

Ak oboje foo a bar kde rovnakého typu by sme museli namiesto toho použiť názov poľa:

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

4.4. Používanie ukazovateľov

JNA ponúka a Ukazovateľ abstrakcia, ktorá pomáha pri riešení API deklarovaných pomocou netypovaného ukazovateľa - zvyčajne a neplatné *. Táto trieda ponúka metódy, ktoré umožňujú prístup na čítanie a zápis do základnej vyrovnávacej pamäte natívnej pamäte, ktorá má zjavné riziká.

Pred začatím používania tejto triedy si musíme byť istí, že jasne rozumieme tomu, kto zakaždým „vlastní“ odkazovanú pamäť. Ak to neurobíte, bude pravdepodobne ťažké vyladiť chyby súvisiace s únikmi pamäte alebo neplatnými prístupmi.

Za predpokladu, že vieme, čo robíme (ako vždy), pozrime sa, ako môžeme využiť dobre známe malloc () a zadarmo() funkcie s JNA, ktoré sa používajú na pridelenie a uvoľnenie vyrovnávacej pamäte. Najskôr si znova vytvoríme naše obalové rozhranie:

verejné rozhranie StdC rozširuje knižnicu {StdC INSTANCE = // ... vynechanie vytvárania inštancie Ukazovateľ malloc (dlhé n); voľný priestor (ukazovateľ p); } 

Teraz ho použijeme na pridelenie medzipamäte a hru s ňou:

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

The setMemory () metóda iba vyplní podkladový buffer konštantnou hodnotou bajtu (v tomto prípade nula). Všimnite si, že Ukazovateľ inštancia vôbec netuší, na čo ukazuje, tým menej jej veľkosť. To znamená, že pomocou našich metód môžeme pomerne ľahko poškodiť našu haldu.

Uvidíme neskôr, ako môžeme tieto chyby zmierniť pomocou funkcie ochrany pred zlyhaním JNA.

4.5. Riešenie chýb

Staré verzie štandardnej knižnice C používali globálnu errno premenná na uloženie dôvodu, prečo konkrétny hovor zlyhal. Napríklad takto je to typické otvorené() volanie použije túto globálnu premennú v C:

int fd = open ("nejaká cesta", O_RDONLY); if (fd <0) {printf ("Open failed: errno =% d \ n", errno); výstup (1); }

Samozrejme, v moderných viacvláknových programoch by tento kód nefungoval, však? Vďaka preprocesoru C môžu vývojári stále písať tento kód a bude to fungovať dobre. Ukazuje sa, že v dnešnej dobe errno je makro, ktoré sa rozšíri na volanie funkcie:

// ... výňatok z bitov / errno.h v systéme Linux #define errno (* __ errno_location ()) // ... výňatok z balíka Visual Studio #define errno (* _errno ())

Teraz tento prístup funguje dobre pri kompilácii zdrojového kódu, ale pri použití JNA nič také neexistuje. Mohli by sme deklarovať rozšírenú funkciu v našom súhrnnom rozhraní a zavolať ju explicitne, ale JNA ponúka lepšiu alternatívu: LastErrorException.

Akákoľvek metóda deklarovaná v rozhraní wrapper s hodí LastErrorException po natívnom hovore automaticky zahrnie kontrolu chyby. Ak nahlási chybu, JNA hodí a LastErrorException, ktorý obsahuje pôvodný chybový kód.

Pridajme niekoľko metód k StdC obalové rozhranie, ktoré sme predtým používali na zobrazenie tejto funkcie v akcii:

verejné rozhranie StdC rozširuje Knižnicu {// ... iné metódy vynechané int otvorené (reťazcová cesta, int príznaky) vyvolá LastErrorException; int close (int fd) hodí LastErrorException; } 

Teraz môžeme použiť otvorené() v klauzule try / catch:

StdC lib = StdC.INSTANCE; int fd = 0; skus {fd = lib.open ("/ some / path", 0); // ... použite fd} catch (LastErrorException err) {// ... spracovanie chýb} konečne {if (fd> 0) {lib.close (fd); }} 

V chytiť blok, môžeme použiť LastErrorException.getErrorCode () získať originál errno hodnotu a použiť ju ako súčasť logiky spracovania chýb.

4.6. Riešenie porušení prístupu

Ako už bolo spomenuté, JNA nás nechráni pred zneužitím daného API, najmä keď sa jedná o vyrovnávacie pamäte prenášané tam a späť natívnym kódom. V normálnych situáciách majú takéto chyby za následok porušenie prístupu a ukončenie JVM.

JNA do istej miery podporuje metódu, ktorá umožňuje kódu Java spracovávať chyby narušenia prístupu. Existujú dva spôsoby, ako ju aktivovať:

  • Nastavenie jna.chránené systémový majetok do pravda
  • Telefonovanie Native.setProtected (true)

Len čo aktivujeme tento chránený režim, JNA zachytí chyby narušenia prístupu, ktoré by za normálnych okolností mali za následok haváriu, a hod java.lang.Error výnimkou. To, či to funguje, môžeme overiť pomocou a Ukazovateľ inicializovaný s neplatnou adresou a pokúšajúci sa na ňu zapísať nejaké údaje:

Native.setProtected (true); Ukazovateľ p = nový Ukazovateľ (0l); skúsiť {p.setMemory (0, 100 * 1024, (bajt) 0); } catch (Error err) {// ... spracovanie chýb vynechané} 

Ako sa však uvádza v dokumentácii, táto funkcia by sa mala používať iba na účely ladenia / vývoja.

5. Záver

V tomto článku sme si ukázali, ako používať JNA na ľahký prístup k natívnemu kódu v porovnaní s JNI.

Ako obvykle je všetok kód k dispozícii na GitHub.


$config[zx-auto] not found$config[zx-overlay] not found