# FFI relationships `newFFIRelationship` stores an edge and its payload in a LuaJIT FFI struct. Use it for fixed-size numeric data that a hot loop or native API reads: ```teal local record Follows is tecs.ecs.Relationship delay: number maxDistance: number metamethod __call: function( self, target: integer, delay?: number, maxDistance?: number ): Follows end tecs.ecs.newFFIRelationship({ name = "Follows", container = Follows, fields = { {"delay", "float"}, {"maxDistance", "float"}, }, defaults = {0.5, 100}, }) world:set(follower, Follows(leader, 0.25, 50)) world:set( follower, Follows.new({ target = leader, delay = 0.25, maxDistance = 50, }) ) ``` Use [`newRelationship`](/modules/ecs/relationships/) for a target-only edge or a payload that needs strings, tables, functions, or other Lua values. ## Struct layout Tecs prepends this field to the generated struct: ```c double target; ``` Do not declare `target` in `fields`. Tecs owns the field; callers must treat it as read-only and replace the edge through `world:set`. An entity ID packs a 22-bit slot with a generation, so a 32-bit integer would truncate a valid ID. The positional constructor takes the target first; `.new` reads it from the `target` key. Callers may mutate dense payload fields through `getMut`. For sparse storage, replace the edge through `world:set` instead. Each entry in `fields` contains a C identifier and a type string. Tecs passes the type string to LuaJIT's `ffi.cdef`. Common choices include `float`, `double`, the fixed-width integer types, `bool`, and fixed arrays such as `float[4]`. Registration rejects an empty or duplicate field name, an invalid C identifier, and an empty type. Registration requires `name`, `container`, and `fields`. ## Initialization `fields` defines the positional order and lets Tecs generate `.new`. `defaults` fills omitted payload fields in that same order. `init` runs after Tecs writes the target, generated fields, and defaults: ```teal tecs.ecs.newFFIRelationship({ name = "SafeFollows", container = SafeFollows, fields = { {"delay", "float"}, {"maxDistance", "float"}, }, init = function( edge: SafeFollows, _target: integer, delay: number, maxDistance: number ) edge.delay = math.max(0.1, delay) edge.maxDistance = math.max(1, maxDistance) end, }) ``` An `init` hook needs `fields` or an explicit `new`; otherwise Tecs cannot map a table to the hook's positional arguments. A custom `__call` replaces the generated constructor and does not run `init`. Call shared initialization explicitly from that hook. [Component construction](/modules/ecs/components/construction) covers the shared constructor rules. ## Relationship behavior FFI relationships support `exclusive`, `sparse`, `reverseIndex`, and `cascadeDelete` with the same rules as [other relationships](/modules/ecs/relationships/). Storage changes the payload layout, not query or lifecycle behavior. Snapshots write `target` and every declared field, then restore the edge through `.new(data)`. A custom serializer cannot accompany `transient = true`.