FFI
FFI (Foreign Function Interface) is LuaJIT’s mechanism for calling C functions from Lua. X4’s Lua sandbox exposes engine C functions via the global ffi table — modders use ffi.C.X() to call them directly without going through Helper or MD events.
FFI is the fastest way to query engine state from Lua (no MD round-trip), but its surface is largely undocumented and the X4 9.x sandbox restricts some calls — see Common gotchas.
Basic usage
Section titled “Basic usage”local ffi = require("ffi")local C = ffi.C
-- Get the player's IDlocal playerID = C.GetPlayerID()
-- Read object datalocal data = C.GetComponentData(component, "knownname")Exposed function families
Section titled “Exposed function families”Vanilla ui/addons/*/menu_*.lua files reveal the available functions. Common families:
| Family | Examples | Purpose |
|---|---|---|
GetComponentData | GetComponentData(component, "X") | Read any object property |
GetPlayerID | GetPlayerID() | Player’s MD ID |
GetMacroData | GetMacroData(macro, "X") | Read macro data |
GetSectorOwner | GetSectorOwner(sector) | Sector ownership |
GetFactionData | GetFactionData(faction, "X") | Faction props |
GetWareData | GetWareData(ware, "X") | Ware props (⚠ 9.x restricted) |
IsValidComponent | IsValidComponent(c) | Null-check |
IsObjectDocked | IsObjectDocked(o) | Dock state |
Helper.C | various | Higher-level wrappers |
The exposed surface is hundreds of functions. No formal catalog exists.
Common patterns
Section titled “Common patterns””Read object data quickly”
Section titled “”Read object data quickly””local function getShipName(component) if C.IsValidComponent(component) then return ffi.string(C.GetComponentName(component)) end return "(invalid)"endffi.string converts a C string return into a Lua string.
”Use struct-style access for object data”
Section titled “”Use struct-style access for object data””local data = ffi.new("ComponentData[1]")C.GetComponentData2(component, data)print(data[0].knownname)Some engine functions write into out-parameter structs. Define the struct via ffi.cdef if not already exposed.
”Wrap FFI in a try-catch”
Section titled “”Wrap FFI in a try-catch””local function safeCall(fn, ...) local ok, result = pcall(fn, ...) if not ok then DebugError("FFI call failed: " .. tostring(result)) return nil end return resultend
local name = safeCall(C.GetComponentName, component)X4 9.x throws on some restricted calls — wrap to avoid crashes.
Common gotchas
Section titled “Common gotchas”- ⚠
C.GetWareDatais RESTRICTED in X4 9.x. Direct calls trigger sandbox errors.pcalldoesn’t hide the log error and doesn’t save the calling event handler. Resolve ware names MD-side and push viaraise_lua_eventwire. (Memory:x4_lua_ffi_getwaredata_restricted.) - ⚠ The X4 9.x sandbox likely restricts more
GetXDatafamily functions. Test each function; if it errors in the log, find an MD-side workaround. - ⚠
ffi.stringis REQUIRED for converting C-string returns to Lua strings. Skipping it gives you acdata*charthat doesn’t compare equal to anything. - ⚠ FFI is fast but DANGEROUS. Engine functions don’t validate inputs. Passing a null or invalid component crashes the engine — not just the Lua script.
- ⚠ No documentation. Read vanilla menu Lua files for usage. Vanilla
menu_object.lua,menu_map.lua, etc. show the patterns. - ⚠
ffi.Cis the same as rawCin most contexts. Some scripts uselocal C = ffi.Cto shorten access — convention only. - ⚠ Hot-reload of FFI calls may not work cleanly. After a reload, cached FFI references may be stale. Re-initialize on
init().
Examples
Section titled “Examples”Example 1: Read object name safely
Section titled “Example 1: Read object name safely”local function getName(component) if not C.IsValidComponent(component) then return nil end return ffi.string(C.GetComponentName(component))endExample 2: Fallback for restricted FFI
Section titled “Example 2: Fallback for restricted FFI”Memory note: C.GetWareData is restricted in 9.x. Instead of calling FFI:
<!-- MD side: pre-resolve ware data, push to Lua --><raise_lua_event name="'MyMod.WareDataPush'" param="$ware.name"/>-- Lua side: receive resolved data via eventRegisterEvent("MyMod.WareDataPush", function(_, wareName) storedWareName = wareNameend)Pre-compute MD-side; transport via the event bus. Avoids the sandbox.
Example 3: Defensive FFI wrapper
Section titled “Example 3: Defensive FFI wrapper”local function safeFFI(callable, ...) local args = {...} local ok, result = pcall(function() return callable(table.unpack(args)) end) if not ok then DebugError("FFI failed: " .. tostring(result)) return nil end return resultendUse for FFI calls that may throw.
Architectural context
Section titled “Architectural context”- FFI is LuaJIT-native. X4 ships with LuaJIT; standard Lua doesn’t have FFI built-in.
- C function discovery: vanilla menu files have
ffi.cdef[[...]]blocks that declare C signatures. Modders reuse these or read existing definitions. - 9.x sandbox tightening: Egosoft progressively restricts FFI access as security model evolves. Test mods against each version.
Related
Section titled “Related”- Helper API — high-level UI alternative.
- Globals — MD-bridge alternative.
- SN APIs — wrappers for common FFI patterns.