Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/LWJGL/lwjgl3/llms.txt

Use this file to discover all available pages before exploring further.

Starting with LWJGL 3.4.0, when the library runs on JDK 25 or later, a new backend takes over automatically. Instead of JNI for downcalls, libffi+JNI for upcalls, and sun.misc.Unsafe for off-heap memory access, LWJGL uses the JDK’s own Foreign Function and Memory (FFM) API throughout. The switch is invisible to most application code: the same memAlloc, glfwCreateWindow, and GLFWWindowSizeCallback APIs you already use continue to work; only the machinery underneath changes. The FFM backend also exposes a user-facing runtime bindings generator in org.lwjgl.system.ffm, letting you define custom native bindings in plain Java without a separate code-generation step.

Requirements

LWJGL 3.4.0 or later

The FFM backend was introduced in this release. Earlier versions use the JNI/Unsafe backend exclusively.

JDK 25 or later

FFM API became stable in JDK 22 (JEP 454), but LWJGL targets JDK 25+ for its backend. On older JDKs, LWJGL automatically falls back to JNI/Unsafe.
No additional JVM flags are required to enable the FFM backend. When both conditions are met, it is active by default.

Opting out

If your application encounters compatibility or performance issues with the FFM backend, use the unsafe Maven classifier to select the JNI/Unsafe artifact instead:
<dependency>
  <groupId>org.lwjgl</groupId>
  <artifactId>lwjgl</artifactId>
  <version>3.4.0</version>
  <classifier>unsafe</classifier>
</dependency>
Only the core lwjgl artifact has an unsafe classifier. Other module artifacts (lwjgl-glfw, lwjgl-vulkan, etc.) are unchanged.
With the FFM backend active, LWJGL is fully functional when the JVM is started with --sun-misc-unsafe-memory-access=deny, making it compatible with future JDK hardening.

Using the runtime bindings generator

The FFM backend ships a runtime generator in org.lwjgl.system.ffm. You describe a native API as a Java interface, annotate its methods, and call one of the bootstrap methods in FFM to get back a working implementation at runtime. No Kotlin, no offline build step. It is recommended to use static imports for the FFM class:
import static org.lwjgl.system.ffm.FFM.*;

Bootstrap methods

MethodPurpose
ffmGenerate(Class<T>)Generate an implementation of a downcall interface
ffmUpcall(Class<T>)Create a binder for a single upcall type
ffmStruct(Class<T>)Create a binder for a struct type
ffmUnion(Class<T>)Create a binder for a union type

Defining a downcall interface

import java.lang.foreign.*;
import java.lang.invoke.*;
import org.lwjgl.system.ffm.*;
import static org.lwjgl.system.ffm.FFM.*;

// 1. Declare the interface — one method per native function
public interface MyLib {
    int myAdd(int a, int b);
    @FFMNullable MemorySegment myAllocate(long size);
}

// 2. Register an FFMConfig for your class (in a static initializer)
//    Use withSymbolLookup to tell the generator where to resolve symbols.
static {
    ffmConfig(MyApp.class, ffmConfigBuilder(MethodHandles.lookup())
        .withSymbolLookup(SymbolLookup.libraryLookup("mylib", Arena.global()))
        .build());
}

// 3. Generate and cache the implementation (store as static final)
static final MyLib MY_LIB = ffmGenerate(MyLib.class);

// 4. Call it like any Java interface
int result = MY_LIB.myAdd(2, 3);

Defining a struct binder

public interface MyPoint {
    // Field accessors generated from the interface shape
    int x();
    void x(int value);
    int y();
    void y(int value);
}

// ffmStruct returns a binder, not an instance
var PointBinder = ffmStruct(MyPoint.class);

// Allocate a struct in an Arena
try (Arena arena = Arena.ofConfined()) {
    MyPoint p = PointBinder.allocate(arena);
    p.x(10);
    p.y(20);
    MY_LIB.draw(p);
}

Nullable parameters

FFM represents null pointers as MemorySegment.NULL (not Java null). LWJGL generates wrapper logic based on annotations:
  • No annotation → parameter is non-nullable; passing null or MemorySegment.NULL throws.
  • @FFMNullable → null-restricted (Java null throws), but MemorySegment.NULL is accepted.
  • A configured @Nullable annotation (e.g., org.jspecify.annotations.Nullable) → LWJGL replaces Java null with MemorySegment.NULL automatically.
Configure the nullable annotation globally:
// Must be set before the bindings are first generated
Configuration.FFM_DEFAULT_NULLABLE_ANNOTATION.set("org.jspecify.annotations.Nullable");

Performance characteristics

The FFM backend is designed for zero overhead versus direct FFM API usage:
  • Lazy generation. A downcall MethodHandle is not created until the first invocation of the corresponding interface method. Upcall and struct binders are not generated until their binder instances are first accessed.
  • Inlineable by the JIT. Implementations are created as hidden classes with trusted final fields (similar to Java records). Storing binder objects as static final fields enables the JIT to inline them aggressively.
  • Zero-allocation structs. Struct instance copies are inlined at bytecode level with hardcoded byte counts, eliminating intermediate allocations.
  • Upcall throughput. Upcall invocation overhead is 3–4× lower than JNI upcalls. Instantiation is roughly 15× slower, so prefer long-lived upcall objects.
Upgrading to JDK 26 or later is strongly recommended. It is the first version where the JIT compiles on-heap and off-heap memory accesses in a uniform fashion, enabling loop unrolling and SIMD vectorization even for long-indexed off-heap loops.

Known limitations

The FFM API limits ByteBuffer instances to Integer.MAX_VALUE - 8 bytes (just under 2 GB). Creating a multi-byte buffer larger than this is no longer supported. Use MemorySegment directly for 64-bit addressed memory regions.
Native code cannot propagate Java exceptions. By default, LWJGL wraps every upcall in a try-catch handler that prints uncaught exceptions. This behavior is controlled by two options:
// Disable the automatic try-catch wrapper (only if your upcall handles exceptions internally)
Configuration.FFM_UPCALL_EXCEPTION_CATCH.set(false);

// Install a custom handler
Configuration.FFM_UPCALL_EXCEPTION_HANDLER.set((Throwable t) -> myLogger.error("upcall", t));
Upcalls that return a struct by value cannot be called concurrently from multiple threads. As of LWJGL 3.4.0, this affects only YGMeasureFunc in the Yoga module.
Upcalls must be allocated in an FFM Arena. By default, LWJGL uses a GC-managed auto arena. You can change this globally or use the ffmScoped* methods to supply your own arena per upcall:
// Change the default arena type (static option — set before first use)
Configuration.FFM_UPCALL_ARENA.set("confined"); // or "shared"

// Recommended: use a user-managed arena for fine-grained lifetime control
try (Arena arena = Arena.ofConfined()) {
    // ffmScopedRun binds the arena to the current scope for upcall allocation
    ffmScopedRun(arena, () -> {
        myCallback = MyCallback.create(arena, (a, b) -> a + b);
        nativeRegisterCallback(myCallback);
    });
}
// When arena closes, the upcall's native stub is released immediately
The org.lwjgl.system.ffm API is in preview and subject to change between releases. The range of supported QoL transformations (auto-sizing array parameters, return value rewriting, etc.) is currently narrower than the offline Kotlin generator.

Example: custom binding end-to-end

The following shows a complete, minimal custom binding for a hypothetical C library greeter that exports a single function greet(const char* name) -> int:
package com.example.greeter;

import java.lang.foreign.*;
import java.lang.invoke.*;
import org.lwjgl.system.ffm.*;
import static org.lwjgl.system.ffm.FFM.*;

// Step 1: declare the native interface
public interface Greeter {
    int greet(MemorySegment name); // const char* → MemorySegment
}

// Step 2: in your application class, register a config and generate the implementation
public class App {

    static {
        // Associate App's class with the native "greeter" library
        ffmConfig(App.class, ffmConfigBuilder(MethodHandles.lookup())
            .withSymbolLookup(SymbolLookup.libraryLookup("greeter", Arena.global()))
            .build());
    }

    private static final Greeter GREETER = ffmGenerate(Greeter.class);

    public static void main(String[] args) {
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment name = arena.allocateFrom("World");
            int result = GREETER.greet(name);
            System.out.println("greet returned: " + result);
        }
    }
}
For more complete examples, see the modules/samples/src/test/java25/org/lwjgl/demo package in the LWJGL repository, and the struct/union unit tests in modules/lwjgl/core25/src/test/java/org/lwjgl/system/ffm/StructTest.java.

Build docs developers (and LLMs) love