Java Foreign Function & Memory API
Chiama codice nativo e accedi alla memoria off-heap in Java moderno con la Foreign Function and Memory API.
La Foreign Function and Memory (FFM) API è il modo moderno e sicuro di Java per fare due cose che un tempo richiedevano la fragile Java Native Interface (JNI): chiamare funzioni scritte in C e altri linguaggi nativi, e leggere e scrivere memoria che vive al di fuori dell'heap Java. È diventata una funzionalità definitiva in JDK 22 e si trova nel pacchetto java.lang.foreign.
Questo capitolo spiega come funziona la memoria off-heap in FFM, come un Arena controlla la sua durata, come i layout descrivono i dati nativi e come chiamare una funzione C da Java. Al termine dovresti capire quando FFM è lo strumento giusto e come le sue parti si incastrano.
Perché FFM Sostituisce JNI
Prima di FFM, comunicare con il codice nativo significava scrivere manualmente il codice di collegamento JNI, gestire buffer di byte a mano e rischiare costantemente di mandare in crash la JVM con un puntatore errato. Un singolo tipo non corrispondente o un offset sbagliato di uno poteva corrompere l'heap o causare un segfault nell'intero processo — e poiché il crash avveniva nel codice nativo, non si otteneva alcuno stack trace Java.
FFM sostituisce tutto ciò con una piccola API type-safe costruita attorno a tre idee:
- Un
Arenacontrolla la durata della memoria: quando si chiude, tutto ciò che ha allocato viene liberato. - Un
MemorySegmentè una vista con controllo dei limiti in quella memoria, quindi un accesso fuori intervallo genera un'eccezione invece di corrompere la memoria. - Un
Linkercostruisce un handle chiamabile a una funzione nativa, mappando i tipi C ai tipi Java in anticipo.
Il risultato è che gli errori emergono come eccezioni Java al momento del collegamento, non come crash casuali in seguito. Il resto di questo capitolo esamina ciascun componente singolarmente.
Memoria Off-Heap con Arena e MemorySegment
Un MemorySegment è una regione contigua di memoria con una dimensione nota. A differenza di un array Java, può vivere fuori dall'heap, quindi il garbage collector non lo sposta mai e può essere passato direttamente al codice nativo. Non si costruisce mai un segmento direttamente — si chiede a un Arena di crearne uno, e l'arena possiede la durata del segmento.
Quando l'arena si chiude, ogni segmento che ha allocato viene liberato contemporaneamente. Questo rende difficile scrivere bug di perdita di memoria e use-after-free: toccare un segmento dopo la chiusura dell'arena genera un'eccezione, non 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 hereOgni lettura e scrittura avviene attraverso un ValueLayout, che specifica esattamente quanti byte occupa un valore e come è disposto. Questo è ciò che mantiene ogni accesso con controllo dei limiti e type-safe.
Scegliere un Arena
Arena è il gestore della durata, e il metodo factory che si sceglie decide chi può accedere alla memoria e quando viene rilasciata. Scegliere quello giusto è la principale decisione di sicurezza nel codice FFM.
| Arena | Durata | Accesso ai thread |
|---|---|---|
Arena.ofConfined() | Fino a close() | Solo il thread che l'ha creata |
Arena.ofShared() | Fino a close() | Qualsiasi thread |
Arena.ofAuto() | Finché il GC non la raccoglie | Qualsiasi thread |
Arena.global() | L'intero programma | Qualsiasi thread |
Usa ofConfined() per il caso comune: memoria di breve durata utilizzata da un solo thread e liberata in modo deterministico con try-with-resources. Ricorri a ofShared() solo quando più thread devono leggere lo stesso segmento, e a ofAuto() quando non riesci facilmente a contrassegnare la fine della durata. Se il tuo codice usa thread virtuali, preferisci ofShared() o ofAuto(), poiché un arena confinato è legato a un singolo thread carrier.
Descrivere i Layout
Un ValueLayout descrive un singolo valore primitivo; un MemoryLayout può descrivere interi struct e array. I layout permettono di calcolare offset e dimensioni senza inserire numeri magici nel codice, il che mantiene leggibile l'accesso agli struct nativi.
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
}I campi con nome e gli accessor PathElement significano che si descrive lo struct una volta sola e si lascia che l'API calcoli gli offset in byte.
Chiamare Funzioni Native con Linker
La funzionalità principale di FFM è il downcall: invocare una funzione C da Java. Si ottiene il Linker della piattaforma, si cerca l'indirizzo della funzione con un SymbolLookup, si descrive la sua firma con un FunctionDescriptor e si riceve un MethodHandle che può essere invocato come qualsiasi metodo 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
}Il FunctionDescriptor mappa i tipi C ai carrier Java: un puntatore C diventa ValueLayout.ADDRESS, un size_t C si mappa su JAVA_LONG, un int C su JAVA_INT. Ottenere la mappatura corretta rende la chiamata type-safe; sbagliarla si scopre al momento del collegamento, non come un crash casuale. Poiché le chiamate native sfuggono alla rete di sicurezza della JVM, FFM è un'operazione riservata — il modulo che la usa deve ottenere l'accesso con il flag --enable-native-access.
Un Esempio Completo ed Eseguibile
L'API java.lang.foreign è una funzionalità in anteprima prima di JDK 22, quindi il programma seguente esegue le stesse due idee — memoria off-heap e gestione di stringhe in stile nativo — usando solo le classi JDK sempre disponibili che FFM è stata progettata per sostituire. Un ByteBuffer diretto è memoria allocata al di fuori dell'heap Java, proprio come un MemorySegment; la lettura di valori tipizzati a offset di byte rispecchia un accesso con ValueLayout; e la scansione dei byte fino a un terminatore zero è esattamente ciò che fa strlen di C.
Cosa trarre dall'esecuzione:
isDirect = trueconferma che il buffer è allocato al di fuori dell'heap Java — la stessa proprietà che consente a unMemorySegmentdi essere passato in modo sicuro al codice nativo senza che il GC lo sposti.- Scrivere
(i + 1) * 10a ogni offset di 4 byte e rileggerlo produce10, 20, 30, 40consum = 100, dimostrando che la memoria off-heap è un archivio reale, indicizzabile e tipizzato proprio come unMemorySegment. byteSize = 16sono quattro int da 4 byte — l'indirizzamento tramite offset di byte esplicito è esattamente il modo in cui unValueLayoutcalcola le posizioni nella vera API FFM.- La
cStringcostruita a mano termina con un byte zero, quindi la scansione in stile strlen si ferma lì:strlen of the C string = 16corrisponde aJava String.length() = 16, dimostrando che il terminatore null segna la fine nel modo che C si aspetta. - Nessun buffer viene liberato a mano — i buffer diretti vengono recuperati quando non sono più raggiungibili, rispecchiando
Arena.ofAuto(), mentre il veroofConfined()di FFM libererebbe in modo deterministico aclose().
Quando Usare FFM
FFM è uno strumento specialistico, non uno per uso quotidiano. Usalo quando hai davvero bisogno di interoperabilità nativa o di memoria off-heap:
- Chiamare una libreria nativa esistente — un codec di immagini in C, un driver di database, un SDK hardware — senza scrivere codice di collegamento JNI.
- Condividere grandi buffer con il codice nativo dove copiare sull'heap Java sarebbe dispendioso, come nelle pipeline grafiche o audio.
- Lavorare con set di dati off-heap molto grandi che non dovrebbero esercitare pressione sul garbage collector.
Per il lavoro ordinario con file e buffer, rimani con API di livello superiore come Java NIO; sono più semplici e sicure per impostazione predefinita. E ricorda che FFM è un'operazione riservata: poiché le chiamate native sfuggono alle garanzie di sicurezza della JVM, devi avviare con --enable-native-access o riceverai un avviso o un errore in fase di esecuzione.