Java >> Java tutorial >  >> Tag >> native

Brug af JNA til at få adgang til Native Dynamic Libraries

1. Oversigt

I denne øvelse vil vi se, hvordan du bruger Java Native Access-biblioteket (forkortet JNA) til at få adgang til native biblioteker uden at skrive nogen JNI-kode (Java Native Interface).

2. Hvorfor JNA?

I mange år har Java og andre JVM-baserede sprog i vid udstrækning opfyldt sit motto "skriv én gang, løb overalt". Nogle gange er vi dog nødt til at bruge indbygget kode for at implementere nogle funktioner :

  • Genbrug af ældre kode skrevet i C/C++ eller et hvilket som helst andet sprog, der kan oprette indbygget kode
  • Adgang til systemspecifik funktionalitet er ikke tilgængelig i standard Java-runtime
  • Optimering af hastighed og/eller hukommelsesbrug for specifikke sektioner af en given applikation.

I første omgang betød denne form for krav, at vi skulle ty til JNI – Java Native Interface. Selv om denne tilgang er effektiv, har den sine ulemper og blev generelt undgået på grund af nogle få problemer:

  • Kræver, at udviklere skriver C/C++ "limkode" for at bygge bro mellem Java og native kode
  • Kræver en komplet kompilerings- og linkværktøjskæde tilgængelig for hvert målsystem
  • Arbejdning og opdeling af værdier til og fra JVM er en kedelig og fejlbehæftet opgave
  • Juridiske og supportproblemer ved blanding af Java og native biblioteker

JNA kom til at løse det meste af kompleksiteten forbundet med at bruge JNI. I særdeleshed er det ikke nødvendigt at oprette nogen JNI-kode for at bruge indbygget kode placeret i dynamiske biblioteker, hvilket gør hele processen meget nemmere.

Selvfølgelig er der nogle afvejninger:

  • Vi kan ikke direkte bruge statiske biblioteker
  • Langsommere sammenlignet med håndlavet JNI-kode

For de fleste applikationer opvejer JNAs enkelhedsfordele dog langt disse ulemper. Som sådan er det rimeligt at sige, at medmindre vi har meget specifikke krav, er JNA i dag sandsynligvis det bedst tilgængelige valg til at få adgang til indfødt kode fra Java – eller et hvilket som helst andet JVM-baseret sprog i øvrigt.

3. JNA-projektopsætning

Den første ting, vi skal gøre for at bruge JNA, er at tilføje dens afhængigheder til vores projekts pom.xml :

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna-platform</artifactId>
    <version>5.6.0</version>
</dependency>

Den seneste version af jna-platform kan downloades fra Maven Central.

4. Bruger JNA

Brug af JNA er en to-trins proces:

  • Først opretter vi en Java-grænseflade, der udvider JNA's bibliotek grænseflade til at beskrive de metoder og typer, der bruges, når den oprindelige målkode kaldes
  • Dernæst videregiver vi denne grænseflade til JNA, som returnerer en konkret implementering af denne grænseflade, som vi bruger til at påberåbe native metoder

4.1. Opkaldsmetoder fra C Standard Library

For vores første eksempel, lad os bruge JNA til at kalde cosh funktion fra standard C-biblioteket, som er tilgængeligt i de fleste systemer. Denne metode tager en dobbelt argument og beregner dens hyperbolske cosinus. A-C-programmet kan bruge denne funktion blot ved at inkludere header-fil:

#include <math.h>
#include <stdio.h>
int main(int argc, char** argv) {
    double v = cosh(0.0);
    printf("Result: %f\n", v);
}

Lad os oprette den Java-grænseflade, der er nødvendig for at kalde denne metode:

public interface CMath extends Library { 
    double cosh(double value);
}

Dernæst bruger vi JNA's Native klasse for at skabe en konkret implementering af denne grænseflade, så vi kan kalde vores API:

CMath lib = Native.load(Platform.isWindows()?"msvcrt":"c", CMath.class);
double result = lib.cosh(0);

Den virkelig interessante del her er opkaldet til load() metode . Det kræver to argumenter:det dynamiske biblioteksnavn og en Java-grænseflade, der beskriver de metoder, vi vil bruge. Det returnerer en konkret implementering af denne grænseflade, så vi kan kalde enhver af dens metoder.

Nu er dynamiske biblioteksnavne normalt systemafhængige, og C standardbibliotek er ingen undtagelse:libc.so i de fleste Linux-baserede systemer, men msvcrt.dll i Windows. Det er derfor, vi har brugt Platformen hjælperklasse, inkluderet i JNA, for at kontrollere, hvilken platform vi kører på, og vælge det korrekte biblioteksnavn.

Bemærk, at vi ikke behøver at tilføje .so eller .dll udvidelse, som de er underforstået. For Linux-baserede systemer behøver vi heller ikke at angive "lib"-præfikset, der er standard for delte biblioteker.

Da dynamiske biblioteker opfører sig som singletons fra et Java-perspektiv, er en almindelig praksis at erklære en INSTANS felt som en del af grænsefladeerklæringen:

public interface CMath extends Library {
    CMath INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CMath.class);
    double cosh(double value);
}

4.2. Kortlægning af grundlæggende typer

I vores indledende eksempel brugte den kaldte metode kun primitive typer som både argument og returværdi. JNA håndterer disse sager automatisk, normalt ved at bruge deres naturlige Java-modstykker, når de kortlægger fra C-typer:

  • char => byte
  • kort => kort
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • lang lang => lang
  • float => flyd
  • dobbelt => dobbelt
  • char * => Streng

En kortlægning, der kan se mærkelig ud, er den, der bruges til den oprindelige lange type. Dette skyldes, at den lange i C/C++ type kan repræsentere en 32- eller 64-bit værdi, afhængigt af om vi kører på et 32- eller 64-bit system.

For at løse dette problem leverer JNA NativeLong type, som bruger den korrekte type afhængigt af systemets arkitektur.

4.3. Strukturer og fagforeninger

Et andet almindeligt scenarie er håndtering af indbyggede kode-API'er, der forventer en pointer til en struktur eller union skriv. Når du opretter Java-grænsefladen for at få adgang til den, skal det tilsvarende argument eller returværdi være en Java-type, der udvider Struktur eller Union hhv.

For eksempel givet denne C-struktur:

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

Dens Java peer-klasse ville være:

@FieldOrder({"field1","field2","field3"})
public class FooType extends Structure {
    int field1;
    int field2;
    String field3;
};

JNA kræver @FieldOrder annotering, så den korrekt kan serialisere data til en hukommelsesbuffer, før den bruges som argument for målmetoden.

Alternativt kan vi tilsidesætte getFieldOrder() metode til samme effekt. Når man målretter mod en enkelt arkitektur/platform, er førstnævnte metode generelt god nok. Vi kan bruge sidstnævnte til at håndtere tilpasningsproblemer på tværs af platforme, som nogle gange kræver tilføjelse af nogle ekstra udfyldningsfelter.

Fagforeninger arbejde på samme måde, bortset fra nogle få punkter:

  • Ingen grund til at bruge en @FieldOrder annotering eller implementer getFieldOrder()
  • Vi skal kalde setType() før du kalder den oprindelige metode

Lad os se, hvordan du gør det med et simpelt eksempel:

public class MyUnion extends Union {
    public String foo;
    public double bar;
};

Lad os nu bruge MyUnion med et hypotetisk bibliotek:

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

Hvis begge foo og bar hvor af samme type skal vi bruge feltets navn i stedet:

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

4.4. Brug af pointere

JNA tilbyder en Pointer abstraktion, der hjælper med at håndtere API'er, der er erklæret med utyperet pointer – typisk et tomt * . Denne klasse tilbyder metoder, der tillader læse- og skriveadgang til den underliggende native hukommelsesbuffer, hvilket har indlysende risici.

Før vi begynder at bruge denne klasse, skal vi være sikre på, at vi tydeligt forstår, hvem der "ejer" den refererede hukommelse til enhver tid. Hvis du ikke gør det, vil det sandsynligvis producere svære fejlretningsfejl relateret til hukommelseslækager og/eller ugyldige adgange.

Forudsat at vi ved, hvad vi laver (som altid), lad os se, hvordan vi kan bruge den velkendte malloc() og free() funktioner med JNA, bruges til at allokere og frigive en hukommelsesbuffer. Lad os først igen oprette vores indpakningsgrænseflade:

public interface StdC extends Library {
    StdC INSTANCE = // ... instance creation omitted
    Pointer malloc(long n);
    void free(Pointer p);
}

Lad os nu bruge den til at allokere en buffer og lege med den:

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

setMemory() metoden fylder bare den underliggende buffer med en konstant byteværdi (nul, i dette tilfælde). Bemærk, at Markøren forekomsten har ingen idé om, hvad den peger på, meget mindre dens størrelse. Det betyder, at vi ganske nemt kan ødelægge vores bunke ved hjælp af dens metoder.

Vi vil se senere, hvordan vi kan afhjælpe sådanne fejl ved at bruge JNA's funktion til beskyttelse mod nedbrud.

4.5. Håndtering af fejl

Gamle versioner af standard C-biblioteket brugte det globale errno variabel for at gemme årsagen til, at et bestemt opkald mislykkedes. Det er for eksempel sådan en typisk open() call ville bruge denne globale variabel i C:

int fd = open("some path", O_RDONLY);
if (fd < 0) {
    printf("Open failed: errno=%d\n", errno);
    exit(1);
}

Selvfølgelig, i moderne multi-threaded programmer ville denne kode ikke fungere, vel? Nå, takket være C's præprocessor kan udviklere stadig skrive kode som denne, og det vil fungere fint. Det viser sig, at i dag errno er en makro, der udvides til et funktionskald:

// ... excerpt from bits/errno.h on Linux
#define errno (*__errno_location ())

// ... excerpt from <errno.h> from Visual Studio
#define errno (*_errno())

Nu fungerer denne tilgang fint, når du kompilerer kildekode, men der er ikke sådan noget, når du bruger JNA. Vi kunne erklære den udvidede funktion i vores wrapper-grænseflade og kalde den eksplicit, men JNA tilbyder et bedre alternativ:LastErrorException .

Enhver metode, der er erklæret i wrapper-grænseflader med throws LastErrorException vil automatisk inkludere en check for en fejl efter et indbygget opkald. Hvis den rapporterer en fejl, sender JNA en LastErrorException , som inkluderer den originale fejlkode.

Lad os tilføje et par metoder til StdC wrapper-grænseflade, vi har brugt før til at vise denne funktion i aktion:

public interface StdC extends Library {
    // ... other methods omitted
    int open(String path, int flags) throws LastErrorException;
    int close(int fd) throws LastErrorException;
}

Nu kan vi bruge open() i en try/catch-klausul:

StdC lib = StdC.INSTANCE;
int fd = 0;
try {
    fd = lib.open("/some/path",0);
    // ... use fd
}
catch (LastErrorException err) {
    // ... error handling
}
finally {
    if (fd > 0) {
       lib.close(fd);
    }
}

fangsten blok, kan vi bruge LastErrorException.getErrorCode() for at få den originale errno værdi og bruge den som en del af fejlhåndteringslogikken.

4.6. Håndtering af adgangsovertrædelser

Som før nævnt beskytter JNA os ikke mod at misbruge en given API, især når vi håndterer hukommelsesbuffere, der sendes frem og tilbage indbygget kode . I normale situationer resulterer sådanne fejl i en adgangsovertrædelse og afslutter JVM.

JNA understøtter til en vis grad en metode, der tillader Java-kode at håndtere adgangsfejl. Der er to måder at aktivere det på:

  • Indstilling af jna.protected systemegenskaben til true
  • Ringer til Native.setProtected(true)

Når vi har aktiveret denne beskyttede tilstand, vil JNA fange adgangsfejl, der normalt ville resultere i et nedbrud og kaste en java.lang.Error undtagelse. Vi kan bekræfte, at dette virker ved hjælp af en Pointer initialiseret med en ugyldig adresse og forsøger at skrive nogle data til den:

Native.setProtected(true);
Pointer p = new Pointer(0l);
try {
    p.setMemory(0, 100*1024, (byte) 0);
}
catch (Error err) {
    // ... error handling omitted
}

Men, som det fremgår af dokumentationen, bør denne funktion kun bruges til fejlretnings-/udviklingsformål.

5. Konklusion

I denne artikel har vi vist, hvordan man bruger JNA til nemt at få adgang til indbygget kode sammenlignet med JNI.

Som sædvanlig er al kode tilgængelig på GitHub.


Java tag