Skip to content

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.

Modern FFI usage in Lune revolves around three core concepts:

  1. Arenas: Scoped memory allocators that handle cleanup for you.
  2. Pointers: Two-tiered system (RawPointer for byte access, TypedPointer for array access).
  3. Structs: C-ABI compliant schemas that map Lua fields to memory offsets.
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 access
local floats = ffi.cast(raw, "f32")
-- 4. Direct Zero-Copy Access
floats[0] = 10.5
floats[1] = 20.25
print(floats[0]) --> 10.5
-- 5. Load native library with SmartLibrary (recommended)
local C = ffi.ctypes
local Kernel32 = ffi.load("kernel32.dll", {
GetCurrentProcessId = { ret = C.u32 },
Sleep = { args = { C.u32 } },
})
local pid = Kernel32.GetCurrentProcessId()
print("PID:", pid)

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.

Lune provides two distinct pointer types to prevent common arithmetic errors.

Used for generic memory and byte-level manipulation.

  • Arithmetic: ptr + 1 advances by 1 byte.
  • No Indexing: You cannot do ptr[0].
  • Methods: offset(bytes), read(offset, ctype), write(offset, ctype, value)

Used for accessing data arrays.

  • Arithmetic: ptr + 1 advances 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 0
ints[1] = 456 -- Writes at offset 4 (sizeof i32)

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 layout
local Player = ffi.struct({
-- { name, type, arraySize? }
{ "x", "f32" },
{ "y", "f32" },
{ "health", "i32" },
{ "name", "u8", 32 }, -- Fixed array [u8; 32]
})
-- Allocate and View
local raw = arena:alloc(Player.size)
local player = ffi.view(raw, Player)
-- Field Access (Read/Write)
player.x = 100.5
player.health = 50
print(player.x, player.health)

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

Use ffi.load to open native libraries (.dll, .so, .dylib).

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 calls
local pid = Kernel32.GetCurrentProcessId()
Kernel32.Sleep(100)
print(Kernel32.INFINITE)

Use ffi.ctypes for type names instead of string literals:

local C = ffi.ctypes
print(C.u32) -- "u32"
print(C.f64) -- "f64"
print(C.string) -- "string"

Without interface, use lib:call():

local lib = ffi.load("kernel32.dll")
local pid = lib:call("GetCurrentProcessId", "u32", {})
MethodReturn TypeDescription
lib:callInti32Fast path for integers
lib:callDoublef64Fast path for doubles
lib:callVoidvoidNo return value
lib:callStringstringReturns C string

You can create Lua functions that C code can call (“function pointers”).

local cb = ffi.callback(function(a, b)
return a - b
end, "i32", {"i32", "i32"})
-- Pass to C function
lib:call("qsort", "void", {"pointer", "usize", "usize", "pointer"},
arrayPtr, count, 4, cb.ptr)

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 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
TypeSizeAliases
i8, u81 bytechar, byte
i16, u162 bytesshort, ushort
i32, u324 bytesint, uint
i64, u648 byteslong, ulong
f324 bytesfloat
f648 bytesdouble
pointer4/8 bytesvoid*
string4/8 byteschar*
print(ffi.sizeof("i32")) --> 4
print(ffi.alignof("f64")) --> 8

For high-performance memory manipulation, use SIMD-optimized bulk operations:

ffi.fill(ptr, 1024, 0) -- memset
ffi.copy(dst, src, 512) -- memcpy
ffi.unsafe.zero(addr, 1024) -- zero memory
LegacyModernWhy?
ffi.buffer(size)arena:alloc(size)Arenas prevent memory leaks.
ffi.open()ffi.load()Consistent naming convention.
lib:call()SmartLibrary interfaceType-safe, direct calls.
Manual ptr + offsetptr:offset(n)LSP-friendly pointer arithmetic.