Skip to content

FFI

Foreign Function Interface for loading native libraries and zero-copy memory access.

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 access
local ints = ffi.cast(ptr, "i32")
ints[0] = 42
print(ints[0]) -- 42
-- Load native library with SmartLibrary interface (recommended)
local C = ffi.ctypes
local 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.


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)
ParameterTypeDescription
ptrRawPointer | TypedPointer | BufferMemory pointer
offsetnumberByte offset
ctypeCTypeType 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.copy instead of manual loops for large data transfers.

Fills memory with a byte value (SIMD-optimized memset).

ffi.fill(ptr, 1024, 0)

Byte-level arithmetic. Created by arena:alloc().

  • ptr + n → advances by n bytes
  • No indexing - must cast first

Properties:

  • addr - Raw address as number
  • isNull - Whether pointer is null
  • isManaged - Whether bounds-checked

Methods:

  • read(offset, ctype) → any
  • write(offset, ctype, value)
  • offset(bytes) → RawPointer
  • toLightUserData() → any

Stride-based arithmetic with array indexing. Created by ffi.cast().

  • ptr + n → advances by n * sizeof(T) bytes
  • ptr[i] → reads/writes at index
local raw = arena:alloc(100)
local ints = ffi.cast(raw, "i32")
ints[0] = 42
ints[1] = 100
print(ints[0], ints[1]) -- 42, 100

Properties:

  • addr - Raw address
  • stride - Bytes per element
  • isNull - Whether null
  • count - Element count (if known)

Methods:

  • get(index) → T
  • set(index, value)
  • toRaw() → RawPointer

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) -- 44
print(Player:offsetOf("health")) -- 8

StructDefinition Properties:

  • size - Total size in bytes
  • alignment - Struct alignment
  • fieldCount - Number of fields

Methods:

  • offsetOf(name) → number
  • sizeOf(name) → number
  • fields() → { string }

Creates a StructView for field access.

local ptr = arena:alloc(Player.size)
local player = ffi.view(ptr, Player)
player.x = 10.5
player.health = 100
print(player.health) -- 100

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 identifier
  • totalAllocated - Total bytes
  • allocationCount - Number of allocations

Methods:

  • alloc(size) → RawPointer
  • allocAligned(size, align) → RawPointer
  • allocType(ctype) → RawPointer
  • allocArray(ctype, count) → RawPointer
  • reset()

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 mode
local lib = ffi.load("kernel32.dll")
local pid = lib:call("GetCurrentProcessId", "u32", {})
-- SmartLibrary mode (recommended)
local C = ffi.ctypes
local Kernel32 = ffi.load("kernel32.dll", {
GetCurrentProcessId = { ret = C.u32 },
Sleep = { args = { C.u32 } },
GetLastError = { args = {}, ret = C.u32 },
-- Constants
INFINITE = 0xFFFFFFFF,
})
-- Direct function calls
local pid = Kernel32.GetCurrentProcessId()
Kernel32.Sleep(100)
print(Kernel32.INFINITE) -- 4294967295

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

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.

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 CType
  • argTypes: Table of CTypes { "i32", "pointer" }
  • args: Argument values matching types

Optimized call for functions taking zero arguments and returning i32.

local val = lib:callInt("MyFunc")

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)

Optimized call for functions taking zero arguments and returning f64.

local val = lib:callDouble("GetValue")

Optimized call for functions taking zero arguments and returning void.

lib:callVoid("Initialize")

Optimized call for functions taking zero arguments and returning string (char*).

local name = lib:callString("GetName")

Calls a function pointer directly. Useful for callbacks or dynamic function pointers.

lib:callPtr(funcPtr, retType, argTypes, ...args)

Gets a raw pointer to an exported symbol.

local ptr = lib:getSymbol("my_func")

Checks if a symbol exists.

if lib:hasSymbol("my_func") then ... end

Lists all exported symbols. Returns a table of { name: string, ordinal: number? }.

for _, exp in ipairs(lib:listExports()) do
print(exp.name)
end

Unloads the library.

lib:close()

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)

Checks if pointer is null.

if ffi.isNull(ptr) then
print("null pointer")
end

Table containing all C type constants and utilities.

print(ffi.types.int) -- "i32"
print(ffi.types.void) -- "void"

TypeSizeAliases
void0-
bool1-
i81char, int8
u81uchar, byte
i162short, int16
u162ushort, uint16
i324int, int32
u324uint, uint32
i648long, int64
u648ulong, uint64
isizePlatformssize_t, ptrdiff_t
usizePlatformsize_t
f324float
f648double
pointerPlatformptr, void*
stringPlatformcstring, char*
ffi.sizeof("i32") -- 4
ffi.alignof("f64") -- 8

Create Lua functions callable from C.

local callback = ffi.callback(function(a, b)
return a + b
end, "i32", {"i32", "i32"})
print(callback.ptr) -- Function pointer

Properties:

  • ptr - C function pointer (RawPointer)
  • retType - Return type string
  • argCount - Number of arguments
  • isValid - Whether callback is valid

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 address
ffi.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) -- memset
ffi.unsafe.zero(addr, 1024) -- zero memory
ffi.unsafe.copy(dst, src, len) -- memcpy

Methods:

MethodDescription
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

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 iterations
local 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.5
end

[!NOTE] Prefer ffi.arena() for new code.

local buf = ffi.buffer(1024)
buf:write(0, "i32", 42)
print(buf:read(0, "i32")) -- 42

Methods:

  • read(offset, ctype) / write(offset, ctype, value)
  • zero() - Fill with zeros
  • readBytes(offset, len) / writeBytes(offset, bytes)
  • readString(offset?) / writeString(offset, s)
  • slice(offset, size) → Buffer