Utilizzo di JNA per accedere alle librerie dinamiche native

1. Panoramica

In questo tutorial vedremo come utilizzare la libreria Java Native Access (in breve JNA) per accedere alle librerie native senza scrivere alcun codice JNI (Java Native Interface).

2. Perché JNA?

Per molti anni, Java e altri linguaggi basati su JVM hanno, in larga misura, rispettato il motto "scrivi una volta, esegui ovunque". Tuttavia, a volte è necessario utilizzare codice nativo per implementare alcune funzionalità :

  • Riutilizzo di codice legacy scritto in C / C ++ o qualsiasi altro linguaggio in grado di creare codice nativo
  • Accesso alla funzionalità specifica del sistema non disponibile nel runtime Java standard
  • Ottimizzazione della velocità e / o dell'utilizzo della memoria per sezioni specifiche di una determinata applicazione.

Inizialmente, questo tipo di requisito significava che avremmo dovuto ricorrere a JNI - Java Native Interface. Sebbene efficace, questo approccio ha i suoi svantaggi ed è stato generalmente evitato a causa di alcuni problemi:

  • Richiede agli sviluppatori di scrivere "codice collante" C / C ++ per collegare Java e codice nativo
  • Richiede una toolchain completa di compilazione e collegamento disponibile per ogni sistema di destinazione
  • Il marshalling e l'unmarshalling dei valori da e verso la JVM è un'attività noiosa e soggetta a errori
  • Problemi legali e di supporto quando si combinano Java e librerie native

JNA è arrivato a risolvere la maggior parte della complessità associata all'utilizzo di JNI. In particolare, non è necessario creare alcun codice JNI per utilizzare il codice nativo situato nelle librerie dinamiche, il che rende l'intero processo molto più semplice.

Naturalmente, ci sono alcuni compromessi:

  • Non possiamo usare direttamente le librerie statiche
  • Più lento rispetto al codice JNI artigianale

Per la maggior parte delle applicazioni, tuttavia, i vantaggi della semplicità di JNA superano di gran lunga questi svantaggi. In quanto tale, è giusto dire che, a meno che non abbiamo requisiti molto specifici, JNA oggi è probabilmente la migliore scelta disponibile per accedere al codice nativo da Java - o qualsiasi altro linguaggio basato su JVM, tra l'altro.

3. Configurazione del progetto JNA

La prima cosa che dobbiamo fare per utilizzare JNA è aggiungere le sue dipendenze al pom.xml del nostro progetto :

 net.java.dev.jna jna-platform 5.6.0  

L'ultima versione della piattaforma jna può essere scaricata da Maven Central.

4. Utilizzo di JNA

L'utilizzo di JNA è un processo in due fasi:

  • Innanzitutto, creiamo un'interfaccia Java che estende l' interfaccia della libreria di JNA per descrivere i metodi ei tipi utilizzati quando si chiama il codice nativo di destinazione
  • Successivamente, passiamo questa interfaccia a JNA che restituisce un'implementazione concreta di questa interfaccia che usiamo per invocare metodi nativi

4.1. Metodi di chiamata dalla libreria standard C.

Per il nostro primo esempio, utilizziamo JNA per chiamare la funzione cosh dalla libreria C standard, disponibile nella maggior parte dei sistemi. Questo metodo accetta un doppio argomento e calcola il suo coseno iperbolico. Il programma AC può utilizzare questa funzione semplicemente includendo il file file di intestazione:

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

Creiamo l'interfaccia Java necessaria per chiamare questo metodo:

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

Successivamente, utilizziamo la classe Native di JNA per creare un'implementazione concreta di questa interfaccia in modo da poter chiamare la nostra API:

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

La parte davvero interessante qui è la chiamata al metodo load () . Richiede due argomenti: il nome della libreria dinamica e un'interfaccia Java che descrive i metodi che utilizzeremo. Restituisce un'implementazione concreta di questa interfaccia, permettendoci di chiamare uno qualsiasi dei suoi metodi.

Ora, i nomi delle librerie dinamiche dipendono solitamente dal sistema e la libreria standard C non fa eccezione: libc.so nella maggior parte dei sistemi basati su Linux, ma msvcrt.dll in Windows. Questo è il motivo per cui abbiamo utilizzato la classe helper Platform , inclusa in JNA, per verificare su quale piattaforma stiamo eseguendo e selezionare il nome della libreria appropriato.

Si noti che non è necessario aggiungere l' estensione .so o .dll , poiché sono implicite. Inoltre, per i sistemi basati su Linux, non è necessario specificare il prefisso "lib" che è standard per le librerie condivise.

Poiché le librerie dinamiche si comportano come Singleton da una prospettiva Java, una pratica comune è dichiarare un campo INSTANCE come parte della dichiarazione dell'interfaccia:

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

4.2. Mappatura dei tipi di base

Nel nostro esempio iniziale, il metodo chiamato utilizzava solo tipi primitivi sia come argomento che come valore restituito. JNA gestisce questi casi automaticamente, di solito utilizzando le loro controparti Java naturali durante la mappatura dai tipi C:

  • char => byte
  • breve => breve
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • lungo lungo => lungo
  • float => float
  • double => double
  • char * => String

A mapping that might look odd is the one used for the native long type. This is because, in C/C++, the long type may represent a 32- or 64-bit value, depending on whether we're running on a 32- or 64-bit system.

To address this issue, JNA provides the NativeLong type, which uses the proper type depending on the system's architecture.

4.3. Structures and Unions

Another common scenario is dealing with native code APIs that expect a pointer to some struct or union type. When creating the Java interface to access it, the corresponding argument or return value must be a Java type that extends Structure or Union, respectively.

For instance, given this C struct:

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

Its Java peer class would be:

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

JNA requires the @FieldOrder annotation so it can properly serialize data into a memory buffer before using it as an argument to the target method.

Alternatively, we can override the getFieldOrder() method for the same effect. When targeting a single architecture/platform, the former method is generally good enough. We can use the latter to deal with alignment issues across platforms, that sometimes require adding some extra padding fields.

Unions work similarly, except for a few points:

  • No need to use a @FieldOrder annotation or implement getFieldOrder()
  • We have to call setType() before calling the native method

Let's see how to do it with a simple example:

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

Now, let's use MyUnion with a hypothetical library:

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

If both foo and bar where of the same type, we'd have to use the field's name instead:

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

4.4. Using Pointers

JNA offers a Pointer abstraction that helps to deal with APIs declared with untyped pointer – typically a void *. This class offers methods that allow read and write access to the underlying native memory buffer, which has obvious risks.

Before start using this class, we must be sure we clearly understand who “owns” the referenced memory at each time. Failing to do so will likely produce hard to debug errors related to memory leaks and/or invalid accesses.

Assuming we know what we're doing (as always), let's see how we can use the well-known malloc() and free() functions with JNA, used to allocate and release a memory buffer. First, let's again create our wrapper interface:

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

Now, let's use it to allocate a buffer and play with it:

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

The setMemory() method just fills the underlying buffer with a constant byte value (zero, in this case). Notice that the Pointer instance has no idea to what it is pointing to, much less its size. This means that we can quite easily corrupt our heap using its methods.

We'll see later how we can mitigate such errors using JNA's crash protection feature.

4.5. Handling Errors

Old versions of the standard C library used the global errno variable to store the reason a particular call failed. For instance, this is how a typical open() call would use this global variable in C:

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

Of course, in modern multi-threaded programs this code would not work, right? Well, thanks to C's preprocessor, developers can still write code like this and it will work just fine. It turns out that nowadays, errno is a macro that expands to a function call:

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

Now, this approach works fine when compiling source code, but there's no such thing when using JNA. We could declare the expanded function in our wrapper interface and call it explicitly, but JNA offers a better alternative: LastErrorException.

Any method declared in wrapper interfaces with throws LastErrorException will automatically include a check for an error after a native call. If it reports an error, JNA will throw a LastErrorException, which includes the original error code.

Let's add a couple of methods to the StdC wrapper interface we've used before to show this feature in action:

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

Now, we can use open() in a try/catch clause:

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); } } 

In the catch block, we can use LastErrorException.getErrorCode() to get the original errno value and use it as part of the error handling logic.

4.6. Handling Access Violations

As mentioned before, JNA does not protect us from misusing a given API, especially when dealing with memory buffers passed back and forth native code. In normal situations, such errors result in an access violation and terminate the JVM.

JNA supports, to some extent, a method that allows Java code to handle access violation errors. There are two ways to activate it:

  • Setting the jna.protected system property to true
  • Calling Native.setProtected(true)

Una volta attivata questa modalità protetta, JNA rileverà gli errori di violazione dell'accesso che normalmente causerebbero un arresto anomalo e genererà un'eccezione java.lang.Error . Possiamo verificare che funzioni utilizzando un puntatore inizializzato con un indirizzo non valido e provando a scrivere alcuni dati su di esso:

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

Tuttavia, come afferma la documentazione, questa funzionalità dovrebbe essere utilizzata solo per scopi di debug / sviluppo.

5. conclusione

In questo articolo, abbiamo mostrato come utilizzare JNA per accedere facilmente al codice nativo rispetto a JNI.

Come al solito, tutto il codice è disponibile su GitHub.