W3docs

API Java Foreign Function & Memory

Appelez du code natif et accédez à la mémoire hors tas en Java moderne avec l'API Foreign Function and Memory.

L'API Foreign Function and Memory (FFM) est la façon moderne et sûre de Java pour faire deux choses qui nécessitaient autrefois la fragile Java Native Interface (JNI) : appeler des fonctions écrites en C et dans d'autres langages natifs, et lire et écrire de la mémoire qui se trouve en dehors du tas Java. Elle est devenue une fonctionnalité finale dans JDK 22 et réside dans le package java.lang.foreign.

Ce chapitre explique comment fonctionne la mémoire hors tas dans FFM, comment une Arena contrôle sa durée de vie, comment les layouts décrivent les données natives, et comment appeler une fonction C depuis Java. À la fin, vous devriez comprendre quand FFM est le bon outil et comment ses pièces s'assemblent.

Pourquoi FFM Remplace JNI

Avant FFM, communiquer avec du code natif impliquait des glues JNI écrites à la main, des buffers d'octets manuels, et un risque constant de faire planter la JVM avec un pointeur errant. Un seul type mal correspondant ou un décalage d'un octet pouvait corrompre le tas ou provoquer une erreur de segmentation sur tout le processus — et parce que le crash se produisait dans le code natif, vous n'obteniez aucune trace de pile Java.

FFM remplace tout cela par une petite API à typage sûr construite autour de trois idées :

  • Une Arena contrôle la durée de vie de la mémoire : lorsqu'elle se ferme, tout ce qu'elle a alloué est libéré.
  • Un MemorySegment est une vue avec vérification des limites dans cette mémoire, de sorte qu'un accès hors limites lève une exception au lieu de corrompre la mémoire.
  • Un Linker construit un handle appelable vers une fonction native, en mappant les types C vers les types Java à l'avance.

Le résultat est que les erreurs apparaissent comme des exceptions Java au moment de la liaison, et non comme des crashes aléatoires ultérieurs. Le reste de ce chapitre passe en revue chaque élément tour à tour.

Mémoire Hors Tas avec Arena et MemorySegment

Un MemorySegment est une région contiguë de mémoire de taille connue. Contrairement à un tableau Java, il peut vivre hors du tas, donc le garbage collector ne le déplace jamais et il peut être transmis directement au code natif. Vous ne construisez jamais un segment directement — vous en demandez un à une Arena, et l'arena possède la durée de vie du segment.

Lorsque l'arena se ferme, chaque segment qu'elle a alloué est libéré d'un coup. Cela rend difficile l'écriture de fuites mémoire et de bugs d'utilisation après libération : toucher un segment après la fermeture de son arena vous donne une exception, pas un crash.

import java.lang.foreign.*;

try (Arena arena = Arena.ofConfined()) {
    // Allocate room for four ints, off the Java heap.
    MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT, 4);
    seg.setAtIndex(ValueLayout.JAVA_INT, 0, 100);
    int first = seg.getAtIndex(ValueLayout.JAVA_INT, 0);
    System.out.println(first); // 100
} // arena.close() frees the segment here

Chaque lecture et écriture passe par un ValueLayout, qui indique exactement combien d'octets occupe une valeur et comment elle est disposée. C'est ce qui maintient chaque accès avec vérification des limites et à typage sûr.

Choisir une Arena

Arena est le gestionnaire de durée de vie, et la méthode factory que vous choisissez décide qui peut toucher la mémoire et quand elle est libérée. Choisir la bonne est la principale décision de sécurité dans le code FFM.

ArenaDurée de vieAccès aux threads
Arena.ofConfined()Jusqu'à close()Seulement le thread créateur
Arena.ofShared()Jusqu'à close()N'importe quel thread
Arena.ofAuto()Jusqu'à ce que le GC le collecteN'importe quel thread
Arena.global()Tout le programmeN'importe quel thread

Utilisez ofConfined() pour le cas courant : mémoire de courte durée utilisée par un seul thread et libérée de manière déterministe avec try-with-resources. Utilisez ofShared() uniquement lorsque plusieurs threads doivent lire le même segment, et ofAuto() lorsque vous ne pouvez pas facilement marquer la fin de la durée de vie. Si votre code utilise des threads virtuels, préférez ofShared() ou ofAuto(), car une arena confinée est liée à un thread porteur.

Décrire les Layouts

Un ValueLayout décrit une valeur primitive unique ; un MemoryLayout peut décrire des structs et des tableaux entiers. Les layouts vous permettent de calculer des décalages et des tailles sans coder en dur des nombres magiques, ce qui rend l'accès aux structs natifs lisible.

import java.lang.foreign.*;
import static java.lang.foreign.ValueLayout.*;

// A C struct:  struct Point { int x; int y; };
MemoryLayout point = MemoryLayout.structLayout(
    JAVA_INT.withName("x"),
    JAVA_INT.withName("y")
);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment p = arena.allocate(point);
    var xHandle = point.varHandle(MemoryLayout.PathElement.groupElement("x"));
    var yHandle = point.varHandle(MemoryLayout.PathElement.groupElement("y"));
    xHandle.set(p, 0L, 3);
    yHandle.set(p, 0L, 4);
    System.out.println(xHandle.get(p, 0L) + ", " + yHandle.get(p, 0L)); // 3, 4
}

Les champs nommés et les accesseurs PathElement signifient que vous décrivez le struct une fois et laissez l'API calculer les décalages d'octets pour vous.

Appeler des Fonctions Natives avec Linker

La fonctionnalité phare de FFM est le downcall : invoquer une fonction C depuis Java. Vous obtenez le Linker de la plateforme, recherchez l'adresse de la fonction avec un SymbolLookup, décrivez sa signature avec un FunctionDescriptor, et recevez un MethodHandle que vous pouvez invoquer comme n'importe quelle méthode Java.

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

Linker linker = Linker.nativeLinker();
// strlen lives in the standard C library, found via the default lookup.
MethodHandle strlen = linker.downcallHandle(
    linker.defaultLookup().find("strlen").orElseThrow(),
    // size_t strlen(const char *s);
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment cString = arena.allocateUtf8String("hello");
    long len = (long) strlen.invoke(cString); // 5
}

Le FunctionDescriptor mappe les types C vers les porteurs Java : un pointeur C devient ValueLayout.ADDRESS, un size_t C correspond à JAVA_LONG, un int C à JAVA_INT. Obtenez le bon mappage et l'appel est à typage sûr ; obtenez-le mal et vous le découvrez au moment de la liaison, pas comme un crash aléatoire. Parce que les appels natifs échappent au filet de sécurité de la JVM, FFM est une opération restreinte — le module qui l'utilise doit avoir accès accordé avec le flag --enable-native-access.

Un Exemple Complet et Exécutable

L'API java.lang.foreign est une fonctionnalité en préversion avant JDK 22, donc le programme ci-dessous exécute les deux mêmes idées — mémoire hors tas et gestion de chaînes de style natif — en utilisant uniquement les classes JDK toujours disponibles que FFM a été conçu pour remplacer. Un ByteBuffer direct est de la mémoire allouée en dehors du tas Java, tout comme un MemorySegment ; lire des valeurs typées à des décalages d'octets reflète un accès ValueLayout ; et scanner les octets jusqu'à un terminateur zéro est exactement ce que fait strlen de C.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • isDirect = true confirme que le buffer est alloué hors du tas Java — la même propriété qui permet à un MemorySegment d'être passé en toute sécurité au code natif sans que le GC le déplace.
  • Écrire (i + 1) * 10 à chaque décalage de 4 octets et le relire donne 10, 20, 30, 40 avec sum = 100, montrant que la mémoire hors tas est un vrai stockage indexable et typé, tout comme un MemorySegment.
  • byteSize = 16 représente quatre entiers de 4 octets — adresser par décalage d'octets explicite est exactement la façon dont un ValueLayout calcule les positions dans la vraie API FFM.
  • La cString construite à la main se termine par un octet zéro, donc le scan de style strlen s'arrête là : strlen of the C string = 16 correspond à Java String.length() = 16, prouvant que le terminateur null marque la fin comme C le préconise.
  • Aucun buffer n'est libéré manuellement — les buffers directs sont récupérés lorsqu'ils ne sont plus accessibles, reflétant Arena.ofAuto(), tandis que la vraie ofConfined() arena FFM libèrerait de manière déterministe à close().

Quand Utiliser FFM

FFM est un outil spécialisé, pas un outil de tous les jours. Utilisez-le lorsque vous avez vraiment besoin d'interopérabilité native ou de mémoire hors tas :

  • Appeler une bibliothèque native existante — un codec d'image C, un pilote de base de données, un SDK matériel — sans écrire de glue JNI.
  • Partager de grands buffers avec du code natif où la copie sur le tas Java serait gaspilleuse, comme dans les pipelines graphiques ou audio.
  • Travailler avec de très grands ensembles de données hors tas qui ne devraient pas solliciter le garbage collector.

Pour les travaux ordinaires sur les fichiers et les buffers, restez avec des API de plus haut niveau comme Java NIO ; elles sont plus simples et sûres par défaut. Et rappelez-vous que FFM est une opération restreinte : parce que les appels natifs échappent aux garanties de sécurité de la JVM, vous devez lancer avec --enable-native-access ou vous obtiendrez un avertissement ou une erreur à l'exécution.

Pratique

Pratique
Dans l'API FFM, quel est le rôle d'une Arena ?
Dans l'API FFM, quel est le rôle d'une Arena ?
Was this page helpful?