Utilisation de JNA pour accéder aux bibliothèques dynamiques natives

1. Vue d'ensemble

Dans ce tutoriel, nous verrons comment utiliser la bibliothèque Java Native Access (JNA en abrégé) pour accéder aux bibliothèques natives sans écrire de code JNI (Java Native Interface).

2. Pourquoi JNA?

Pendant de nombreuses années, Java et d'autres langages basés sur JVM ont, dans une large mesure, rempli sa devise «écrire une fois, courir partout». Cependant, nous devons parfois utiliser du code natif pour implémenter certaines fonctionnalités :

  • Réutilisation du code hérité écrit en C / C ++ ou tout autre langage capable de créer du code natif
  • Accès aux fonctionnalités spécifiques au système non disponibles dans l'environnement d'exécution Java standard
  • Optimiser la vitesse et / ou l'utilisation de la mémoire pour des sections spécifiques d'une application donnée.

Au départ, ce type d'exigence signifiait que nous devions recourir à JNI - Java Native Interface. Bien qu'efficace, cette approche a ses inconvénients et a généralement été évitée en raison de quelques problèmes:

  • Nécessite que les développeurs écrivent du «code glu» C / C ++ pour relier Java et le code natif
  • Nécessite une chaîne d'outils complète de compilation et de liaison disponible pour chaque système cible
  • Le marshaling et le démarshalling des valeurs vers et depuis la JVM est une tâche fastidieuse et sujette aux erreurs
  • Problèmes juridiques et d'assistance lors du mélange de bibliothèques Java et natives

JNA est venu pour résoudre la plupart de la complexité associée à l'utilisation de JNI. En particulier, il n'est pas nécessaire de créer un code JNI pour utiliser du code natif situé dans des bibliothèques dynamiques, ce qui rend l'ensemble du processus beaucoup plus facile.

Bien sûr, il y a quelques compromis:

  • Nous ne pouvons pas utiliser directement les bibliothèques statiques
  • Plus lent par rapport au code JNI fabriqué à la main

Pour la plupart des applications, cependant, les avantages de la simplicité de JNA l'emportent largement sur ces inconvénients. En tant que tel, il est juste de dire que, sauf si nous avons des exigences très spécifiques, JNA est aujourd'hui probablement le meilleur choix disponible pour accéder au code natif à partir de Java - ou de tout autre langage basé sur JVM, d'ailleurs.

3. Configuration du projet JNA

La première chose que nous devons faire pour utiliser JNA est d'ajouter ses dépendances au pom.xml de notre projet :

 net.java.dev.jna jna-platform 5.6.0  

La dernière version de jna-platform peut être téléchargée depuis Maven Central.

4. Utilisation de JNA

L'utilisation de JNA est un processus en deux étapes:

  • Tout d'abord, nous créons une interface Java qui étend l' interface de la bibliothèque JNA pour décrire les méthodes et les types utilisés lors de l'appel du code natif cible
  • Ensuite, nous passons cette interface à JNA qui retourne une implémentation concrète de cette interface que nous utilisons pour appeler des méthodes natives

4.1. Appel de méthodes à partir de la bibliothèque standard C

Pour notre premier exemple, utilisons JNA pour appeler la fonction cosh de la bibliothèque C standard, qui est disponible dans la plupart des systèmes. Cette méthode prend un argument double et calcule son cosinus hyperbolique. Le programme AC peut utiliser cette fonction simplement en incluant En tête de fichier:

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

Créons l'interface Java nécessaire pour appeler cette méthode:

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

Ensuite, nous utilisons la classe Native de JNA pour créer une implémentation concrète de cette interface afin de pouvoir appeler notre API:

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

La partie vraiment intéressante ici est l'appel à la méthode load () . Il prend deux arguments: le nom de la bibliothèque dynamique et une interface Java décrivant les méthodes que nous utiliserons. Il renvoie une implémentation concrète de cette interface, nous permettant d'appeler n'importe laquelle de ses méthodes.

Maintenant, les noms de bibliothèques dynamiques dépendent généralement du système, et la bibliothèque standard C ne fait pas exception: libc.so dans la plupart des systèmes Linux, mais msvcrt.dll sous Windows. C'est pourquoi nous avons utilisé la classe d'assistance Platform , incluse dans JNA, pour vérifier sur quelle plate-forme nous fonctionnons et sélectionner le nom de bibliothèque approprié.

Notez que nous n'avons pas besoin d'ajouter l' extension .so ou .dll , car elles sont implicites. De plus, pour les systèmes basés sur Linux, nous n'avons pas besoin de spécifier le préfixe «lib» qui est standard pour les bibliothèques partagées.

Puisque les bibliothèques dynamiques se comportent comme des singletons du point de vue Java, une pratique courante consiste à déclarer un champ INSTANCE dans le cadre de la déclaration d'interface:

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

4.2. Mappage des types de base

Dans notre exemple initial, la méthode appelée n'utilisait que des types primitifs à la fois comme argument et comme valeur de retour. JNA gère ces cas automatiquement, généralement en utilisant leurs équivalents Java naturels lors du mappage à partir de types C:

  • char => octet
  • court => court
  • wchar_t => caractère
  • int => int
  • long => com.sun.jna.NativeLong
  • long long => long
  • float => float
  • double => double
  • char * => Chaîne

Un mappage qui peut sembler étrange est celui utilisé pour le type long natif . En effet, en C / C ++, le type long peut représenter une valeur 32 ou 64 bits, selon que nous fonctionnons sur un système 32 ou 64 bits.

Pour résoudre ce problème, JNA fournit le type NativeLong , qui utilise le type approprié en fonction de l'architecture du système.

4.3. Structures et syndicats

Un autre scénario courant traite avec les API de code natif qui attendent un pointeur vers une struct ou union de type . Lors de la création de l'interface Java pour y accéder, l'argument ou la valeur de retour correspondant doit être un type Java qui étend respectivement Structure ou Union .

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)

Une fois que nous avons activé ce mode protégé, JNA détectera les erreurs de violation d'accès qui entraîneraient normalement un crash et lèvera une exception java.lang.Error . Nous pouvons vérifier que cela fonctionne en utilisant un pointeur initialisé avec une adresse non valide et en essayant d'y écrire des données:

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

Cependant, comme l'indique la documentation, cette fonctionnalité ne doit être utilisée qu'à des fins de débogage / développement.

5. Conclusion

Dans cet article, nous avons montré comment utiliser JNA pour accéder facilement au code natif par rapport à JNI.

Comme d'habitude, tout le code est disponible sur sur GitHub.