The Foreign Function & Memory API (FFM API) is a Java incubator API that enables Java programs to interoperate with code and data outside the Java runtime. It provides a pure-Java replacement for JNI (Java Native Interface) with better performance, safety, and ease of use.
Understanding FFM API Components
Key Features:
- Memory Access: Safe allocation and access to native memory
- Foreign Functions: Call native functions from Java code
- Type Safety: Memory access validation at runtime
- Resource Management: Automatic resource cleanup
- Vector Support: SIMD operations via Vector API integration
Project Setup and Dependencies
<properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- JUnit for testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <compilerArgs> <arg>--enable-preview</arg> <arg>--add-modules=jdk.incubator.foreign</arg> </compilerArgs> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.1.2</version> <configuration> <argLine>--enable-preview --add-modules=jdk.incubator.foreign</argLine> </configuration> </plugin> </plugins> </build>
Core Memory Management
package com.example.ffm;
import jdk.incubator.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.file.Path;
/**
* Basic Memory Allocation and Management with FFM API
*/
public class MemoryManagement {
/**
* Demonstrates basic memory allocation and access
*/
public static void basicMemoryAllocation() {
// Using Arena for automatic resource management
try (Arena arena = Arena.ofConfined()) {
// Allocate a segment of 100 bytes
MemorySegment segment = arena.allocate(100);
System.out.println("Allocated segment: " + segment.byteSize() + " bytes");
// Write data to the segment
segment.set(ValueLayout.JAVA_BYTE, 0, (byte) 65); // 'A'
segment.set(ValueLayout.JAVA_BYTE, 1, (byte) 66); // 'B'
segment.set(ValueLayout.JAVA_BYTE, 2, (byte) 67); // 'C'
// Read data back
byte firstByte = segment.get(ValueLayout.JAVA_BYTE, 0);
System.out.println("First byte: " + (char) firstByte);
// Bulk operations
byte[] data = {68, 69, 70}; // D, E, F
MemorySegment.copy(data, 0, segment, ValueLayout.JAVA_BYTE, 3, 3);
// Print the string
byte[] result = new byte[6];
MemorySegment.copy(segment, ValueLayout.JAVA_BYTE, 0, result, 0, 6);
System.out.println("String: " + new String(result));
} // Arena automatically closed here, memory freed
}
/**
* Working with structured data (like C structs)
*/
public static void structuredMemoryLayout() {
try (Arena arena = Arena.ofConfined()) {
// Define a struct layout: { int id; double value; boolean flag; }
GroupLayout structLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("id"),
ValueLayout.JAVA_DOUBLE.withName("value"),
ValueLayout.JAVA_BOOLEAN.withName("flag")
);
// Allocate array of 5 structs
SequenceLayout arrayLayout = MemoryLayout.sequenceLayout(5, structLayout);
MemorySegment arraySegment = arena.allocate(arrayLayout);
// Calculate offsets
long idOffset = structLayout.byteOffset(MemoryLayout.PathElement.groupElement("id"));
long valueOffset = structLayout.byteOffset(MemoryLayout.PathElement.groupElement("value"));
long flagOffset = structLayout.byteOffset(MemoryLayout.PathElement.groupElement("flag"));
long structSize = structLayout.byteSize();
// Initialize structs
for (int i = 0; i < 5; i++) {
long baseOffset = i * structSize;
arraySegment.set(ValueLayout.JAVA_INT, baseOffset + idOffset, i * 10);
arraySegment.set(ValueLayout.JAVA_DOUBLE, baseOffset + valueOffset, i * 1.5);
arraySegment.set(ValueLayout.JAVA_BOOLEAN, baseOffset + flagOffset, i % 2 == 0);
}
// Read and print structs
for (int i = 0; i < 5; i++) {
long baseOffset = i * structSize;
int id = arraySegment.get(ValueLayout.JAVA_INT, baseOffset + idOffset);
double value = arraySegment.get(ValueLayout.JAVA_DOUBLE, baseOffset + valueOffset);
boolean flag = arraySegment.get(ValueLayout.JAVA_BOOLEAN, baseOffset + flagOffset);
System.out.printf("Struct[%d]: id=%d, value=%.2f, flag=%b%n", i, id, value, flag);
}
}
}
/**
* Memory segment slicing and views
*/
public static void memorySlicing() {
try (Arena arena = Arena.ofConfined()) {
// Allocate a large segment
MemorySegment original = arena.allocate(1024);
// Fill with pattern
for (int i = 0; i < 1024; i++) {
original.set(ValueLayout.JAVA_BYTE, i, (byte) (i % 256));
}
// Create slices (views)
MemorySegment slice1 = original.asSlice(0, 100); // First 100 bytes
MemorySegment slice2 = original.asSlice(100, 200); // Next 200 bytes
MemorySegment slice3 = original.asSlice(300); // From offset 300 to end
System.out.println("Original size: " + original.byteSize());
System.out.println("Slice1 size: " + slice1.byteSize());
System.out.println("Slice2 size: " + slice2.byteSize());
System.out.println("Slice3 size: " + slice3.byteSize());
// Verify slices point to same memory
slice1.set(ValueLayout.JAVA_BYTE, 0, (byte) 255);
byte valueFromOriginal = original.get(ValueLayout.JAVA_BYTE, 0);
System.out.println("Value from original after modifying slice: " + valueFromOriginal);
}
}
/**
* Different arena types and their use cases
*/
public static void arenaTypes() {
// Confined Arena - single thread, automatic cleanup
try (Arena confined = Arena.ofConfined()) {
MemorySegment segment1 = confined.allocate(100);
System.out.println("Confined arena allocated: " + segment1.byteSize());
}
// Shared Arena - multiple threads, manual control
Arena shared = Arena.ofShared();
MemorySegment segment2 = shared.allocate(200);
System.out.println("Shared arena allocated: " + segment2.byteSize());
shared.close(); // Manual cleanup required
// Global Arena - JVM lifetime
Arena global = Arena.global();
MemorySegment segment3 = global.allocate(50);
System.out.println("Global arena allocated: " + segment3.byteSize());
// Global arena doesn't need closing
}
}
Foreign Function Interface
package com.example.ffm;
import jdk.incubator.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.file.Path;
/**
* Foreign Function Interface examples
*/
public class ForeignFunctions {
/**
* Basic C standard library function calls
*/
public static void callStandardLibraryFunctions() throws Throwable {
// Get linker for standard C library
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
try (Arena arena = Arena.ofConfined()) {
// Call strlen function
MethodHandle strlen = linker.downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
// Allocate and initialize a string
MemorySegment string = arena.allocateFrom("Hello, FFI!");
// Call strlen
long length = (long) strlen.invoke(string);
System.out.println("String length: " + length);
// Call printf
MethodHandle printf = linker.downcallHandle(
stdlib.find("printf").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)
);
MemorySegment format = arena.allocateFrom("Formatted output: %s (length: %d)\n");
printf.invoke(format, string, length);
}
}
/**
* Calling mathematical functions
*/
public static void callMathFunctions() throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
try (Arena arena = Arena.ofConfined()) {
// Call pow function
MethodHandle pow = linker.downcallHandle(
stdlib.find("pow").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE)
);
double result = (double) pow.invoke(2.0, 3.0); // 2^3
System.out.println("pow(2, 3) = " + result);
// Call sqrt function
MethodHandle sqrt = linker.downcallHandle(
stdlib.find("sqrt").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE)
);
double sqrtResult = (double) sqrt.invoke(16.0);
System.out.println("sqrt(16) = " + sqrtResult);
}
}
/**
* Working with custom native libraries
*/
public static void callCustomLibrary() throws Throwable {
Linker linker = Linker.nativeLinker();
// Load custom library (example - you'd need to create this library)
System.loadLibrary("mylib");
SymbolLookup mylib = SymbolLookup.loaderLookup();
try (Arena arena = Arena.ofConfined()) {
// Example: Call a function that adds two integers
MethodHandle addNumbers = linker.downcallHandle(
mylib.find("add_numbers").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)
);
int sum = (int) addNumbers.invoke(15, 25);
System.out.println("15 + 25 = " + sum);
}
}
/**
* String manipulation with C functions
*/
public static void stringManipulation() throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
try (Arena arena = Arena.ofConfined()) {
// Call strcpy function
MethodHandle strcpy = linker.downcallHandle(
stdlib.find("strcpy").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS,
ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
// Allocate source and destination buffers
MemorySegment source = arena.allocateFrom("Source string");
MemorySegment dest = arena.allocate(100); // Ensure enough space
// Copy string
strcpy.invoke(dest, source);
// Verify copy
String copiedString = dest.getUtf8String(0);
System.out.println("Copied string: " + copiedString);
}
}
}
Advanced Memory Layouts
package com.example.ffm;
import jdk.incubator.foreign.*;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.StructLayout;
import java.lang.invoke.VarHandle;
import java.util.List;
/**
* Advanced memory layout patterns
*/
public class AdvancedLayouts {
/**
* Complex struct with nested layouts
*/
public static void complexStructLayout() {
try (Arena arena = Arena.ofConfined()) {
// Define a Point struct
GroupLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_DOUBLE.withName("x"),
ValueLayout.JAVA_DOUBLE.withName("y")
);
// Define a Rectangle struct containing two Points
GroupLayout rectLayout = MemoryLayout.structLayout(
pointLayout.withName("topLeft"),
pointLayout.withName("bottomRight"),
ValueLayout.JAVA_INT.withName("color")
);
// Allocate and initialize
MemorySegment rect = arena.allocate(rectLayout);
// Calculate offsets using VarHandle for type safety
VarHandle topLeftXHandle = rectLayout.varHandle(
MemoryLayout.PathElement.groupElement("topLeft"),
MemoryLayout.PathElement.groupElement("x")
);
VarHandle topLeftYHandle = rectLayout.varHandle(
MemoryLayout.PathElement.groupElement("topLeft"),
MemoryLayout.PathElement.groupElement("y")
);
VarHandle colorHandle = rectLayout.varHandle(
MemoryLayout.PathElement.groupElement("color")
);
// Set values using VarHandle
topLeftXHandle.set(rect, 0.0, 10.5);
topLeftYHandle.set(rect, 0.0, 20.3);
colorHandle.set(rect, 0.0, 0xFF0000); // Red
// Read back
double x = (double) topLeftXHandle.get(rect, 0.0);
double y = (double) topLeftYHandle.get(rect, 0.0);
int color = (int) colorHandle.get(rect, 0.0);
System.out.printf("Rectangle: topLeft=(%.1f, %.1f), color=0x%X%n", x, y, color);
}
}
/**
* Union layout example
*/
public static void unionLayout() {
try (Arena arena = Arena.ofConfined()) {
// Define a union that can hold either int, double, or boolean
GroupLayout unionLayout = MemoryLayout.unionLayout(
ValueLayout.JAVA_INT.withName("asInt"),
ValueLayout.JAVA_DOUBLE.withName("asDouble"),
ValueLayout.JAVA_BOOLEAN.withName("asBoolean")
);
MemorySegment union = arena.allocate(unionLayout);
// Store as int
union.set(ValueLayout.JAVA_INT, 0, 42);
System.out.println("As int: " + union.get(ValueLayout.JAVA_INT, 0));
// Store as double (overwrites the int)
union.set(ValueLayout.JAVA_DOUBLE, 0, 3.14159);
System.out.println("As double: " + union.get(ValueLayout.JAVA_DOUBLE, 0));
// The size is the largest member
System.out.println("Union size: " + unionLayout.byteSize() + " bytes");
}
}
/**
* Array of structs with padding
*/
public static void arrayOfStructsWithPadding() {
try (Arena arena = Arena.ofConfined()) {
// Struct with padding for alignment
GroupLayout paddedStruct = MemoryLayout.structLayout(
ValueLayout.JAVA_BYTE.withName("type"),
MemoryLayout.paddingLayout(3), // 3 bytes padding for 4-byte alignment
ValueLayout.JAVA_INT.withName("data"),
ValueLayout.JAVA_DOUBLE.withName("value")
);
SequenceLayout arrayLayout = MemoryLayout.sequenceLayout(10, paddedStruct);
MemorySegment array = arena.allocate(arrayLayout);
System.out.println("Struct size: " + paddedStruct.byteSize());
System.out.println("Array size: " + arrayLayout.byteSize());
// Initialize array
for (int i = 0; i < 10; i++) {
long offset = i * paddedStruct.byteSize();
array.set(ValueLayout.JAVA_BYTE, offset, (byte) i);
array.set(ValueLayout.JAVA_INT, offset + 4, i * 100);
array.set(ValueLayout.JAVA_DOUBLE, offset + 8, i * 1.5);
}
}
}
/**
* Memory layout with bit fields
*/
public static void bitFieldLayout() {
try (Arena arena = Arena.ofConfined()) {
// Simulate C bit fields using Java int with bit manipulation
GroupLayout flagsLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("flags")
);
MemorySegment flags = arena.allocate(flagsLayout);
// Define bit positions
int READ_FLAG = 1 << 0; // 0001
int WRITE_FLAG = 1 << 1; // 0010
int EXECUTE_FLAG = 1 << 2; // 0100
int HIDDEN_FLAG = 1 << 3; // 1000
// Set flags
int currentFlags = 0;
currentFlags |= READ_FLAG | WRITE_FLAG; // Set read and write flags
flags.set(ValueLayout.JAVA_INT, 0, currentFlags);
// Check flags
int storedFlags = flags.get(ValueLayout.JAVA_INT, 0);
boolean canRead = (storedFlags & READ_FLAG) != 0;
boolean canWrite = (storedFlags & WRITE_FLAG) != 0;
boolean canExecute = (storedFlags & EXECUTE_FLAG) != 0;
boolean isHidden = (storedFlags & HIDDEN_FLAG) != 0;
System.out.println("Permissions - Read: " + canRead +
", Write: " + canWrite +
", Execute: " + canExecute +
", Hidden: " + isHidden);
}
}
}
Performance-Critical Operations
package com.example.ffm;
import jdk.incubator.foreign.*;
import java.util.Arrays;
/**
* Performance-focused FFM usage patterns
*/
public class PerformanceOperations {
/**
* Bulk memory operations for performance
*/
public static void bulkMemoryOperations() {
try (Arena arena = Arena.ofConfined()) {
int size = 1000000;
MemorySegment segment = arena.allocate(size * 4); // 1M integers
long startTime = System.nanoTime();
// Fill with values using bulk operations
for (int i = 0; i < size; i++) {
segment.set(ValueLayout.JAVA_INT, i * 4L, i);
}
long fillTime = System.nanoTime() - startTime;
System.out.printf("Fill time: %.2f ms%n", fillTime / 1_000_000.0);
// Bulk copy to Java array
startTime = System.nanoTime();
int[] javaArray = new int[size];
for (int i = 0; i < size; i++) {
javaArray[i] = segment.get(ValueLayout.JAVA_INT, i * 4L);
}
long copyTime = System.nanoTime() - startTime;
System.out.printf("Copy to Java array time: %.2f ms%n", copyTime / 1_000_000.0);
// Verify first and last elements
System.out.println("First element: " + javaArray[0]);
System.out.println("Last element: " + javaArray[size - 1]);
}
}
/**
* Matrix operations using native memory
*/
public static void matrixOperations() {
int rows = 100;
int cols = 100;
try (Arena arena = Arena.ofConfined()) {
// Allocate matrices
MemorySegment matrixA = arena.allocate(rows * cols * 8); // doubles
MemorySegment matrixB = arena.allocate(rows * cols * 8);
MemorySegment result = arena.allocate(rows * cols * 8);
// Initialize matrices
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
long offset = (i * cols + j) * 8L;
matrixA.set(ValueLayout.JAVA_DOUBLE, offset, i + j * 0.1);
matrixB.set(ValueLayout.JAVA_DOUBLE, offset, i * 0.5 + j);
}
}
// Matrix addition
long startTime = System.nanoTime();
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
long offset = (i * cols + j) * 8L;
double a = matrixA.get(ValueLayout.JAVA_DOUBLE, offset);
double b = matrixB.get(ValueLayout.JAVA_DOUBLE, offset);
result.set(ValueLayout.JAVA_DOUBLE, offset, a + b);
}
}
long operationTime = System.nanoTime() - startTime;
System.out.printf("Matrix addition time: %.2f ms%n", operationTime / 1_000_000.0);
// Verify result
long testOffset = (0 * cols + 0) * 8L;
double testValue = result.get(ValueLayout.JAVA_DOUBLE, testOffset);
System.out.println("Result[0][0]: " + testValue);
}
}
/**
* Memory-mapped file operations
*/
public static void memoryMappedFile() throws Exception {
// Note: This is a simplified example
// Real memory mapping would use FileChannel and MemorySegment.map
try (Arena arena = Arena.ofConfined()) {
// Simulate working with file-like data
byte[] fileData = "This is simulated file content for memory mapping example".getBytes();
MemorySegment mappedSegment = arena.allocateFrom(fileData);
// Work with the "mapped" memory
System.out.println("File size: " + mappedSegment.byteSize() + " bytes");
// Read first few bytes as string
String beginning = mappedSegment.getUtf8String(0);
System.out.println("Beginning of file: " + beginning.substring(0, Math.min(20, beginning.length())));
// Modify content
mappedSegment.set(ValueLayout.JAVA_BYTE, 0, (byte) 't'); // Change first character to lowercase
String modified = mappedSegment.getUtf8String(0);
System.out.println("Modified: " + modified.substring(0, Math.min(20, modified.length())));
}
}
}
Integration with Java Ecosystem
package com.example.ffm;
import jdk.incubator.foreign.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Integration with existing Java APIs
*/
public class JavaIntegration {
/**
* Convert between MemorySegment and ByteBuffer
*/
public static void memorySegmentToByteBuffer() {
try (Arena arena = Arena.ofConfined()) {
// Create MemorySegment and initialize
MemorySegment segment = arena.allocate(100);
for (int i = 0; i < 100; i++) {
segment.set(ValueLayout.JAVA_BYTE, i, (byte) i);
}
// Convert to ByteBuffer
ByteBuffer buffer = segment.asByteBuffer();
// Use with existing Java APIs
byte[] javaArray = new byte[50];
buffer.get(javaArray, 0, 50);
System.out.println("First 10 bytes from ByteBuffer:");
for (int i = 0; i < 10; i++) {
System.out.print(javaArray[i] + " ");
}
System.out.println();
// Modify through ByteBuffer and see changes in MemorySegment
buffer.put(0, (byte) 255);
byte fromSegment = segment.get(ValueLayout.JAVA_BYTE, 0);
System.out.println("Modified value via ByteBuffer: " + fromSegment);
}
}
/**
* Working with Java arrays and MemorySegment
*/
public static void arrayIntegration() {
try (Arena arena = Arena.ofConfined()) {
// From Java array to MemorySegment
int[] javaArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
MemorySegment segment = arena.allocate(javaArray.length * 4L);
// Copy array to segment
for (int i = 0; i < javaArray.length; i++) {
segment.set(ValueLayout.JAVA_INT, i * 4L, javaArray[i]);
}
// Process in native memory (example: double each value)
for (int i = 0; i < javaArray.length; i++) {
long offset = i * 4L;
int value = segment.get(ValueLayout.JAVA_INT, offset);
segment.set(ValueLayout.JAVA_INT, offset, value * 2);
}
// Copy back to Java array
int[] resultArray = new int[javaArray.length];
for (int i = 0; i < javaArray.length; i++) {
resultArray[i] = segment.get(ValueLayout.JAVA_INT, i * 4L);
}
System.out.println("Original: " + Arrays.toString(javaArray));
System.out.println("Processed: " + Arrays.toString(resultArray));
}
}
/**
* String conversion utilities
*/
public static void stringUtilities() {
try (Arena arena = Arena.ofConfined()) {
// Java String to C string (null-terminated)
String javaString = "Hello from Java!";
MemorySegment cString = arena.allocateFrom(javaString);
// Get back as Java String
String roundTrip = cString.getUtf8String(0);
System.out.println("Round trip string: " + roundTrip);
// Working with string arrays
String[] stringArray = {"First", "Second", "Third"};
MemorySegment stringArraySegment = arena.allocate(stringArray.length * ValueLayout.ADDRESS.byteSize());
// Allocate each string and store pointers
for (int i = 0; i < stringArray.length; i++) {
MemorySegment stringSeg = arena.allocateFrom(stringArray[i]);
stringArraySegment.set(ValueLayout.ADDRESS, i * ValueLayout.ADDRESS.byteSize(), stringSeg);
}
// Read back
for (int i = 0; i < stringArray.length; i++) {
MemorySegment stringPtr = stringArraySegment.get(ValueLayout.ADDRESS, i * ValueLayout.ADDRESS.byteSize());
String retrieved = stringPtr.getUtf8String(0);
System.out.println("String[" + i + "]: " + retrieved);
}
}
}
}
Error Handling and Safety
package com.example.ffm;
import jdk.incubator.foreign.*;
import java.lang.invoke.MethodHandle;
/**
* Error handling and safety patterns
*/
public class SafetyPatterns {
/**
* Safe memory access with bounds checking
*/
public static void safeMemoryAccess() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(100);
// Safe access within bounds
try {
segment.set(ValueLayout.JAVA_BYTE, 0, (byte) 42); // OK
segment.set(ValueLayout.JAVA_BYTE, 99, (byte) 43); // OK
// This would throw IndexOutOfBoundsException
// segment.set(ValueLayout.JAVA_BYTE, 100, (byte) 44);
System.out.println("All memory accesses were within bounds");
} catch (IndexOutOfBoundsException e) {
System.err.println("Memory access out of bounds: " + e.getMessage());
}
}
}
/**
* Resource cleanup with try-with-resources
*/
public static void resourceCleanup() {
// Arena implements AutoCloseable for automatic cleanup
try (Arena arena = Arena.ofConfined()) {
MemorySegment resource = arena.allocate(1024);
// Use the resource...
resource.set(ValueLayout.JAVA_INT, 0, 123);
System.out.println("Resource allocated and will be automatically cleaned up");
} // Arena closed here, memory freed
System.out.println("Resources have been cleaned up");
}
/**
* Handling native function call errors
*/
public static void safeNativeCalls() throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
try (Arena arena = Arena.ofConfined()) {
MethodHandle strlen = linker.downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
// Safe call with null check
MemorySegment validString = arena.allocateFrom("Valid string");
long length = (long) strlen.invoke(validString);
System.out.println("Valid string length: " + length);
// What happens with null? This would likely crash in C, but FFM provides safety
try {
MemorySegment nullSegment = MemorySegment.NULL;
// This should be handled safely by the FFM API
// strlen.invoke(nullSegment);
} catch (Exception e) {
System.err.println("Safe handling of null pointer: " + e.getMessage());
}
}
}
/**
* Memory alignment safety
*/
public static void alignmentSafety() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(100);
// Properly aligned access
segment.set(ValueLayout.JAVA_INT, 0, 100); // 4-byte aligned
// Misaligned access - FFM handles this safely
try {
segment.set(ValueLayout.JAVA_INT, 1, 200); // Misaligned
System.out.println("Misaligned access succeeded (platform may support it)");
} catch (Exception e) {
System.out.println("Misaligned access prevented: " + e.getMessage());
}
// Use VarHandle for guaranteed alignment
VarHandle intHandle = ValueLayout.JAVA_INT.varHandle();
intHandle.set(segment, 0L, 300); // Always properly aligned
System.out.println("Value set with VarHandle: " + intHandle.get(segment, 0L));
}
}
}
Complete Example: Image Processing
package com.example.ffm;
import jdk.incubator.foreign.*;
import java.util.Arrays;
/**
* Complete example: Simple image processing using FFM API
*/
public class ImageProcessor {
public static class Image {
private final int width;
private final int height;
private final MemorySegment pixels;
private final Arena arena;
public Image(int width, int height) {
this.width = width;
this.height = height;
this.arena = Arena.ofConfined();
this.pixels = arena.allocate(width * height * 3L); // 3 bytes per pixel (RGB)
}
public void setPixel(int x, int y, byte r, byte g, byte b) {
long offset = (y * width + x) * 3L;
pixels.set(ValueLayout.JAVA_BYTE, offset, r);
pixels.set(ValueLayout.JAVA_BYTE, offset + 1, g);
pixels.set(ValueLayout.JAVA_BYTE, offset + 2, b);
}
public byte[] getPixel(int x, int y) {
long offset = (y * width + x) * 3L;
return new byte[] {
pixels.get(ValueLayout.JAVA_BYTE, offset),
pixels.get(ValueLayout.JAVA_BYTE, offset + 1),
pixels.get(ValueLayout.JAVA_BYTE, offset + 2)
};
}
public void toGrayscale() {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
long offset = (y * width + x) * 3L;
byte r = pixels.get(ValueLayout.JAVA_BYTE, offset);
byte g = pixels.get(ValueLayout.JAVA_BYTE, offset + 1);
byte b = pixels.get(ValueLayout.JAVA_BYTE, offset + 2);
// Convert to grayscale
byte gray = (byte) ((r & 0xFF) * 0.299 + (g & 0xFF) * 0.587 + (b & 0xFF) * 0.114);
pixels.set(ValueLayout.JAVA_BYTE, offset, gray);
pixels.set(ValueLayout.JAVA_BYTE, offset + 1, gray);
pixels.set(ValueLayout.JAVA_BYTE, offset + 2, gray);
}
}
}
public void invertColors() {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
long offset = (y * width + x) * 3L;
for (int i = 0; i < 3; i++) {
byte component = pixels.get(ValueLayout.JAVA_BYTE, offset + i);
pixels.set(ValueLayout.JAVA_BYTE, offset + i, (byte) (~component & 0xFF));
}
}
}
}
public void close() {
arena.close();
}
public int getWidth() { return width; }
public int getHeight() { return height; }
}
public static void main(String[] args) {
// Create a simple 3x3 image
Image image = new Image(3, 3);
// Set some pixels (simple pattern)
image.setPixel(0, 0, (byte) 255, (byte) 0, (byte) 0); // Red
image.setPixel(1, 0, (byte) 0, (byte) 255, (byte) 0); // Green
image.setPixel(2, 0, (byte) 0, (byte) 0, (byte) 255); // Blue
image.setPixel(0, 1, (byte) 255, (byte) 255, (byte) 0); // Yellow
image.setPixel(1, 1, (byte) 255, (byte) 255, (byte) 255); // White
image.setPixel(2, 1, (byte) 0, (byte) 255, (byte) 255); // Cyan
image.setPixel(0, 2, (byte) 255, (byte) 0, (byte) 255); // Magenta
image.setPixel(1, 2, (byte) 128, (byte) 128, (byte) 128); // Gray
image.setPixel(2, 2, (byte) 0, (byte) 0, (byte) 0); // Black
System.out.println("Original image:");
printImage(image);
// Apply grayscale
image.toGrayscale();
System.out.println("\nGrayscale image:");
printImage(image);
// Apply color inversion
image.invertColors();
System.out.println("\nInverted image:");
printImage(image);
image.close();
}
private static void printImage(Image image) {
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
byte[] pixel = image.getPixel(x, y);
System.out.printf("[%3d,%3d,%3d] ",
pixel[0] & 0xFF, pixel[1] & 0xFF, pixel[2] & 0xFF);
}
System.out.println();
}
}
}
Best Practices and Considerations
- Use Arenas: Always use
Arenafor automatic resource management - Bounds Checking: Leverage FFM's built-in bounds checking
- Type Safety: Use
VarHandlefor type-safe memory access - Error Handling: Always handle potential native call failures
- Performance: Use bulk operations for better performance
- Testing: Thoroughly test with different data patterns and sizes
- Platform Differences: Be aware of platform-specific memory alignment
Conclusion
The Foreign Function & Memory API provides a powerful, safe, and efficient way to work with native memory and functions from Java. Key benefits include:
- Performance: Direct memory access without JNI overhead
- Safety: Built-in bounds checking and type validation
- Productivity: Pure Java API without native code generation
- Modern: Integration with modern Java features like pattern matching
- Future-proof: Designed for upcoming Java features like Valhalla
This API represents a significant step forward in Java's ability to efficiently interoperate with native code while maintaining Java's safety guarantees and developer productivity.