API de Función Foránea y Memoria en Java
Llama a código nativo y accede a memoria fuera del heap en Java moderno con la API de Función Foránea y Memoria.
La API de Función Foránea y Memoria (FFM) es la forma moderna y segura de Java para hacer dos cosas que antes requerían la frágil Interfaz Nativa de Java (JNI): llamar a funciones escritas en C y otros lenguajes nativos, y leer y escribir memoria que vive fuera del heap de Java. Se convirtió en una característica final en JDK 22 y reside en el paquete java.lang.foreign.
Este capítulo cubre cómo funciona la memoria fuera del heap en FFM, cómo un Arena controla su ciclo de vida, cómo los layouts describen datos nativos y cómo llamar a una función de C desde Java. Al final deberías entender cuándo FFM es la herramienta adecuada y cómo encajan sus piezas.
Por Qué FFM Reemplaza a JNI
Antes de FFM, hablar con código nativo implicaba código JNI escrito a mano, buffers de bytes manuales y un riesgo constante de bloquear la JVM con un puntero erróneo. Un solo tipo mal coincidente o un desplazamiento fuera de rango podía corromper el heap o provocar un segfault en todo el proceso — y como el fallo ocurría en código nativo, no obtenías ningún stack trace de Java.
FFM reemplaza todo eso con una API pequeña y con seguridad de tipos construida alrededor de tres ideas:
- Un
Arenacontrola el ciclo de vida de la memoria: cuando se cierra, todo lo que asignó queda liberado. - Un
MemorySegmentes una vista con comprobación de límites de esa memoria, por lo que el acceso fuera de rango lanza una excepción en lugar de corromper la memoria. - Un
Linkerconstruye un manejador invocable para una función nativa, mapeando tipos de C a tipos de Java de antemano.
El resultado es que los errores aparecen como excepciones de Java en tiempo de enlace, no como fallos aleatorios más adelante. El resto de este capítulo recorre cada pieza por turno.
Memoria Fuera del Heap con Arena y MemorySegment
Un MemorySegment es una región contigua de memoria con un tamaño conocido. A diferencia de un array de Java, puede vivir fuera del heap, por lo que el recolector de basura nunca lo mueve y puede pasarse directamente a código nativo. Nunca construyes un segmento directamente — se lo pides a un Arena, y el arena posee el ciclo de vida del segmento.
Cuando el arena se cierra, cada segmento que asignó queda liberado de una vez. Esto hace que las fugas y los errores de uso posterior a la liberación sean difíciles de escribir: toca un segmento después de que su arena se cierre y obtendrás una excepción, no un fallo.
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 hereCada lectura y escritura pasa por un ValueLayout, que indica exactamente cuántos bytes ocupa un valor y cómo está dispuesto. Eso es lo que mantiene cada acceso con comprobación de límites y con seguridad de tipos.
Elegir un Arena
Arena es el gestor del ciclo de vida, y el método de fábrica que elijas decide quién puede tocar la memoria y cuándo se libera. Elegir el correcto es la principal decisión de seguridad en el código FFM.
| Arena | Ciclo de vida | Acceso de hilos |
|---|---|---|
Arena.ofConfined() | Hasta close() | Solo el hilo creador |
Arena.ofShared() | Hasta close() | Cualquier hilo |
Arena.ofAuto() | Hasta que el GC lo recolecta | Cualquier hilo |
Arena.global() | Todo el programa | Cualquier hilo |
Usa ofConfined() para el caso común: memoria de corta duración usada por un hilo y liberada de forma determinista con try-with-resources. Recurre a ofShared() solo cuando varios hilos deben leer el mismo segmento, y a ofAuto() cuando no puedes marcar fácilmente el final del ciclo de vida. Si tu código usa hilos virtuales, prefiere ofShared() o ofAuto(), ya que un arena confinado está ligado a un hilo portador.
Describir Layouts
Un ValueLayout describe un único valor primitivo; un MemoryLayout puede describir structs y arrays completos. Los layouts te permiten calcular desplazamientos y tamaños sin codificar números mágicos, lo que mantiene el acceso a structs nativos legible.
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
}Los campos con nombre y los accesores PathElement significan que describes el struct una vez y dejas que la API calcule los desplazamientos de bytes por ti.
Llamar a Funciones Nativas con Linker
La característica principal de FFM es el downcall: invocar una función de C desde Java. Obtienes el Linker de la plataforma, buscas la dirección de la función con un SymbolLookup, describes su firma con un FunctionDescriptor y recibes un MethodHandle que puedes invocar como cualquier método de 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
}El FunctionDescriptor mapea tipos de C a portadores de Java: un puntero de C se convierte en ValueLayout.ADDRESS, un size_t de C se mapea a JAVA_LONG, un int de C a JAVA_INT. Obtén el mapeo correcto y la llamada es con seguridad de tipos; equivócate y lo descubres en tiempo de enlace, no como un fallo aleatorio. Dado que las llamadas nativas escapan de la red de seguridad de la JVM, FFM es una operación restringida — el módulo que la usa debe tener acceso concedido con el indicador --enable-native-access.
Un Ejemplo Completo y Ejecutable
La API java.lang.foreign es una característica de vista previa antes de JDK 22, por lo que el programa siguiente ejecuta las mismas dos ideas — memoria fuera del heap y manejo de strings al estilo nativo — usando solo las clases JDK siempre activas que FFM fue diseñado para reemplazar. Un ByteBuffer directo es memoria asignada fuera del heap de Java, igual que un MemorySegment; leer valores tipados en desplazamientos de bytes refleja un acceso con ValueLayout; y escanear bytes hasta un terminador cero es exactamente lo que hace strlen de C.
Lo que hay que observar de la ejecución:
isDirect = trueconfirma que el buffer está asignado fuera del heap de Java — la misma propiedad que permite que unMemorySegmentse pase de forma segura a código nativo sin que el GC lo reubique.- Escribir
(i + 1) * 10en cada desplazamiento de 4 bytes y leerlo de vuelta produce10, 20, 30, 40consum = 100, lo que muestra que la memoria fuera del heap es almacenamiento real, indexable y tipado igual que unMemorySegment. byteSize = 16son cuatro enteros de 4 bytes — el direccionamiento por desplazamiento de bytes explícito es exactamente cómo unValueLayoutcalcula posiciones en la API FFM real.- El
cStringconstruido a mano termina en un byte cero, por lo que el escaneo al estilo strlen se detiene ahí:strlen of the C string = 16coincide conJava String.length() = 16, lo que prueba que el terminador nulo marca el final como espera C. - Ningún buffer se libera a mano — los buffers directos se recuperan cuando son inalcanzables, reflejando
Arena.ofAuto(), mientras que el arena realofConfined()de FFM liberaría de forma determinista enclose().
Cuándo Usar FFM
FFM es una herramienta especializada, no una de uso cotidiano. Recurre a ella cuando realmente necesites interoperabilidad nativa o memoria fuera del heap:
- Llamar a una biblioteca nativa existente — un codec de imágenes en C, un controlador de base de datos, un SDK de hardware — sin escribir código JNI.
- Compartir buffers grandes con código nativo donde copiar al heap de Java sería un desperdicio, como en pipelines de gráficos o audio.
- Trabajar con conjuntos de datos muy grandes fuera del heap que no deberían presionar al recolector de basura.
Para el trabajo ordinario con archivos y buffers, quédate con APIs de mayor nivel como Java NIO; son más simples y seguras por defecto. Y recuerda que FFM es una operación restringida: dado que las llamadas nativas escapan de las garantías de seguridad de la JVM, debes lanzar con --enable-native-access o recibirás una advertencia o error en tiempo de ejecución.