Davide Angelocola

Java + RocksDB - JNI

11 April 2026

The best way I know to learn something is to build something real with it. I wanted to understand Java’s Foreign Function & Memory (FFM) API properly — not just read the Javadoc, but actually use it. RocksDB was the obvious vehicle: I’m a long-time user of RocksDB and it has a well-defined C API and a problem space I already understood.

The result is rocksdbffm: an experimental RocksDB wrapper for Java using FFM instead of JNI.


FFM Concepts

The FFM API (part of java.lang.foreign, finalised in JDK 22) has a few central concepts worth understanding before you touch RocksDB. There are a couple of excellent starting points: dev.java and JEP-454 that also explain the why.

MemorySegment and Arena

MemorySegment is FFM’s handle to off-heap memory. It knows its size and its confinement — which thread can access it, and for how long. An Arena controls the lifecycle: when the arena closes, all segments allocated within it become invalid.

try (Arena arena = Arena.ofConfined()) {
    MemorySegment key = arena.allocateFrom("hello");
    MemorySegment value = arena.allocateFrom("world");
    db.put(key, value);
} // memory freed here

Linker and MethodHandle

To call a native function, you obtain a MethodHandle for it via the Linker:

Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.libraryLookup("librocksdb.so", arena); // Linux; use librocksdb.dylib on macOS

MethodHandle rocksdbOpen = linker.downcallHandle(
    lookup.find("rocksdb_open").orElseThrow(),
    FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, ADDRESS)
);

FunctionDescriptor describes the C signature in Java terms: ADDRESS for pointers, ValueLayout.JAVA_INT for int, and so on. The handle is then called like any other MethodHandle.

The Generated Layer

The low-level binding — MethodHandle declarations, FunctionDescriptor definitions, pointer layouts — is generated from RocksDB’s rocksdb/c.h by an LLM. The generated code is plain Java: it’s in the repository, you can read it, grep it, and understand what’s happening. No magic .so build step for the glue layer itself.

On top of that generated layer sits an idiomatic Java API: RocksDB, WriteBatch, Transaction, Iterator. The generated code is internal; the public API hides the MemorySegment plumbing behind familiar abstractions.


Design Choices Worth Explaining

AI-driven

This is also my first “AI project”: I started it by writing the CLAUDE.md and AGENTS.md files, most of the code is generated by AI. Code is structured to be friendly to new developers, either humans or AI agents. jextract was considered as a starting point as well but rejected in the end. Why? Because AI can do the whole job: integrating, reviewing, generating test cases, and following other rules.

The C API as the binding surface

RocksDB is a C++ library, but it ships an official stable C API (rocksdb/c.h). This is the right layer to bind against — not the C++ classes directly.

This is not a novel choice: rust-rocksdb binds against rocksdb/c.h via Rust’s FFI.

rocksdbffm applies the same idea to Java. The official Java client, rocksdbjni, requires a JNI glue layer — C++ code that bridges between the JVM and the library. FFM eliminates that entirely: it can call C functions directly from Java. The C header is the contract; there is nothing in between.

Here is what a bound function looks like at the generated layer, and how the public API wraps it. Take rocksdb_put as an example. In rocksdb/c.h:

extern ROCKSDB_LIBRARY_API void rocksdb_put(
    rocksdb_t* db,
    const rocksdb_writeoptions_t* options,
    const char* key, size_t keylen,
    const char* val, size_t vallen,
    char** errptr);

The generated Java binding:

static final MethodHandle rocksdb_put = Linker.nativeLinker().downcallHandle(
    SYMBOL_LOOKUP.find("rocksdb_put").orElseThrow(),
    FunctionDescriptor.ofVoid(ADDRESS, ADDRESS, ADDRESS, JAVA_LONG, ADDRESS, JAVA_LONG, ADDRESS)
);

JAVA_LONG is used for size_t — correct on any 64-bit JVM, which is the only target platform.

And the public API method that calls it:

public void put(byte[] key, byte[] value) {
    try (Arena arena = Arena.ofConfined()) {
        MemorySegment errPtr = arena.allocate(ADDRESS);
        RocksDbBindings.rocksdb_put.invokeExact(
            handle, writeOptions,
            arena.allocateFrom(ValueLayout.JAVA_BYTE, key), (long) key.length,
            arena.allocateFrom(ValueLayout.JAVA_BYTE, value), (long) value.length,
            errPtr
        );
        throwIfError(errPtr);
    }
}

The pattern repeats across the whole API: allocate a confined arena for the call, copy Java data into native segments, invoke the handle, check the error pointer, free everything when the arena closes. Mechanical, auditable, boring… perfect for an LLM.

Type safe API

Every operation in RocksDB exposes only methods that are actually possible. Example: in RocksDB it is possible to open a readonly instance, call put and the program compiles correctly but fails at runtime.

This project turns those into compile-time errors instead. RocksDB.open() returns a ReadWriteDB and RocksDB.openReadOnly() returns a ReadOnlyDB. These are separate concrete classes with no shared interface: put, delete, and merge simply do not exist on ReadOnlyDB. The compiler rejects the call before any code runs.

Embracing modern Java

The project requires JDK 25+. There is no legacy compatibility shim. The API uses FFM (which already cuts out a lot of production workloads), records, etc.

A SequenceNumber is a record, not a long. A MemorySize is a record, not an undocumented byte count passed silently.

// rocksdbjni style
options.setBlockSize(67108864L); // what unit? what range?

// rocksdbffm style
options.setBlockSize(MemorySize.ofMB(64));

The same principle I wrote about in the domain primitives post applies here: illegal values should be unrepresentable. MemorySize.ofMB(-1) throws at construction.

Path, not String

Every method that accepts a filesystem location takes java.nio.file.Path. This prevents the classic confusion between absolute and relative paths and integrates with the NIO API naturally.

Unchecked exceptions

Every operation that can fail throws RocksDBException (unchecked). Failures here are always loud. You can still catch them if you need to; you cannot accidentally swallow them.

No public constructors taking MemorySegment

That would break encapsulation by passing an arbitrary native pointer where a specific RocksDB handle is expected. For this reason, no Java wrapper exposes a public constructor.


The Build: Zig as a C++ Compiler

Building RocksDB from source is the part that surprised me most. The project uses Zig as a drop-in C/C++ compiler (zig cc / zig c++) instead of the system toolchain. Zig bundles clang and libc++ for every target, which means the build is hermetic: no sysroot, no separate apt install for build dependencies.

PORTABLE=1 CC="zig cc" CXX="zig c++" make shared_lib

This was not obvious to me before starting. It is the right call for a project that needs to build reliably on macOS and Linux without assuming a particular system toolchain. If you clone the repository, the Maven native-build profile runs this for you.


Contribute

The project is experimental, not yet on Maven Central, without Windows support, and not a drop-in replacement for rocksdbjni.

The long-term goal, stated in the README, is to contribute it back upstream into the RocksDB project itself — or run it as a separate library if the community wants that instead.

If you work with RocksDB in Java, or you want a concrete project to learn FFM with, take a look.

The library covers most of the core RocksDB API:

The missing features in the roadmap are well-scoped contributions. Column Families in particular is the right next step — it is important for production use and the C API is straightforward.

Learnings

Building this answered the question I started with: FFM is genuinely usable for real libraries, the mental model is clean once you have Arena and MemorySegment straight, and the absence of a JNI layer makes the whole stack easier to reason about. The hardest part was not FFM — it was building RocksDB itself. zig turns out to be an excellent hermetic C/C++ toolchain, and I would reach for it again in any project that needs to build native code portably.