Aller au contenu principal
~/GiwiSoft
LvH8Qg

Appelez un programme C depuis java avec JNA

Giwi 4 min de lecture Java

Et oui, on peut appeler nativement une lib C, C++ Rust ou Go directement en Java grâce à JNA.

En fait c’est facile.

D’abord, il faut une lib en C. Par exemple en voici une :

helloworld.h

char *hello(char* ch);

helloworld.c

#include "helloworld.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

char* hello(char* ch) {
char prefix[] = "Hello ";
char *res;
res = malloc(strlen(prefix) + strlen(ch));
strcpy(res, prefix);
strcat(res,ch);
return res;
}

Hey, mais, il y a un malloc sans free, ça craint. Bon, pas de soucis, on verra plus loi comment faire un free après l’appel.

Ok, maintenant, on compile :

$ gcc -c helloworld.c 
$ gcc -shared -dynamiclib helloworld.o -o ./helloworld.so

Maintenant, la partie Java. Il faut avoir la lib JNA dans son classpath, bien évidement.

CHelloWorld.java

package io.warp10.ext.test;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

public interface CHelloWorld extends Library {
// On charge la lib
CHelloWorld INSTANCE = Native.load("helloworld.so", CHelloWorld.class);
// on décrit l'interface utilisée
Pointer hello(String g);
// Youpi, on pourra appeler free
void free(Pointer p); // JNA le fournit (et pas que ça)
}

Test.java

public class Test {
public static void main(String[] args) {
Pointer ptr = null;
try {
// on appelle notre fonction C
ptr = CHelloWorld.INSTANCE.hello(args[0]);
// on parse le résultat
System.out.println(ptr.getString(0));
} catch (Exception e) {
throw e;
} finally {
// On libère la mémoire
if (null != ptr) {
CHelloWorld.INSTANCE.free(ptr);
}
}
}
}

On compile :

$ javac -cp ./jna-5.10.0.jar ./*.java

On exécute :

$ java -cp /opt/jna-5.10.0.jar:./* mon.package.Test Kitty
Hello Kitty

Et voilà.

Gestion mémoire avec try-with-resources

La version ci-dessus fonctionne, mais elle est fragile : si une exception survient, le finally gère le free. Avec JNA, on peut faire mieux en utilisant un AutoCloseable :

public class MonCString implements AutoCloseable {
private final Pointer ptr;

public MonCString(Pointer ptr) {
this.ptr = ptr;
}

public String getValue() {
return ptr != null ? ptr.getString(0) : null;
}

@Override
public void close() {
if (ptr != null) {
CHelloWorld.INSTANCE.free(ptr);
}
}
}
public class Test {
public static void main(String[] args) {
try (MonCString result = new MonCString(
CHelloWorld.INSTANCE.hello(args[0]))) {
System.out.println(result.getValue());
}
}
}

Plus de finally explicite, plus de risque de fuite mémoire.

Structures C complexes avec com.sun.jna.Structure

JNA permet aussi de mapper des structures C complètes. Imaginons une fonction C qui renvoie une struct personne :

typedef struct {
char nom[50];
int age;
double taille;
} personne_t;

personne_t creer_personne(const char* nom, int age, double taille);

L’équivalent Java :

import com.sun.jna.Structure;

@Structure.FieldOrder({"nom", "age", "taille"})
public class Personne extends Structure {
public byte[] nom = new byte[50];
public int age;
public double taille;

public String getNom() {
return Native.toString(nom);
}
}

public interface CLib extends Library {
CLib INSTANCE = Native.load("libpersonne", CLib.class);
Personne creer_personne(String nom, int age, double taille);
}

Tableaux et callbacks

Passer des tableaux depuis Java vers C :

double somme_tableau(double* tab, int taille);
double[] valeurs = {1.5, 2.5, 3.0};
double somme = CLib.INSTANCE.somme_tableau(valeurs, valeurs.length);

Et les callbacks C depuis Java :

public interface CallbackC extends Callback {
void invoke(int valeur);
}

CLib.INSTANCE.inscrit_callback((CallbackC) valeur ->
System.out.println("Callback reçu : " + valeur));

JNA vs JNI vs Panama FFI

Critère JNA JNI Panama FFI
Complexité Aucune binding Écrire du JNI C Assez simple
Performance Bonne (moyennant direct mapping) Excellente Très bonne
Maintenance Facile Lourde Facile
Infos de type Réflexion Header manuel Descripteurs

JNA est parfait pour du prototypage, de l’intégration rapide ou des appels ponctuels.

Project Panama (FFI moderne)

Depuis Java 22, le Foreign Function & Memory API (Panama) est proposé en standard (prévisualisation). C’est le successeur moderne de JNA/JNI, intégré directement dans le JDK :

import java.lang.foreign.*;

MemorySegment hello = Linker.nativeLinker().downcallHandle(
SymbolLookup.loaderLookup().find("hello").get(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)
).invoke(segmentFromString("Kitty"));

System.out.println(hello.getString(0));

Panama offre des performances proches de JNI sans le coût de maintenance. Pour les nouveaux projets, c’est la voie à privilégier - mais JNA reste très valable pour sa maturité et sa simplicité.