Skip to content

FFI Components

FFI (Foreign Function Interface) components leverage LuaJIT's FFI capabilities to provide high-performance, cache-friendly component storage using C structs instead of Lua tables. Use FFI components if your data is mostly made up of numbers, booleans, and other primitive types that map cleanly to C structs. Otherwise, use normal table-based components.

Shared constructor rules live in Component Construction. This page focuses on FFI-specific storage behavior and field types.

Basic usage

Define FFI components using tecs.newFFIComponent:

teal
local tecs = require("tecs")

local record Velocity is tecs.Component
    x: number
    y: number
end

tecs.newFFIComponent({
    name = "Velocity",
    container = Velocity,
    fields = {
        {"x", "float"},
        {"y", "float"}
    }
})

Field types

FFI components support all standard C types:

Numeric types

  • Integers: int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t
  • Floating Point: float, double, long double
  • Standard C: char, short, int, long, size_t, ptrdiff_t
  • Boolean: bool, _Bool

Pointer types

  • Generic: void*
  • String: char*, const char*
  • Numeric: int*, float*, double*

Fixed-size arrays

You can define fixed-size arrays by appending [size] to any type:

  • Numeric Arrays: float[16], int32_t[4], uint8_t[256]
  • Matrix/Vector: float[3] for vec3, float[16] for mat4
  • Buffers: char[256] for string buffers

Constructor support

FFI components use the same __call(...) / .new(data) / fields / defaults / init model as table components. The FFI-specific difference is that the base instance is an FFI struct instead of a Lua table.

teal
-- Define component
local record Position is tecs.Component
    x: number
    y: number
    z: number
    metamethod __call: function(self, x?: number, y?: number, z?: number): Position
end

tecs.newFFIComponent({
    name = "Position",
    container = Position,
    fields = {
        {"x", "float"},
        {"y", "float"},
        {"z", "float"}
    }
})

-- Use with positional arguments
local pos1: Position = Position(10, 20)      -- z defaults to 0
local pos2: Position = Position(10, 20, 30)  -- all values provided

Default values

FFI components support explicit defaults, just like table components:

teal
tecs.newFFIComponent({
    name = "Position",
    container = Position,
    fields = {
        {"x", "float"},
        {"y", "float"},
        {"z", "float"}
    },
    defaults = {0, 0, 0},
})

Fields with no explicit default remain zero-initialized by the FFI allocator. That means omitted values fall back to the underlying FFI zero value:

API compatibility

FFI components share the same API as table-based components. Field access, construction, and mutation work identically regardless of the underlying storage:

teal
local vel: Velocity = Velocity(10, 20)
vel.x = vel.x + 5
print(vel.x, vel.y)  -- 15, 20

Limitations

For more details on LuaJIT FFI limitations and semantics, see the official LuaJIT FFI documentation and FFI semantics.

API reference

tecs.newFFIComponent(options)

Creates an FFI-based component with optimized memory layout and optional recycling.

Parameters:

  • options

The options table supports the following properties:

ParameterTypeDescriptionRequired
namestringComponent nameYes
containerComponentComponent container/typeYes
fields{ {string, string} }Array of field tuples {name, type}Yes
metatabletableMetatable to apply to FFI instances for adding instance methodsNo
defaults{any}Default positional values, in the same order as fieldsNo
initfunctionValidation and initialization hook (positional args only; .new routes through it after unpacking)No
__callfunctionCustom constructor hook. Receives an allocated instance plus the call args after defaults are applied. init is not auto-run on this path.No
newfunction(data: {string: any}): ComponentOverride the auto-codegenned table-form constructor. Defaults to a field-name unpacker through __call.No
requires{Component}Components to auto-add alongside this one (see Auto-dependencies)No
serializefunction(instance: Component): {string: any}Custom serializer for durable data. Mutually exclusive with transient.No
deserializefunction(world: tecs.World, data: {string: any}): ComponentCustom deserializerNo
transientbooleanIf true, omit this component from snapshots. Mutually exclusive with serialize.No

To run code when the component is added to or removed from an entity, attach query callbacks (onEntitiesAdded / onEntitiesRemoved) to a query that includes the component.

Returns:

  • The created FFI Component

Example:

teal
local record Velocity is tecs.Component
    x: number
    y: number
    speed: {number, number}
end

tecs.newFFIComponent({
    name = "Velocity",
    container = Velocity,
    fields = {
        {"x", "float"},
        {"y", "float"},
        {"speed", "float[2]"}
    }
})

Init hooks

FFI components can include an optional init hook for validation and derived state:

teal
init = function(instance: Component, ...: any)

The init hook receives the allocated instance plus the positional arguments. When a caller uses Component.new({...}), the framework unpacks the table by field name into positional args before calling init, so the hook never sees a table in its first slot and doesn't need a type(x) == "table" fork.

Custom __call

For unusual FFI components whose public constructor arguments do not line up with their raw struct fields, provide config __call(instance, ...).

On that path, Tecs allocates the FFI instance, applies defaults, and then calls your hook. It does not auto-run init afterwards, so call Component.init(...) explicitly if you want to reuse init logic.

For example:

teal
local record Health is tecs.Component
    current: integer
    maximum: integer
end

tecs.newFFIComponent({
    name = "Health",
    container = Health,
    fields = {
        {"current", "int32_t"},
        {"maximum", "int32_t"}
    },
    init = function(
        instance: Health,
        current: integer,
        maximum: integer
    )
        if current < 0 then
            error("Health current cannot be negative")
        end
        if maximum <= 0 then
            error("Health maximum must be positive")
        end
    end
})

Use defaults for static default values; use init for validation, normalization, and derived state.