FFI
Foreign Function Interface for loading native libraries and zero-copy memory access.
Quick Start
Section titled “Quick Start”local ffi = require("@lune/ffi")
-- Create memory arena (auto-cleanup)local arena = ffi.arena()local ptr = arena:alloc(1024)
-- Cast to typed pointer for array accesslocal ints = ffi.cast(ptr, "i32")ints[0] = 42print(ints[0]) -- 42
-- Load native library with SmartLibrary interface (recommended)local C = ffi.ctypeslocal Kernel32 = ffi.load("kernel32.dll", { GetCurrentProcessId = { ret = C.u32 }, Sleep = { args = { C.u32 } }, INFINITE = 0xFFFFFFFF,})
local pid = Kernel32.GetCurrentProcessId()print("PID:", pid)Kernel32.Sleep(100)[!TIP] Use
ffi.load(path, interface)for type-safe function bindings with direct call syntax.
Memory Access
Section titled “Memory Access”Creates a scoped memory arena. All allocations are freed when GC’d.
local arena = ffi.arena()local ptr = arena:alloc(256)Reads a primitive from memory.
local value = ffi.read(ptr, offset, ctype)| Parameter | Type | Description |
|---|---|---|
| ptr | RawPointer | TypedPointer | Buffer | Memory pointer |
| offset | number | Byte offset |
| ctype | CType | Type to read |
Writes a primitive to memory.
ffi.write(ptr, offset, ctype, value)Copies memory (SIMD-optimized memcpy).
ffi.copy(dst, src, len)[!TIP] Use
ffi.copyinstead of manual loops for large data transfers.
Fills memory with a byte value (SIMD-optimized memset).
ffi.fill(ptr, 1024, 0)Pointer System
Section titled “Pointer System”RawPointer (void*)
Section titled “RawPointer (void*)”Byte-level arithmetic. Created by arena:alloc().
ptr + n→ advances by n bytes- No indexing - must cast first
Properties:
addr- Raw address as numberisNull- Whether pointer is nullisManaged- Whether bounds-checked
Methods:
read(offset, ctype)→ anywrite(offset, ctype, value)offset(bytes)→ RawPointertoLightUserData()→ any
TypedPointer (T*)
Section titled “TypedPointer (T*)”Stride-based arithmetic with array indexing. Created by ffi.cast().
ptr + n→ advances by n * sizeof(T) bytesptr[i]→ reads/writes at index
local raw = arena:alloc(100)local ints = ffi.cast(raw, "i32")ints[0] = 42ints[1] = 100print(ints[0], ints[1]) -- 42, 100Properties:
addr- Raw addressstride- Bytes per elementisNull- Whether nullcount- Element count (if known)
Methods:
get(index)→ Tset(index, value)toRaw()→ RawPointer
Struct System
Section titled “Struct System”struct
Section titled “struct”Defines a C-ABI struct layout.
local Player = ffi.struct({ -- { Name, Type, [ArraySize] } { "x", "f32" }, { "y", "f32" }, { "health", "i32" }, { "name", "u8", 32 }, -- Fixed array})
print(Player.size) -- 44print(Player:offsetOf("health")) -- 8StructDefinition Properties:
size- Total size in bytesalignment- Struct alignmentfieldCount- Number of fields
Methods:
offsetOf(name)→ numbersizeOf(name)→ numberfields()→ { string }
Creates a StructView for field access.
local ptr = arena:alloc(Player.size)local player = ffi.view(ptr, Player)
player.x = 10.5player.health = 100print(player.health) -- 100Memory Arenas
Section titled “Memory Arenas”Scoped allocator with automatic cleanup.
local arena = ffi.arena()local p1 = arena:alloc(1024)local p2 = arena:allocArray("f32", 100)
print(arena.totalAllocated)arena:reset() -- Free all[!WARNING] Arenas are NOT thread-safe. Do not share between Luau Actors.
Properties:
id- Unique identifiertotalAllocated- Total bytesallocationCount- Number of allocations
Methods:
alloc(size)→ RawPointerallocAligned(size, align)→ RawPointerallocType(ctype)→ RawPointerallocArray(ctype, count)→ RawPointerreset()
Library Loading
Section titled “Library Loading”Loads a native library (.dll, .so, .dylib).
Without interface - Returns legacy Library for lib:call() usage.
With interface - Returns SmartLibrary for direct function access.
-- Legacy modelocal lib = ffi.load("kernel32.dll")local pid = lib:call("GetCurrentProcessId", "u32", {})
-- SmartLibrary mode (recommended)local C = ffi.ctypeslocal Kernel32 = ffi.load("kernel32.dll", { GetCurrentProcessId = { ret = C.u32 }, Sleep = { args = { C.u32 } }, GetLastError = { args = {}, ret = C.u32 }, -- Constants INFINITE = 0xFFFFFFFF,})
-- Direct function callslocal pid = Kernel32.GetCurrentProcessId()Kernel32.Sleep(100)print(Kernel32.INFINITE) -- 4294967295ctypes
Section titled “ctypes”Type name constants for use in function signatures.
local C = ffi.ctypes
print(C.u32) -- "u32"print(C.f64) -- "f64"print(C.pointer) -- "pointer"print(C.string) -- "string"Available: void, bool, i8, u8, i16, u16, i32, u32, i64, u64, f32, f64, isize, usize, pointer, string
SmartLibrary
Section titled “SmartLibrary”Returned by ffi.load(path, interface). Provides direct function access.
Properties:
path- Library path
Methods:
close()- Unload library
Dynamic fields: Functions and constants from interface
[!CAUTION] Deprecated: Use
ffi.load()instead.
Legacy alias for ffi.load.
Library (Legacy)
Section titled “Library (Legacy)”Object representing a loaded library.
Properties:
path(string): The path to the loaded library file.
Methods:
Calls a function with an arbitrary signature.
lib:call(name, retType, argTypes, ...args)name: Symbol name (string)retType: Return CTypeargTypes: Table of CTypes{ "i32", "pointer" }args: Argument values matching types
callInt
Section titled “callInt”Optimized call for functions taking zero arguments and returning i32.
local val = lib:callInt("MyFunc")callIntArg
Section titled “callIntArg”Optimized call for functions taking one i64 argument and returning i32. (Note: Argument is cast to C int/long depending on platform, usually passed as value).
local val = lib:callIntArg("MyFunc", 123)callDouble
Section titled “callDouble”Optimized call for functions taking zero arguments and returning f64.
local val = lib:callDouble("GetValue")callVoid
Section titled “callVoid”Optimized call for functions taking zero arguments and returning void.
lib:callVoid("Initialize")callString
Section titled “callString”Optimized call for functions taking zero arguments and returning string (char*).
local name = lib:callString("GetName")callPtr
Section titled “callPtr”Calls a function pointer directly. Useful for callbacks or dynamic function pointers.
lib:callPtr(funcPtr, retType, argTypes, ...args)getSymbol
Section titled “getSymbol”Gets a raw pointer to an exported symbol.
local ptr = lib:getSymbol("my_func")hasSymbol
Section titled “hasSymbol”Checks if a symbol exists.
if lib:hasSymbol("my_func") then ... endlistExports
Section titled “listExports”Lists all exported symbols. Returns a table of { name: string, ordinal: number? }.
for _, exp in ipairs(lib:listExports()) do print(exp.name)endUnloads the library.
lib:close()Pointer Operations
Section titled “Pointer Operations”Creates a RawPointer from numeric address.
local ptr = ffi.ptr(0x12345678)Casts to typed pointer or struct view.
local ints = ffi.cast(raw, "i32")local player = ffi.cast(raw, PlayerDef)isNull
Section titled “isNull”Checks if pointer is null.
if ffi.isNull(ptr) then print("null pointer")endTable containing all C type constants and utilities.
print(ffi.types.int) -- "i32"print(ffi.types.void) -- "void"Type Reference
Section titled “Type Reference”| Type | Size | Aliases |
|---|---|---|
| void | 0 | - |
| bool | 1 | - |
| i8 | 1 | char, int8 |
| u8 | 1 | uchar, byte |
| i16 | 2 | short, int16 |
| u16 | 2 | ushort, uint16 |
| i32 | 4 | int, int32 |
| u32 | 4 | uint, uint32 |
| i64 | 8 | long, int64 |
| u64 | 8 | ulong, uint64 |
| isize | Platform | ssize_t, ptrdiff_t |
| usize | Platform | size_t |
| f32 | 4 | float |
| f64 | 8 | double |
| pointer | Platform | ptr, void* |
| string | Platform | cstring, char* |
sizeof / alignof
Section titled “sizeof / alignof”ffi.sizeof("i32") -- 4ffi.alignof("f64") -- 8Callbacks
Section titled “Callbacks”Create Lua functions callable from C.
local callback = ffi.callback(function(a, b) return a + bend, "i32", {"i32", "i32"})
print(callback.ptr) -- Function pointerProperties:
ptr- C function pointer (RawPointer)retType- Return type stringargCount- Number of argumentsisValid- Whether callback is valid
Unsafe Intrinsics
Section titled “Unsafe Intrinsics”Direct memory access without safety checks. Maximum performance for hot paths.
[!CAUTION] No null checks, no bounds checks! Use only when you’re certain about memory validity.
local arena = ffi.arena()local ptr = arena:alloc(1024)local addr = ptr.addr
-- Direct read/write by addressffi.unsafe.write(addr, "i32", 12345)local val = ffi.unsafe.read(addr, "i32")
-- Bulk operations (SIMD-optimized, 5-15 GB/s)ffi.unsafe.fill(addr, 1024, 0xFF) -- memsetffi.unsafe.zero(addr, 1024) -- zero memoryffi.unsafe.copy(dst, src, len) -- memcpyMethods:
| Method | Description |
|---|---|
read(addr, ctype) | Read value from address |
write(addr, ctype, value) | Write value to address |
copy(dst, src, len) | Copy memory (no overlap check) |
fill(addr, len, byte) | Fill memory with byte |
zero(addr, len) | Zero memory |
StructView Methods
Section titled “StructView Methods”pointTo
Section titled “pointTo”Repoints a view to a different address. Zero allocations - ideal for iterating arrays.
local arena = ffi.arena()local Point = ffi.struct({ {"x", "f32"}, {"y", "f32"} })local array = arena:allocArray("u8", 1000 * Point.size)
-- Create one view, reuse for all iterationslocal view = ffi.view(array, Point)
for i = 0, 999 do view:pointTo(array:offset(i * Point.size)) view.x = i * 0.5 view.y = i * 1.5endBuffer (Legacy)
Section titled “Buffer (Legacy)”[!NOTE] Prefer
ffi.arena()for new code.
local buf = ffi.buffer(1024)buf:write(0, "i32", 42)print(buf:read(0, "i32")) -- 42Methods:
read(offset, ctype)/write(offset, ctype, value)zero()- Fill with zerosreadBytes(offset, len)/writeBytes(offset, bytes)readString(offset?)/writeString(offset, s)slice(offset, size)→ Buffer