Verwenden von JNA für den Zugriff auf native dynamische Bibliotheken

1. Übersicht

In diesem Tutorial erfahren Sie, wie Sie mit der Java Native Access-Bibliothek (kurz JNA) auf native Bibliotheken zugreifen können, ohne JNI-Code (Java Native Interface) zu schreiben.

2. Warum JNA?

Java und andere JVM-basierte Sprachen erfüllen seit vielen Jahren weitgehend das Motto „Einmal schreiben, überall laufen“. Manchmal müssen wir jedoch nativen Code verwenden, um einige Funktionen zu implementieren :

  • Wiederverwendung von in C / C ++ oder einer anderen Sprache geschriebenem Legacy-Code, mit dem nativer Code erstellt werden kann
  • Zugriff auf systemspezifische Funktionen, die in der Standard-Java-Laufzeit nicht verfügbar sind
  • Optimieren der Geschwindigkeit und / oder der Speichernutzung für bestimmte Abschnitte einer bestimmten Anwendung.

Anfänglich bedeutete diese Art von Anforderung, dass wir auf JNI - Java Native Interface zurückgreifen mussten. Dieser Ansatz ist zwar effektiv, hat jedoch seine Nachteile und wurde im Allgemeinen aufgrund einiger Probleme vermieden:

  • Entwickler müssen C / C ++ - Klebercode schreiben, um Java und nativen Code zu verbinden
  • Erfordert eine vollständige Kompilierungs- und Link-Toolchain, die für jedes Zielsystem verfügbar ist
  • Das Sammeln und Entfernen von Werten zur und von der JVM ist eine mühsame und fehleranfällige Aufgabe
  • Rechtliche und unterstützende Bedenken beim Mischen von Java und nativen Bibliotheken

JNA hat den größten Teil der mit der Verwendung von JNI verbundenen Komplexität gelöst. Insbesondere muss kein JNI-Code erstellt werden, um nativen Code in dynamischen Bibliotheken zu verwenden, was den gesamten Prozess erheblich vereinfacht.

Natürlich gibt es einige Kompromisse:

  • Wir können statische Bibliotheken nicht direkt verwenden
  • Langsamer im Vergleich zu handgefertigtem JNI-Code

Bei den meisten Anwendungen überwiegen jedoch die Vorteile der Einfachheit von JNA bei weitem die Nachteile. Daher kann man mit Recht sagen, dass JNA heute wahrscheinlich die beste verfügbare Wahl ist, um auf nativen Code von Java - oder einer anderen JVM-basierten Sprache - zuzugreifen, es sei denn, wir haben sehr spezielle Anforderungen.

3. JNA-Projekteinrichtung

Das erste, was wir tun müssen, um JNA zu verwenden, ist, seine Abhängigkeiten zur pom.xml unseres Projekts hinzuzufügen :

 net.java.dev.jna jna-platform 5.6.0  

Die neueste Version der jna-Plattform kann von Maven Central heruntergeladen werden.

4. Verwenden von JNA

Die Verwendung von JNA erfolgt in zwei Schritten:

  • Zunächst erstellen wir eine Java-Schnittstelle, die die Bibliotheksschnittstelle von JNA erweitert , um die Methoden und Typen zu beschreiben, die beim Aufrufen des nativen Zielcodes verwendet werden
  • Als Nächstes übergeben wir diese Schnittstelle an JNA, die eine konkrete Implementierung dieser Schnittstelle zurückgibt, mit der wir native Methoden aufrufen

4.1. Methoden aus der C-Standardbibliothek aufrufen

In unserem ersten Beispiel verwenden wir JNA, um die Cosh- Funktion aus der Standard-C-Bibliothek aufzurufen , die in den meisten Systemen verfügbar ist. Diese Methode verwendet ein doppeltes Argument und berechnet den hyperbolischen Kosinus. Das AC-Programm kann diese Funktion nur durch Einfügen des Header-Datei:

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

Erstellen wir die Java-Schnittstelle, die zum Aufrufen dieser Methode erforderlich ist:

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

Als Nächstes verwenden wir die Native- Klasse von JNA , um eine konkrete Implementierung dieser Schnittstelle zu erstellen, damit wir unsere API aufrufen können:

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

Der wirklich interessante Teil hier ist der Aufruf der load () -Methode . Es werden zwei Argumente benötigt: der Name der dynamischen Bibliothek und eine Java-Schnittstelle, die die von uns verwendeten Methoden beschreibt. Es gibt eine konkrete Implementierung dieser Schnittstelle zurück, sodass wir jede ihrer Methoden aufrufen können.

Dynamische Bibliotheksnamen sind normalerweise systemabhängig, und die C-Standardbibliothek ist keine Ausnahme: libc.so in den meisten Linux-basierten Systemen, msvcrt.dll in Windows. Aus diesem Grund haben wir die in JNA enthaltene Platform- Hilfsklasse verwendet, um zu überprüfen, auf welcher Plattform wir ausgeführt werden, und den richtigen Bibliotheksnamen auszuwählen.

Beachten Sie, dass wir die Erweiterung .so oder .dll nicht hinzufügen müssen , wie dies impliziert ist. Außerdem müssen wir für Linux-basierte Systeme nicht das Präfix "lib" angeben, das für gemeinsam genutzte Bibliotheken Standard ist.

Da sich dynamische Bibliotheken aus Java-Sicht wie Singletons verhalten, ist es üblich, ein INSTANCE- Feld als Teil der Schnittstellendeklaration zu deklarieren :

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

4.2. Grundlegende Typenzuordnung

In unserem ersten Beispiel verwendete die aufgerufene Methode nur primitive Typen als Argument und Rückgabewert. JNA behandelt diese Fälle automatisch und verwendet normalerweise ihre natürlichen Java-Gegenstücke bei der Zuordnung von C-Typen:

  • char => byte
  • kurz => kurz
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • lang lang => lang
  • 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)

Sobald wir diesen geschützten Modus aktiviert haben, erkennt JNA Fehler bei Zugriffsverletzungen, die normalerweise zu einem Absturz führen, und löst eine java.lang.Error- Ausnahme aus. Wir können überprüfen, ob dies mit einem Zeiger funktioniert, der mit einer ungültigen Adresse initialisiert wurde und versucht, einige Daten darauf zu schreiben:

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

Wie in der Dokumentation angegeben, sollte diese Funktion jedoch nur für Debugging- / Entwicklungszwecke verwendet werden.

5. Schlussfolgerung

In diesem Artikel haben wir gezeigt, wie Sie mit JNA im Vergleich zu JNI einfach auf nativen Code zugreifen können.

Wie üblich ist der gesamte Code auf GitHub verfügbar.