Roray FFM Utils Guide
📝 This documentation is maintained in the roray-ffm-utils repository. View source | Last synced: 2025-12-05
Roray FFM Utils — Usage Guide
This document explains how to use the public APIs in roray-ffm-utils. It assumes familiarity with the Java Foreign Function & Memory API introduced in Java 14 and refined in Java 22+. All examples compile against JDK 24+ with the preview FFM API enabled.
Prerequisites
- JDK 24 or newer (the project targets 24+ and configures the toolchain for JDK 25).
- Preview features enabled when compiling or running (e.g.
--enable-preview). - Gradle 9+ or equivalent build tooling.
- Opt-in to the FFM API in runtime arguments when required (for long-running apps use
--enable-native-access=ALL-UNNAMED).
Adding the Library to Your Build
Gradle (Kotlin DSL)
dependencies {
implementation("express.mvp.roray.utils.memory:roray-ffm-utils:0.1.0-SNAPSHOT")
}
Gradle (Groovy DSL)
dependencies
Maven
express.mvp.roray.utils.memory
roray-ffm-utils
0.1.0-SNAPSHOT
Tip: When running or testing, pass
--enable-preview --enable-native-access=ALL-UNNAMEDto the JVM.
Memory Lifecycle Strategies
The library is designed for zero-GC workflows that reuse off-heap buffers.
- Arena scope: Use
Arena.ofConfined()for per-thread lifecycles orArena.ofAuto()for shared lifetimes managed by reference counting. - Segment pooling: Use
MemorySegmentPoolto acquire and recycle fixed-size segments without repeated heap pressure. - Reusable codecs: Reuse instances of
SegmentBinaryWriter,SegmentBinaryReader, andUtf8Viewacross invocations on the same thread. - Scratch buffers: Allocate one scratch
MemorySegmentper thread for zero-allocation string encoding.
The sections that follow walk through each API in detail.
MemorySegmentPool
MemorySegmentPool offers a thread-safe pool of fixed-size segments allocated from an auto-managed Arena. It reduces the cost of creating fresh segments in hot paths.
Construction and Sizing
int segmentSize ; // bytes per segment
int initialSize ; // eagerly allocated
int maxSize ; // hard cap
MemorySegmentPool pool ;
// Or with configurable zeroing
boolean zeroOnRelease ; // Skip zeroing for performance (if safe)
MemorySegmentPool fastPool ;
segmentSize > 0defines the capacity of each pooled segment.initialSize >= 0determines how many segments are created up-front and made available.maxSize > 0is the upper bound on total segments (pooled + checked-out). Exceeding this raises anIllegalStateException.zeroOnRelease(default:true) controls whether released segments are zero-filled before returning to pool. Disable only if you have application-level security guarantees.
Acquiring and Releasing Segments
MemorySegment segment ; // fixed-size segment
try finally
acquire() returns a zeroed, fixed-size segment from the pool if available, otherwise it allocates a new one (respecting maxSize).
Release semantics:
- Only segments whose
byteSize()equalssegmentSizeare returned to the pool. - Released segments are zero-filled (
segment.fill((byte) 0)) before re-enqueueing to mitigate data leakage between borrowers.
Variable-Size Requests
long largeSize ;
MemorySegment big ;
try finally
acquire(long requiredSize) returns a fixed-size pooled segment when requiredSize <= segmentSize and allocates an on-demand segment otherwise. Oversized segments bypass pooling on release.
Pool Introspection and Metrics
int available ; // size of the internal queue
int total ; // segments ever allocated up to maxSize
long capacity ; // configured per-segment capacity
// New metrics for monitoring
long totalAllocations ; // Lifetime allocation count
int currentlyInUse ; // Currently checked-out segments
int peakUsage ; // Maximum concurrent usage observed
These metrics are safe to call concurrently and aid in operational monitoring. Use getPeakUsage() to understand maximum concurrency requirements and size your pool accordingly.
Concurrent Usage Pattern
ExecutorService executor ;
SegmentBinaryWriter writer ;
SegmentBinaryReader reader ;
Utf8View view ;
MemorySegment scratch ;
Runnable task ;
executor.;
The pool is safe for use with platform or virtual threads.
SegmentBinaryWriter & BinaryWriter
SegmentBinaryWriter is the canonical implementation of the BinaryWriter interface. It encodes values into a wrapped MemorySegment using fluent, chainable methods. Every method returns this so you can compose writes.
Preparing the Writer
SegmentBinaryWriter writer ;
MemorySegment payload ;
writer.; // resets position to 0
long bytesLeft ;
writer.; // jump ahead (throws IndexOutOfBounds if > byteSize)
wrap(MemorySegment)must be called before writing; it sets the internal segment and resets the write position to0.position()andposition(long)report or update the cursor. Bounds are enforced.remaining()reportssegment.byteSize() - position.skip(long)advances the cursor without writing data.
Writing Big-Endian Primitives
writer.
.
. // encoded as 1 byte: 1 for true, 0 for false
.
.
.
.
.;
These methods rely on value layouts defined in Layouts. They increment the cursor by the width of the primitive.
Writing Little-Endian Primitives
writer.
.
.
.
.
.;
Little-endian writes mirror the big-endian variants so you can match wire formats or packed structs.
Variable-Length Integers (Writing)
writer.
. // 1 byte
. // 3 bytes
.; // up to 10 bytes
writeVarInt(int value)encodes 32-bit values using a LEB128-like scheme.writeVarLong(long value)does the same for 64-bit values.
These are ideal for small numbers and length prefixes.
Note: These methods emit unsigned varints. Negative inputs are still supported but will consume the full 5/10-byte width unless you zig-zag them first. A simple helper:
static int
static int
Strings and Byte Arrays (Zero-Copy Friendly)
MemorySegment scratch ;
writer.
.
. // multi-byte UTF-8
.;
writeString(String value, MemorySegment scratchBuffer)manually encodes UTF-8 bytes into the provided scratch buffer, writes a VarInt length prefix, and copies the bytes into the target segment.- Scratch buffers must be large enough to hold the encoded bytes; reuse one buffer per thread to avoid allocations.
writeBytes(byte[] value)writes a VarInt-prefixed heap array without requiring a scratch buffer.
Nullable Encodings
Every nullable variant writes a 1-byte presence flag (true for present, false for null) and then delegates to the non-null variant.
writer.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.;
These pair with the corresponding readNullable* methods in SegmentBinaryReader.
Bulk Segment Copies
MemorySegment source ;
source.;
writer..;
writeSegment(MemorySegment source) writes a VarLong length prefix followed by the raw bytes of the source segment.
Position Control
writer.
.
. // leaves a gap for future data
.;
writer.;
writer.; // fill skipped region later
skip(long) is useful for reserving space (e.g. checksums). Remember to ensure you do not move beyond the segment bounds.
SegmentBinaryReader & BinaryReader
SegmentBinaryReader mirrors the writer. Wrap a segment, then sequentially decode values. Methods throw if you attempt to read past the end or if the variable-length encodings are malformed.
Preparing the Reader
SegmentBinaryReader reader ;
reader.;
long pos ;
reader.; // rewinds or jumps around
long remaining ;
reader.; // advances cursor with bounds checks
Reading Big-Endian Primitives
reader.;
byte marker ;
boolean flag ;
short shortBE ;
int intBE ;
long longBE ;
float floatBE ;
double doubleBE ;
Each method advances the cursor by the width of the primitive.
Reading Little-Endian Primitives
short shortLE ;
int intLE ;
long longLE ;
float floatLE ;
double doubleLE ;
Variable-Length Integers (Reading)
int small ;
long big ;
Both throw IllegalStateException if more continuation bytes are encountered than the format allows.
Reading Strings and Byte Arrays
Utf8View utf8 ;
reader.; // populates view with next VarInt-prefixed string
String heapCopy ;
boolean present ; // false => value was null
byte[] bytes ;
byte[] optional ;
readString(Utf8View view)consumes the VarInt length and points the view at the substring inside the backing segment. No heap allocations occur untiltoString()is called.readNullableString(Utf8View view)returnsfalseand leaves the view untouched when the presence byte is0.
Nullable Primitives
Byte optByte ;
Short optShortBE ;
Integer optIntBE ;
Long optLongBE ;
byte[] optBytes ;
Each method reads the presence flag, returning null if absent and delegating to the corresponding non-null method when present.
Combined Reader/Writer Round Trip Example
MemorySegment scratch ;
MemorySegment buffer ;
SegmentBinaryWriter writer ;
SegmentBinaryReader reader ;
Utf8View view ;
writer.
.
.
.
.
.
.;
reader.;
int id ;
long counter ;
reader.;
Integer maybeInt ;
boolean stringPresent ;
boolean lastFlag ;
This pattern underpins serialization/deserialization in zero-GC pipelines.
Utf8View Deep Dive
Utf8View is a reusable flyweight that exposes UTF-8 slices inside a MemorySegment.
Wrapping and Inspecting Data
Utf8View view ;
MemorySegment segment ;
view.;
// Zero-allocation comparisons
if
// Heap allocation only when necessary
String copy ;
wrap(MemorySegment segment, long offset, int length)points the view at UTF-8 bytes located atsegment.asSlice(offset, length).toString()allocates and decodes into a JavaString(use sparingly on hot paths).equalsString(String other)compares without allocating intermediate byte arrays (currently delegates totoString()internally, but centralises the comparison logic for future optimisation).
Using with SegmentBinaryReader
SegmentBinaryReader reader ;
Utf8View customerName ;
reader.;
reader.; // view now references bytes in the buffer
; // downstream logic can inspect without copying
The view remains valid as long as the underlying segment stays alive.
Layouts Utility
Layouts defines reusable ValueLayout constants with byte alignment set to 1 (withByteAlignment(1)), permitting unaligned access.
Accessing Primitives Manually
MemorySegment segment ;
long base ;
segment.;
segment.;
segment.;
int intBE ;
short shortLE ;
double doubleBE ;
These layouts are leveraged internally by the reader/writer but are available for custom low-level work such as flyweights or manual struct manipulation.
Supporting Custom Flyweights
public record
Using Layouts ensures consistent endianness and alignment across the codebase.
FlyweightAccessor Interface
FlyweightAccessor defines the contract for objects that provide structured access to bytes stored in a MemorySegment.
Implementing a Flyweight
Using the Flyweight
MemorySegment buffer ;
TradeHeader header ;
header.;
header.;
header.;
header.;
SegmentBinaryWriter writer ;
writer..;
SegmentBinaryReader reader ;
reader.;
TradeHeader snapshot ;
snapshot.;
Flyweights allow you to manipulate structured records without copying data between heap objects.
SegmentUtils
SegmentUtils contains stateless helpers for MemorySegment operations.
CRC32 Calculation
MemorySegment payload ;
payload.;
payload.;
int checksum ;
System.out.;
calculateCrc32(MemorySegment segment)creates a zero-copyByteBufferview and computes the CRC32 checksum without copying to the heap.- Useful for message integrity checks or detecting buffer corruption before releasing to the pool.
Extending SegmentUtils
The class is final with a private constructor, encouraging addition of more high-performance utilities (e.g. vectorized zeroing or copying using Java’s Vector API). Use this file as the central place for shared segment helpers.
Putting It All Together: End-to-End Example
The following example shows a complete request/response codec that utilises every major component.
This demonstrates how to combine the pool, codec primitives, UTF-8 views, and utilities in a cohesive pipeline.
VarFieldWriter
VarFieldWriter builds messages with variable-length fields in a flyweight-compatible format. It uses a two-phase layout: a fixed header region containing [offset:int32][length:int32] descriptors for each variable field, followed by a data region where the actual field content is written.
Construction
MemorySegment segment ;
int fixedHeaderSize ; // Reserve 16 bytes for fixed fields
int maxVarFields ; // Support up to 3 variable fields
VarFieldWriter writer ;
Memory layout:
[0..fixedHeaderSize-1] : Fixed header (write with writeIntLE, etc.)
[fixedHeaderSize..N] : Var field headers (8 bytes each: offset+length)
[after var field headers..end] : Variable data region
Writing Fixed Header Fields
writer.; // Write message type at offset 0
writer.; // Write timestamp at offset 4
writer.; // Write flags at offset 12
Writing Variable Fields
// Reserve slot for each variable field
int keySlot ;
int valueSlot ;
int metadataSlot ;
// Reusable scratch buffer for UTF-8 encoding (size it for your largest string)
MemorySegment stringScratch ;
// Write variable data (can be byte[], String, or MemorySegment)
writer.;
writer.;
writer.;
Finishing and Metrics
MemorySegment message ; // Returns slice from start to current position
long totalBytes ; // Total message size
int varCount ; // Number of var fields written
long dataStart ; // Offset where data region begins
Resetting for Reuse
writer.; // Clears state, allows reusing same writer instance
Important: finish() returns a slice of the same segment. If you need to preserve the message after calling reset(), copy it to independent storage first.
BitSetView
BitSetView is a flyweight that treats a MemorySegment as a bit set, enabling zero-copy bit manipulation.
Wrapping a Segment
MemorySegment segment ; // 128 bytes = 1024 bits
BitSetView bitSet ;
bitSet.;
Basic Operations
bitSet.; // Set bit 42 to 1
boolean isSet ; // Returns true
bitSet.; // Clear bit 42 to 0
bitSet.; // Toggle bit 42
bitSet.; // Set all bits to 1
bitSet.; // Clear all bits to 0
Cardinality
long count ; // Count number of 1 bits
Searching
long firstSet ; // Find first 1 bit starting from index 0
long firstClear ; // Find first 0 bit starting from index 0
// Iterate all set bits
for
Boundary Checks
All operations check that the bit index is within the segment bounds. Out-of-bounds access throws IndexOutOfBoundsException.
Utf8View
Utf8View is a zero-GC flyweight for UTF-8 strings stored in off-heap memory. It enables string comparisons and hashing without heap allocations.
Wrapping UTF-8 Data
String text ;
byte[] utf8Bytes ;
MemorySegment segment ;
segment.;
Utf8View view ;
view.;
Zero-GC String Comparison
// Compare with Java String without heap allocation
if
// Compare with another Utf8View
Utf8View other ;
other.;
if
Lexicographic Ordering
int cmp ; // Returns -1, 0, or 1
if
Hash Code
int hash ; // Consistent with String.hashCode() for same content
Map map ; // Can use as map key
String Conversion (Allocates)
String str ; // Allocates heap String - avoid in hot path
Validation
if
Operational Tips
- Bounds safety:
position(long)andskip(long)enforce bounds, but they do not grow the underlying segment. Always size segments to accommodate the maximum payload. - Threading:
SegmentBinaryWriter,SegmentBinaryReader, andUtf8Vieware not thread-safe. Reuse them per thread or guard externally. - Scratch buffers: Maintain one scratch buffer per thread to avoid contention and ensure zero allocations during string writes. If you underestimate capacity, throws occur when copying string bytes.
- Pooling vs. arenas: The pool internally uses
Arena.ofAuto(). If you require deterministic lifetime control (e.g. closing per request), preferArena.ofConfined()and manual resource management. - Monitoring: Track
getAvailableCount()andgetTotalCount()to spot saturation. When the pool reachesmaxSize,acquire()throws — treat this as backpressure.
Test Coverage References
The project ships with comprehensive JUnit tests illustrating most APIs:
MemorySegmentPoolTestexercises pool growth, exhaustion, zeroing, variable-size handling, and concurrency.SegmentBinaryReaderWriterTestcovers primitive round-trips, varints, strings, byte arrays, nullable fields,skip, and positional controls.
Use these tests as additional examples or starting points for your own scenarios.
Next Steps and Extensibility
Future work noted in ToDo.md includes vectorized copy/zero helpers, additional flyweights, and JMH benchmarks. When new utilities are added, extend this guide with corresponding usage patterns to keep it authoritative.
By following this guide, you can leverage roray-ffm-utils to build high-throughput, low-latency, GC-free pipelines on top of Java’s Foreign Function & Memory API.
FFM Function Utilities
The express.mvp.roray.utils.functions package provides zero-overhead utilities for calling native functions via Java’s Foreign Function & Memory API.
Platform: Linux x86_64 and ARM64 (LP64 data model only). These utilities are NOT compatible with Windows or 32-bit systems.
Design Philosophy
All utilities follow the zero-cost abstraction principle:
- Setup-time cost only — Builder patterns and factories run once during class initialization
- Call-time zero overhead — The resulting
MethodHandleis identical to hand-written FFM code - Static final storage — Store handles in
static finalfields for optimal JIT performance
FunctionDescriptorBuilder
Fluent builder for creating FunctionDescriptor instances:
// Simple syscall with no arguments
FunctionDescriptor getpidDesc ;
// File I/O with multiple arguments
FunctionDescriptor writeDesc ;
// Socket creation
FunctionDescriptor socketDesc ;
// Void return (e.g., exit)
FunctionDescriptor exitDesc ;
DowncallFactory
Factory for creating downcall method handles:
private static final DowncallFactory FACTORY ;
// Basic syscall
private static final MethodHandle getpid ;
// With linker options (critical mode for non-blocking calls)
private static final MethodHandle read ;
// Usage at call-time (zero overhead)
public int throws Throwable
Loading a custom library:
// Load liburing.so
private static final DowncallFactory URING_FACTORY ;
private static final MethodHandle io_uring_queue_init ;
LinuxLayouts
Pre-defined MemoryLayout constants for C types and Linux structures:
Primitive Types:
LinuxLayouts.C_CHAR // 1 byte
LinuxLayouts.C_SHORT // 2 bytes
LinuxLayouts.C_INT // 4 bytes
LinuxLayouts.C_LONG // 8 bytes (LP64)
LinuxLayouts.C_POINTER // 8 bytes (64-bit)
LinuxLayouts.C_SIZE_T // 8 bytes
LinuxLayouts.FD // 4 bytes (file descriptor)
Socket Structures:
// struct iovec - scatter/gather I/O
LinuxLayouts.IOVEC // 16 bytes
LinuxLayouts.IOVEC_IOV_BASE // offset 0
LinuxLayouts.IOVEC_IOV_LEN // offset 8
// struct sockaddr_in - IPv4 address
LinuxLayouts.SOCKADDR_IN // 16 bytes
LinuxLayouts.AF_INET // address family constant
// struct sockaddr_in6 - IPv6 address
LinuxLayouts.SOCKADDR_IN6 // 28 bytes
LinuxLayouts.AF_INET6 // address family constant
// struct msghdr - message header for sendmsg/recvmsg
LinuxLayouts.MSGHDR // 56 bytes
io_uring Structures:
// Submission Queue Entry
LinuxLayouts.IO_URING_SQE // 64 bytes
// Completion Queue Entry
LinuxLayouts.IO_URING_CQE // 16 bytes
Socket Example:
private static final MethodHandle socket ;
private static final MethodHandle bind ;
public int throws Throwable
UpcallFactory
Create native callbacks from Java methods:
// Define callback signature
FunctionDescriptor signalHandlerDesc ;
// From a MethodHandle
MethodHandle handler ;
MemorySegment callback ;
// From a functional interface (lambda)
MemorySegment callback ;
Important: The
Arenacontrols the callback’s lifetime. UseArena.global()for callbacks that must outlive any specific scope.
ErrnoCapture
Capture and interpret errno from syscalls:
// Create handle with errno capture
private static final MethodHandle open ;
public int throws Throwable
Common errno constants:
ErrnoCapture.ENOENT // 2 - No such file or directory
ErrnoCapture.EACCES // 13 - Permission denied
ErrnoCapture.EAGAIN // 11 - Resource temporarily unavailable
ErrnoCapture.EINTR // 4 - Interrupted system call
ErrnoCapture.ECONNREFUSED // 111 - Connection refused
StructAccessor
VarHandle-based struct field access:
// Create accessor for iovec struct
StructAccessor iovecAccessor ;
try
io_uring SQE example:
StructAccessor sqeAccessor ;
MemorySegment sqe ;
sqeAccessor.;
sqeAccessor.;
sqeAccessor.;
sqeAccessor.;
sqeAccessor.;
sqeAccessor.;
sqeAccessor.;
Critical Mode Annotations
Documentation annotations to mark functions as safe or unsafe for critical mode:
/**
* @CriticalSafe - Can use Linker.Option.critical(false)
* Criteria: No blocking, no signals, runs in microseconds
*/
private static final MethodHandle getpid ;
/**
* @NeverCritical - Must NOT use critical mode
* Reason: Blocks waiting for I/O
*/
private static final MethodHandle read ;
Complete Socket Server Example
Performance Notes
- JMH verified: Benchmarks confirm zero overhead vs hand-written FFM code (~140ns for getpid)
- Critical mode: Use
Linker.Option.critical(false)only for non-blocking operations - Static final: Always store handles in
static finalfields for JIT optimization - Arena lifetime: Match arena scope to data lifetime; avoid
Arena.global()for short-lived data