🏗️ Internal Architecture
How input4j uses the Foreign Function & Memory API for native input handling
Table of Contents
Architecture Overview
input4j follows a layered architecture that provides a unified API while leveraging platform-specific native APIs:
FFM API Basics
The Foreign Function & Memory API (standard in Java 22+) provides two key capabilities:
Foreign Function Calls
Call native functions directly from Java without JNI overhead
Foreign Memory Access
Read and write native memory directly with strong safety guarantees
import jdk.incubator.foreign.*;
import jdk.incubator.foreign.MemoryLayouts.*;
public class NativeBridge {
// Load the native library
private static final SymbolLookup LOOKUP = SymbolLookup.loaderLookup();
// Define function descriptor
private static final FunctionDescriptor XINPUT_GET_STATE =
FunctionDescriptor.of(
ValueLayout.JAVA_INT, // return: DWORD (int)
ValueLayout.JAVA_INT, // dwUserIndex
ValueLayout.ADDRESS // pState (pointer)
);
// Get function handle
private static final MethodHandle xInputGetState =
LOOKUP.lookup("XInputGetState")
.orElseThrow()
.bindTo(XINPUT_GET_STATE);
}
Memory Management
FFM API provides safe memory access with automatic bounds checking:
// Allocate native memory for XINPUT_STATE structure
MemorySegment stateBuffer = Arena.global().allocate(16);
// Call native function
int result = (int) xInputGetState.invokeExact(0, stateBuffer);
if (result == 0) { // ERROR_SUCCESS
// Read values from native memory
int packetNumber = stateBuffer.get(ValueLayout.JAVA_INT, 0);
short buttons = stateBuffer.get(ValueLayout.JAVA_SHORT, 4);
byte leftTrigger = stateBuffer.get(ValueLayout.JAVA_BYTE, 6);
byte rightTrigger = stateBuffer.get(ValueLayout.JAVA_BYTE, 7);
// Read gamepad struct at offset 8
short leftX = stateBuffer.get(ValueLayout.JAVA_SHORT, 8);
short leftY = stateBuffer.get(ValueLayout.JAVA_SHORT, 10);
short rightX = stateBuffer.get(ValueLayout.JAVA_SHORT, 12);
short rightY = stateBuffer.get(ValueLayout.JAVA_SHORT, 14);
}
// Memory is automatically freed when Arena is closed
// No manual free() calls needed!
// Define structured memory layout
public static final GroupLayout XINPUT_STATE =
StructLayout(
ValueLayout.JAVA_INT.withName("dwPacketNumber"),
StructLayout(
ValueLayout.JAVA_SHORT.withName("sThumbLX"),
ValueLayout.JAVA_SHORT.withName("sThumbLY"),
ValueLayout.JAVA_SHORT.withName("sThumbRX"),
ValueLayout.JAVA_SHORT.withName("sThumbRY"),
ValueLayout.JAVA_BYTE.withName("bLeftTrigger"),
ValueLayout.JAVA_BYTE.withName("bRightTrigger"),
ValueLayout.JAVA_SHORT.withName("wButtons")
).withName("Gamepad"),
MemoryLayout.paddingLayout(6)
).withName("XINPUT_STATE");
// Access structured data
int packetNumber = state.get(XINPUT_STATE, 0, "dwPacketNumber");
short leftX = state.get(XINPUT_STATE, 0, "Gamepad", "sThumbLX");
Platform Abstraction Layer
input4j uses a plugin-based architecture to support multiple platforms:
public interface InputDevicePlugin {
String getName();
Platform getPlatform();
List<InputDevice> getDevices();
void update(); // Poll for changes
void close();
}
public abstract class AbstractInputDevicePlugin implements InputDevicePlugin {
protected final List<InputDevice> devices = new ArrayList<>();
protected final Map<String, InputComponent> components = new HashMap<>();
@Override
public List<InputDevice> getDevices() {
return Collections.unmodifiableList(devices);
}
protected void registerComponent(String id, ComponentType type) {
InputComponent component = new InputComponent(id, type);
components.put(id, component);
}
}
public class InputDevices {
private static final List<InputDevicePlugin> PLUGINS =
List.of(
new XInputPlugin(), // Windows Xbox
new DirectInputPlugin(), // Windows legacy
new LinuxEventDevicePlugin(), // Linux
new IOKitPlugin() // macOS
);
public static AutoCloseable init() {
List<InputDevice> allDevices = new ArrayList<>();
for (InputDevicePlugin plugin : PLUGINS) {
try {
if (plugin.isAvailable()) {
allDevices.addAll(plugin.getDevices());
}
} catch (Throwable e) {
// Platform not supported, skip
System.err.println("Skipping " + plugin.getName() + ": " + e);
}
}
return () -> allDevices.forEach(InputDevice::close);
}
}
Windows Implementation
Windows uses two APIs: XInput (Xbox controllers) and DirectInput (legacy):
XInput
Modern API for Xbox controllers. 12 buttons, 2 analog sticks, 2 triggers.
DirectInput
Legacy API for older gamepads. More flexible but deprecated.
Linux Implementation
Linux uses the evdev interface for reading input events:
public class LinuxEventDevice {
private final MemorySegment eventFD;
private static final int EV_REL = 0x02; // Relative axis
private static final int EV_ABS = 0x03; // Absolute axis
private static final int EV_KEY = 0x01; // Button
// Event structure: time(8) + type(2) + code(2) + value(4) = 16 bytes
private static final GroupLayout INPUT_EVENT =
StructLayout(
StructLayout(
ValueLayout.JAVA_LONG.withName("tvSec"),
ValueLayout.JAVA_LONG.withName("tvUsec")
).withName("time"),
ValueLayout.JAVA_SHORT.withName("type"),
ValueLayout.JAVA_SHORT.withName("code"),
ValueLayout.JAVA_INT.withName("value")
);
public void readEvents() {
MemorySegment event = Arena.global().allocate(INPUT_EVENT);
// Read from /dev/input/eventX
int bytesRead = read(eventFD, event, 16);
if (bytesRead > 0) {
short type = event.get(INPUT_EVENT, 0, "type");
short code = event.get(INPUT_EVENT, 0, "code");
int value = event.get(INPUT_EVENT, 0, "value");
processEvent(type, code, value);
}
}
}
macOS Implementation
macOS uses the IOKit HID framework:
public class IOKitPlugin extends AbstractInputDevicePlugin {
private MemorySegment manager;
@Override
public void init() {
// Create HID Manager
manager = IOKit.HIDManager_Create(kCFAllocatorDefault, MemoryAddress.NULL);
// Set device matching (gamepads only)
MemorySegment matchingDict = createMatchingDictionary(
IOHIDDeviceUsagePage.GENERIC_DESKTOP,
IOHIDDeviceUsage.JOYSTICK
);
IOKit.HIDManager_SetDeviceMatching(manager, matchingDict);
IOKit.HIDManager_ScheduleWithRunLoop(manager, CFRunLoopGetMain(), 0);
IOKit.HIDManager_Open(manager, 0);
}
private MemorySegment createMatchingDictionary(int usagePage, int usage) {
// Create CFMutableDictionaryRef
MemorySegment dict = IOKit.CFDictionary_CreateMutable(
kCFAllocatorDefault, 0,
CFDictionaryKeyCallBacks, CFDictionaryValueCallBacks
);
// Add matching criteria
// ... (matching code)
return dict;
}
}