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.

LWJGL 3 is a thin, zero-overhead Java layer over native C libraries. It does not wrap native calls behind heavyweight objects or reflection—every Java method in a binding maps directly to a C function, every Struct subclass maps to a C struct layout in off-heap memory, and every callback class turns a Java lambda into a C function pointer the native side can call back. Understanding how the pieces fit together helps you write idiomatic, efficient LWJGL code.

How bindings are generated

LWJGL’s Java binding classes are not hand-written. They are produced by an offline Kotlin-based generator that reads C header descriptions and emits Java source files. The generator lives in the modules/generator module of the LWJGL repository.
1

Header description

A Kotlin template file describes the C API: function signatures, struct layouts, enum values, and calling conventions. Annotations on each parameter declare nullability, ownership, and encoding (e.g., null-terminated UTF-8).
2

Code generation

The generator runs and produces Java source files—one class per module (e.g., GLFW, GL11, VK10)—plus accompanying JNI C glue code and Kotlin-based struct layout helpers.
3

Compilation

The generated Java and C sources are compiled into the LWJGL JARs and native shared libraries you depend on.
You consume only the generated output; you never interact with the generator at runtime unless you use the FFM-based runtime generator (see FFM API support).

Static imports — the idiomatic pattern

Each generated binding class exposes all its functions and constants as public static members. The canonical way to use them is with a wildcard static import, mirroring how C code #includes a header:
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL33.*;
import static org.lwjgl.system.MemoryStack.*;

try (MemoryStack stack = stackPush()) {
    IntBuffer w = stack.mallocInt(1);
    IntBuffer h = stack.mallocInt(1);
    glfwGetWindowSize(window, w, h);   // comes from GLFW.*
    glViewport(0, 0, w.get(0), h.get(0)); // comes from GL33.*
}
This keeps call sites terse and avoids the visual noise of fully-qualified names.

How structs are mapped

Every C struct has a corresponding Java class that extends Struct<SELF>. The struct data lives in off-heap memory at a fixed native address; the Java object is just a lightweight view holding that address.
// Base class in org.lwjgl.system.Struct:
//   - address()  → the native pointer (long)
//   - sizeof()   → C sizeof(struct), in bytes
//   - clear()    → memset the struct to zero
//   - free()     → nmemFree(address()), for heap-allocated structs

// Example: VkApplicationInfo from the Vulkan binding
VkApplicationInfo appInfo = VkApplicationInfo.calloc(stack);
appInfo
    .sType(VK_STRUCTURE_TYPE_APPLICATION_INFO)
    .pApplicationName(stack.UTF8("MyApp"))
    .applicationVersion(VK_MAKE_VERSION(1, 0, 0))
    .apiVersion(VK_API_VERSION_1_3);
Field accessors follow a consistent convention: calling foo() with no arguments is the getter; calling foo(value) is the setter and returns this for chaining.

Struct allocation

Structs can be allocated in three ways:

Struct arrays

Many Vulkan and OpenGL calls take pointers to arrays of structs. LWJGL models these as StructBuffer<SELF, BUFFER>:
try (MemoryStack stack = stackPush()) {
    // Allocate an array of 3 VkClearValue structs on the stack
    VkClearValue.Buffer clearValues = VkClearValue.calloc(3, stack);

    clearValues.get(0).color().float32(stack.floats(0.0f, 0.0f, 0.0f, 1.0f));
    clearValues.get(1).depthStencil().set(1.0f, 0);
}

How function pointers become Java methods

C function pointers (stored in dispatch tables, capability objects, or returned directly by the API) are resolved once when a capability object is created and cached for the lifetime of the context. At the Java call site, the generated method reads the stored pointer and invokes it via JNI or the FFM MethodHandle—no reflection, no lookup overhead per call. For APIs with a global function table (such as Vulkan’s instance/device dispatch tables or OpenAL’s AL context), LWJGL generates a capabilities class:
// OpenGL: capabilities are set on the current context
GL.createCapabilities();
GLCapabilities caps = GL.getCapabilities();
if (caps.GL_ARB_direct_state_access) {
    glNamedBufferStorage(buffer, data, GL_DYNAMIC_STORAGE_BIT);
}

// Vulkan: capabilities are per-instance and per-device
VkInstance instance = ...; // created via vkCreateInstance
VKCapabilitiesInstance instCaps = instance.getCapabilities();

The @NativeType annotation

Throughout the generated sources you will see @NativeType("...") on parameters and return types. This is informational: it documents the original C type that the Java type maps to.
// The int parameter maps to C's GLuint (unsigned 32-bit integer)
public static void glBindTexture(@NativeType("GLenum") int target,
                                 @NativeType("GLuint") int texture) { ... }
The annotation has no runtime effect—it is purely documentation for tooling and human readers.

Callbacks and upcalls

When a native API calls back into Java (e.g., GLFW window resize callbacks, Vulkan debug messengers, OpenAL error handlers), LWJGL generates a Callback subclass. You implement the callback as a Java lambda or anonymous class, and LWJGL internally creates a native function pointer (via libffi on JNI builds, or the FFM Linker on JDK 25+) that the native side can call.
// GLFWErrorCallback is a NativeResource — it must be freed
GLFWErrorCallback errorCB = GLFWErrorCallback.createPrint(System.err);
glfwSetErrorCallback(errorCB);

// Window resize callback using a lambda
GLFWWindowSizeCallback sizeCB = GLFWWindowSizeCallback.create((win, w, h) -> {
    glViewport(0, 0, w, h);
});
glfwSetWindowSizeCallback(window, sizeCB);

// Free callbacks when the window is destroyed
sizeCB.free();
errorCB.free();
Callback objects must be kept alive (not garbage collected) for as long as the native side may invoke them. Store them in a field or call free() only after the native API has been told to stop using the callback.

NativeResource and AutoCloseable

Classes that own native resources (callbacks, shared libraries, capability objects) implement NativeResource, which extends AutoCloseable. You can use them in try-with-resources blocks, or call free() / close() manually.
try (GLFWErrorCallback cb = GLFWErrorCallback.createPrint(System.err)) {
    glfwSetErrorCallback(cb);
    runMainLoop();
    glfwSetErrorCallback(null); // detach before cb is freed
}

Debugging binding errors

The LWJGLX/debug Java agent adds OpenGL and OpenAL error checking after every call, translating error codes into human-readable messages with stack traces. Attach it during development:
java -javaagent:lwjglx-debug-1.0.0.jar -jar myapp.jar
Enable Configuration.DEBUG and Configuration.DEBUG_FUNCTIONS during development. When debug mode is active, LWJGL logs a warning for every function pointer that could not be resolved, making it easy to spot missing extensions or uninitialized capability objects.

Build docs developers (and LLMs) love