JNA gebruiken om toegang te krijgen tot native dynamische bibliotheken

1. Overzicht

In deze zelfstudie zullen we zien hoe u de Java Native Access-bibliotheek (afgekort JNA) gebruikt om toegang te krijgen tot native bibliotheken zonder JNI-code (Java Native Interface) te schrijven.

2. Waarom JNA?

Jarenlang hebben Java en andere op JVM gebaseerde talen voor een groot deel het motto "een keer schrijven, overal draaien" vervuld. Soms moeten we echter native code gebruiken om bepaalde functionaliteit te implementeren:

  • Hergebruik van oude code die is geschreven in C / C ++ of een andere taal die native code kan maken
  • Toegang tot systeemspecifieke functionaliteit die niet beschikbaar is in de standaard Java-runtime
  • Het optimaliseren van snelheid en / of geheugengebruik voor specifieke secties van een bepaalde applicatie.

Aanvankelijk betekende dit soort vereisten dat we onze toevlucht moesten nemen tot JNI - Java Native Interface. Hoewel effectief, heeft deze aanpak zijn nadelen en werd deze over het algemeen vermeden vanwege een aantal problemen:

  • Vereist dat ontwikkelaars C / C ++ "glue code" schrijven om Java en native code te overbruggen
  • Vereist een volledige compileer- en linktoolchain die beschikbaar is voor elk doelsysteem
  • Het rangschikken en unmarshalling van waarden van en naar de JVM is een vervelende en foutgevoelige taak
  • Juridische en ondersteunende problemen bij het combineren van Java en native bibliotheken

JNA loste de meeste complexiteit op die gepaard gaat met het gebruik van JNI. In het bijzonder is het niet nodig om een ​​JNI-code te maken om native code te gebruiken die zich in dynamische bibliotheken bevindt, wat het hele proces veel eenvoudiger maakt.

Natuurlijk zijn er enkele afwegingen:

  • We kunnen niet rechtstreeks statische bibliotheken gebruiken
  • Langzamer in vergelijking met handgemaakte JNI-code

Voor de meeste toepassingen wegen de voordelen van JNA echter ruimschoots op tegen die nadelen. Als zodanig is het redelijk om te zeggen dat, tenzij we zeer specifieke vereisten hebben, JNA vandaag waarschijnlijk de beste beschikbare keuze is om toegang te krijgen tot native code vanuit Java - of een andere JVM-gebaseerde taal trouwens.

3. JNA-project instellen

Het eerste dat we moeten doen om JNA te gebruiken, is zijn afhankelijkheden toevoegen aan die van ons project pom.xml:

 net.java.dev.jna jna-platform 5.6.0 

De nieuwste versie van jna-platform kan worden gedownload van Maven Central.

4. JNA gebruiken

Het gebruik van JNA is een proces in twee stappen:

  • Eerst maken we een Java-interface die JNA's uitbreidt Bibliotheek interface om de methoden en typen te beschrijven die worden gebruikt bij het aanroepen van de oorspronkelijke doelcode
  • Vervolgens geven we deze interface door aan JNA die een concrete implementatie van deze interface retourneert die we gebruiken om native methoden aan te roepen

4.1. Methoden aanroepen uit de C-standaardbibliotheek

Laten we voor ons eerste voorbeeld JNA gebruiken om de cosh functie uit de standaard C-bibliotheek, die in de meeste systemen beschikbaar is. Deze methode duurt een dubbele argument en berekent zijn hyperbolische cosinus. A-C-programma kan deze functie gebruiken door de header-bestand:

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

Laten we de Java-interface maken die nodig is om deze methode aan te roepen:

openbare interface CMath breidt Bibliotheek {dubbele cosh (dubbele waarde) uit; } 

Vervolgens gebruiken we JNA's Inheems class om een ​​concrete implementatie van deze interface te maken, zodat we onze API kunnen aanroepen:

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

Het echt interessante deel hier is de oproep aan de laden() methode. Er zijn twee argumenten voor nodig: de naam van de dynamische bibliotheek en een Java-interface die de methoden beschrijft die we zullen gebruiken. Het geeft een concrete implementatie van deze interface terug, waardoor we elk van zijn methoden kunnen aanroepen.

Nu zijn dynamische bibliotheeknamen meestal systeemafhankelijk en de C-standaardbibliotheek is geen uitzondering: libc.so in de meeste op Linux gebaseerde systemen, maar msvcrt.dll in Windows. Daarom hebben we de Platform helper-klasse, opgenomen in JNA, om te controleren op welk platform we draaien en om de juiste bibliotheeknaam te selecteren.

Merk op dat we de .zo of .dll extensie, zoals ze impliceren. Bovendien hoeven we voor op Linux gebaseerde systemen niet het voorvoegsel "lib" te specificeren dat standaard is voor gedeelde bibliotheken.

Aangezien dynamische bibliotheken zich vanuit Java-perspectief als Singletons gedragen, is het gebruikelijk om een VOORBEELD veld als onderdeel van de interfaceverklaring:

openbare interface CMath breidt Bibliotheek {CMath INSTANCE = Native.load (Platform.isWindows ()? "msvcrt": "c", CMath.class); dubbele cosh (dubbele waarde); } 

4.2. Basistypen in kaart brengen

In ons eerste voorbeeld gebruikte de aangeroepen methode alleen primitieve typen als zowel het argument als de retourwaarde. JNA behandelt die gevallen automatisch, meestal met behulp van hun natuurlijke Java-tegenhangers bij het toewijzen van C-typen:

  • char => byte
  • kort => kort
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • lang lang => lang
  • float => zweven
  • dubbel => dubbel
  • char * => String

Een mapping die er misschien vreemd uitziet, is degene die wordt gebruikt voor de native lang type. Dit komt doordat in C / C ++ de lang type kan een 32- of 64-bits waarde vertegenwoordigen, afhankelijk van of we op een 32- of 64-bits systeem werken.

Om dit probleem op te lossen, biedt JNA het NativeLong type, dat het juiste type gebruikt, afhankelijk van de architectuur van het systeem.

4.3. Structuren en vakbonden

Een ander veelvoorkomend scenario is het omgaan met native code API's die voor sommigen een aanwijzer verwachten struct of unie type. Bij het maken van de Java-interface om er toegang toe te krijgen, moet het corresponderende argument of de retourwaarde een Java-type zijn dat uitbreidt Structuur of unie, respectievelijk.

Gezien deze C-structuur bijvoorbeeld:

struct foo_t {int field1; int veld2; char * field3; };

De Java-peer class zou zijn:

@FieldOrder ({"field1", "field2", "field3"}) openbare klasse FooType breidt Structuur {int field1; int veld2; String veld3; };

JNA vereist het @FieldOrder annotatie zodat het gegevens correct kan serialiseren in een geheugenbuffer voordat het als een argument voor de doelmethode wordt gebruikt.

Als alternatief kunnen we de getFieldOrder () methode voor hetzelfde effect. Wanneer u zich richt op een enkele architectuur / platform, is de eerste methode over het algemeen goed genoeg. We kunnen dit laatste gebruiken om uitlijningsproblemen op verschillende platforms op te lossen, waarvoor soms wat extra opvulvelden moeten worden toegevoegd.

Vakbonden werken op dezelfde manier, met uitzondering van een paar punten:

  • Het is niet nodig om een @FieldOrder annotatie of implementeren getFieldOrder ()
  • We moeten bellen setType () voordat u de native methode aanroept

Laten we eens kijken hoe we het moeten doen met een eenvoudig voorbeeld:

openbare klasse MyUnion breidt Union {public String foo; openbare dubbele balk; }; 

Laten we nu gebruiken MyUnion met een hypothetische bibliotheek:

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

Als beide foo en bar waar van hetzelfde type, zouden we in plaats daarvan de naam van het veld moeten gebruiken:

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

4.4. Pointers gebruiken

JNA biedt een Wijzer abstractie die helpt bij het omgaan met API's die zijn gedeclareerd met een niet-getypeerde aanwijzer - meestal een leegte *. Deze klasse biedt methoden die lees- en schrijftoegang tot de onderliggende native geheugenbuffer mogelijk maken, wat duidelijke risico's met zich meebrengt.

Voordat we deze klasse gaan gebruiken, moeten we er zeker van zijn dat we op elk moment duidelijk begrijpen wie de "eigenaar" is van het geheugen waarnaar wordt verwezen. Als u dit niet doet, zal dit waarschijnlijk leiden tot moeilijk te debuggen fouten met betrekking tot geheugenlekken en / of ongeldige toegangen.

Ervan uitgaande dat we weten wat we doen (zoals altijd), laten we dan eens kijken hoe we het bekende kunnen gebruiken malloc () en vrij() functies met JNA, gebruikt om een ​​geheugenbuffer toe te wijzen en vrij te geven. Laten we eerst opnieuw onze wrapper-interface maken:

openbare interface StdC breidt Bibliotheek {StdC INSTANCE = // ... aanmaak instantie weggelaten Pointer malloc (lange n); leegte gratis (Pointer p); } 

Laten we het nu gebruiken om een ​​buffer toe te wijzen en ermee te spelen:

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

De setMemory () methode vult gewoon de onderliggende buffer met een constante bytewaarde (in dit geval nul). Merk op dat de Wijzer instantie heeft geen idee waarnaar het verwijst, laat staan ​​de grootte ervan. Dit betekent dat we onze hoop vrij gemakkelijk kunnen corrumperen met behulp van zijn methoden.

We zullen later zien hoe we dergelijke fouten kunnen verminderen met behulp van de crashbeveiligingsfunctie van JNA.

4.5. Fouten afhandelen

Oude versies van de standaard C-bibliotheek gebruikten de global errno variabele om de reden op te slaan waarom een ​​bepaalde oproep is mislukt. Dit is bijvoorbeeld hoe een typisch Open() call zou deze globale variabele in C gebruiken:

int fd = open ("een pad", O_RDONLY); if (fd <0) {printf ("Openen mislukt: errno =% d \ n", errno); exit (1); }

In moderne multi-threaded programma's zou deze code natuurlijk niet werken, toch? Nou, dankzij de preprocessor van C kunnen ontwikkelaars nog steeds op deze manier code schrijven en het zal prima werken. Het blijkt dat tegenwoordig errno is een macro die zich uitbreidt naar een functieaanroep:

// ... fragment uit bits / errno.h op Linux #define errno (* __ errno_location ()) // ... fragment uit Visual Studio #define errno (* _errno ())

Nu werkt deze aanpak prima bij het compileren van broncode, maar zoiets bestaat niet bij het gebruik van JNA. We zouden de uitgebreide functie in onze wrapper-interface kunnen declareren en deze expliciet kunnen noemen, maar JNA biedt een beter alternatief: LastErrorException.

Elke methode die in wrapper is gedeclareerd, heeft een interface met gooit LastErrorException zal automatisch een controle op fouten bevatten na een native call. Als het een fout rapporteert, gooit JNA een LastErrorException, die de originele foutcode bevat.

Laten we een aantal methoden toevoegen aan het StdC wrapper-interface die we eerder hebben gebruikt om deze functie in actie te laten zien:

openbare interface StdC breidt Bibliotheek uit {// ... andere weggelaten methoden int open (String path, int flags) gooit LastErrorException; int close (int fd) gooit LastErrorException; } 

Nu kunnen we gebruiken Open() in een try / catch-clausule:

StdC lib = StdC.INSTANCE; int fd = 0; probeer {fd = lib.open ("/ sommige / pad", 0); // ... gebruik fd} catch (LastErrorException err) {// ... foutafhandeling} eindelijk {if (fd> 0) {lib.close (fd); }} 

In de vangst blok, we kunnen gebruiken LastErrorException.getErrorCode () om het origineel te krijgen errno waarde en gebruik het als onderdeel van de foutafhandelingslogica.

4.6. Omgaan met toegangsschendingen

Zoals eerder vermeld, beschermt JNA ons niet tegen misbruik van een bepaalde API, vooral niet als het gaat om geheugenbuffers die heen en weer worden gestuurd met native code. In normale situaties resulteren dergelijke fouten in een toegangsovertreding en wordt de JVM beëindigd.

JNA ondersteunt tot op zekere hoogte een methode waarmee Java-code fouten bij toegangsovertredingen kan afhandelen. Er zijn twee manieren om het te activeren:

  • Instellen van de jna. beschermd systeemeigenschap naar waar
  • Roeping Native.setProtected (true)

Zodra we deze beveiligde modus hebben geactiveerd, zal JNA toegangsovertredingsfouten opvangen die normaal gesproken zouden resulteren in een crash en een java.lang.Error uitzondering. We kunnen verifiëren dat dit werkt met een Wijzer geïnitialiseerd met een ongeldig adres en proberen er wat gegevens naar te schrijven:

Native.setProtected (true); Pointer p = nieuwe Pointer (0l); probeer {p.setMemory (0, 100 * 1024, (byte) 0); } catch (Error err) {// ... foutafhandeling weggelaten} 

Zoals de documentatie vermeldt, mag deze functie echter alleen worden gebruikt voor foutopsporings- / ontwikkelingsdoeleinden.

5. Conclusie

In dit artikel hebben we laten zien hoe u JNA kunt gebruiken om eenvoudig toegang te krijgen tot native code in vergelijking met JNI.

Zoals gewoonlijk is alle code beschikbaar op GitHub.