Package org.lwjgl.system.ffm


@NullMarked package org.lwjgl.system.ffm
Experimental FFM-based binding generation at runtime.

The objective is to allow users to create their own LWJGL-style bindings, using a fluent API, dynamically at run-time, without sacrificing performance or worrying about best practices and low-level details.

Several modern Java technologies are being utilized to achieve this:

  • Dynamic Class-File Constants (JEP 309, JDK 11, a.k.a. Condy): Used to lazily initialize bindings, without precompiling a private nested class per function.
  • Hidden Classes (JEP 371, JDK 15): Used to generate classes with minimal metadata and no interaction with ClassLoaders.
  • Class data support for hidden classes (JDK-8256214, JDK 16): Used to boostrap Condy values.
  • Foreign Function & Memory API (JEP 442, JDK 22): Used for native interoperation without JNI.
  • Class-File API (JEP 457, JDK 24): Used to generate bytecode at runtime, without 3rd-party dependencies.

The FFM.ffmGenerate(Class) method is designed to be relatively cheap. The instance returned is lightweight and its methods are never materialized, unless actually used by the application. More specifically:

  • A minimal class is generated that implements the input interface.
  • There is no state in the generated class, only methods.
  • All methods are simple, with minimal bytecode. A method handle is retrieved using Condy and arguments are passed directly to MethodHandle.invokeExact(Object...).
  • There is no overhead from the Condy ldc instruction. It is executed only once, if and when the method is called.
  • Execution of relatively expensive binding code is deferred to the Condy bootstrap. This includes:
    • lookup of FFM* annotations and associated logic
    • function address lookup and creation of FFM method handles
    • further bytecode generation and wrapping of FFM method handles if necessary
  • Hidden class data is used:

When stored in static final fields, performance of the generated bindings is equivalent to direct FFM calls. The JVM can inline through everything, for the following reasons:

  • The binding instance is constant.
  • There is a single implementation of the binding interface.
  • The MethodHandle instances created via Condy are also treated as constants by the JVM.

Downcalls support the specification of 3 virtual parameters, which must be present in a strict order before other parameters:

  • A MemorySegment parameter in methods annotated with FFMFunctionAddress.

    It must be specified before any other virtual or normal parameter.

  • A SegmentAllocator or StackAllocator parameter. This parameter will be used to allocate storage for struct values passed or returned by-value and also for temporary storage needed internally by the method call (e.g. for FFMReturn buffers).

    If the parameter is of type StackAllocator, a stack frame will be pushed & popped inside the method call when temporary storage is needed.

    It must be specified after the FFMFunctionAddress parameter, if one exists, and before any other virtual or normal parameter.

  • A MemorySegment parameter annotated with FFMCaptureCallState.

    It must be specified after other virtual parameters and before any normal parameters.

Upcalls and structs/unions are also supported. They are also defined as interfaces (FunctionalInterface in the case of upcalls), but there are two classes generated:

  • The upcall or struct/union implementation itself.
  • A binder implementation, which is used to allocate and access the corresponding type. The binder instance must be declared as a field in the interface and functions like a factory API for the type.

See UpcallBinder and GroupBinder for details.