Foreign Function Interface
The FFI module provides zero-copy memory access and native library loading for high-performance interop with C/C++ libraries.
Unlike traditional FFIs that copy data back and forth, Lune’s FFI gives you direct, typed access to memory using efficient pointers and struct views.
Warning: FFI is inherently unsafe. Writing to the wrong memory address can crash the runtime. Always ensure your pointers and offsets are correct.
The Zero-Copy Workflow
Section titled “The Zero-Copy Workflow”Modern FFI usage in Lune revolves around three core concepts:
- Arenas: Scoped memory allocators that handle cleanup for you.
- Pointers: Two-tiered system (
RawPointerfor byte access,TypedPointerfor array access). - Structs: C-ABI compliant schemas that map Lua fields to memory offsets.
Quick Start
Section titled “Quick Start”local ffi = require("@lune/ffi")
-- 1. Create a memory arena (freed when garbage collected)local arena = ffi.arena()
-- 2. Allocate memory (returns RawPointer)local raw = arena:alloc(1024)
-- 3. Cast to typed pointer for array accesslocal floats = ffi.cast(raw, "f32")
-- 4. Direct Zero-Copy Accessfloats[0] = 10.5floats[1] = 20.25
print(floats[0]) --> 10.5
-- 5. Load native library with SmartLibrary (recommended)local C = ffi.ctypeslocal Kernel32 = ffi.load("kernel32.dll", { GetCurrentProcessId = { ret = C.u32 }, Sleep = { args = { C.u32 } },})
local pid = Kernel32.GetCurrentProcessId()print("PID:", pid)Memory Management (Arenas)
Section titled “Memory Management (Arenas)”Manual memory management (malloc/free) is error-prone. Lune uses Arenas to group allocations together.
local arena = ffi.arena()
-- All these allocations belong to 'arena'local p1 = arena:alloc(64)local p2 = arena:allocArray("i32", 100)
-- When 'arena' goes out of scope and is collected,-- ALL memory is freed automatically. No manual free() needed.Pointer System
Section titled “Pointer System”Lune provides two distinct pointer types to prevent common arithmetic errors.
RawPointer (void*)
Section titled “RawPointer (void*)”Used for generic memory and byte-level manipulation.
- Arithmetic:
ptr + 1advances by 1 byte. - No Indexing: You cannot do
ptr[0]. - Methods:
offset(bytes),read(offset, ctype),write(offset, ctype, value)
TypedPointer (T*)
Section titled “TypedPointer (T*)”Used for accessing data arrays.
- Arithmetic:
ptr + 1advances by sizeof(T). - Indexing:
ptr[0]reads the first element.
local raw = arena:alloc(100)
-- Cast void* -> i32*local ints = ffi.cast(raw, "i32")
ints[0] = 123 -- Writes at offset 0ints[1] = 456 -- Writes at offset 4 (sizeof i32)Struct Mapping
Section titled “Struct Mapping”Instead of manually calculating byte offsets (e.g., ptr + 12), define a Struct schema. Lune calculates standard C layout rules (padding, alignment) for you.
-- Define layoutlocal Player = ffi.struct({ -- { name, type, arraySize? } { "x", "f32" }, { "y", "f32" }, { "health", "i32" }, { "name", "u8", 32 }, -- Fixed array [u8; 32]})
-- Allocate and Viewlocal raw = arena:alloc(Player.size)local player = ffi.view(raw, Player)
-- Field Access (Read/Write)player.x = 100.5player.health = 50
print(player.x, player.health)view:pointTo (Zero-GC Iteration)
Section titled “view:pointTo (Zero-GC Iteration)”Repoint a view to iterate arrays without allocating new views:
local Point = ffi.struct({ {"x", "f32"}, {"y", "f32"} })local array = arena:allocArray("u8", 1000 * Point.size)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.5endLoading Libraries
Section titled “Loading Libraries”Use ffi.load to open native libraries (.dll, .so, .dylib).
SmartLibrary (Recommended)
Section titled “SmartLibrary (Recommended)”Pass an interface table to get direct function access:
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 callslocal pid = Kernel32.GetCurrentProcessId()Kernel32.Sleep(100)print(Kernel32.INFINITE)Type Constants (ffi.ctypes)
Section titled “Type Constants (ffi.ctypes)”Use ffi.ctypes for type names instead of string literals:
local C = ffi.ctypesprint(C.u32) -- "u32"print(C.f64) -- "f64"print(C.string) -- "string"Legacy Mode
Section titled “Legacy Mode”Without interface, use lib:call():
local lib = ffi.load("kernel32.dll")local pid = lib:call("GetCurrentProcessId", "u32", {})Optimized Calls
Section titled “Optimized Calls”| Method | Return Type | Description |
|---|---|---|
lib:callInt | i32 | Fast path for integers |
lib:callDouble | f64 | Fast path for doubles |
lib:callVoid | void | No return value |
lib:callString | string | Returns C string |
Callbacks
Section titled “Callbacks”You can create Lua functions that C code can call (“function pointers”).
local cb = ffi.callback(function(a, b) return a - bend, "i32", {"i32", "i32"})
-- Pass to C functionlib:call("qsort", "void", {"pointer", "usize", "usize", "pointer"}, arrayPtr, count, 4, cb.ptr)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 certain about memory validity.
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) -- memcpyType System
Section titled “Type System”| Type | Size | Aliases |
|---|---|---|
i8, u8 | 1 byte | char, byte |
i16, u16 | 2 bytes | short, ushort |
i32, u32 | 4 bytes | int, uint |
i64, u64 | 8 bytes | long, ulong |
f32 | 4 bytes | float |
f64 | 8 bytes | double |
pointer | 4/8 bytes | void* |
string | 4/8 bytes | char* |
print(ffi.sizeof("i32")) --> 4print(ffi.alignof("f64")) --> 8Bulk Operations
Section titled “Bulk Operations”For high-performance memory manipulation, use SIMD-optimized bulk operations:
ffi.fill(ptr, 1024, 0) -- memsetffi.copy(dst, src, 512) -- memcpyffi.unsafe.zero(addr, 1024) -- zero memoryMigration Guide
Section titled “Migration Guide”| Legacy | Modern | Why? |
|---|---|---|
ffi.buffer(size) | arena:alloc(size) | Arenas prevent memory leaks. |
ffi.open() | ffi.load() | Consistent naming convention. |
lib:call() | SmartLibrary interface | Type-safe, direct calls. |
Manual ptr + offset | ptr:offset(n) | LSP-friendly pointer arithmetic. |