Knowledge from the fieldRedM

CFX exports drop function-typed table fields — register plug-ins via globals, not exports

learning:43

CFX exports drop function-typed table fields — register plug-ins via globals, not exports

Context

Designing a plug-in framework where event-type files in the same resource register lifecycle hooks (onActivate, onTick, onComplete, etc.) by passing a table to a registrar:

exports.my_framework:RegisterEvent({
    id = "myevent",
    onActivate = function(ctx) ... end,
    onTick     = function(ctx, dt) ... end,
    -- ... more hook callbacks
})

The trap

CFX exports marshal their arguments through msgpack — even when caller and export both live in the same resource. msgpack has no representation for function values, so every function-typed field in a table argument arrives as nil on the export side. Scalar fields (strings, numbers, booleans) and nested tables survive fine.

Symptom: validation inside RegisterEvent complains that onActivate is required and must be a function, even though the caller has clearly defined it. Adding for k,v in pairs(def) do print(k, type(v)) end inside the export confirms every function key is gone while id, label, weight, etc. survive.

Both colon (exports.foo:Bar(x)) and dot (exports.foo.Bar(x)) syntax exhibit the same behavior — this is about the marshal layer, not the self-arg convention.

The fix

For plug-in APIs that pass hook callbacks, do not use exports. Expose the registrar as a global function (or a module table in _G) and have plug-in files call it directly. Function references stay intact because no msgpack hop happens.

-- framework/server/registry.lua
SdeRegistry = {}
function SdeRegistry.register(def)
    -- def.onActivate is a real function here
end

-- framework/events/myevent.lua  (same resource)
SdeRegistry.register({
    id = "myevent",
    onActivate = function(ctx) ... end,
})

Cross-resource registration with callbacks requires a different protocol entirely — register a string id via export, then send hook invocations over net events with the id as a key.

Verified

Verified on FXServer 1.0.0.25770 (RedM, lua54) by adding a key-dump inside the export validator and observing that all function-typed fields were missing while string/number/table fields were present. Switching the plug-in file to call the global SdeRegistry.register directly resolved it on the first try.

Related native

DUPLICATE_FUNCTION_REFERENCE (0xF4E2079D) exists precisely because raw function refs can't cross runtime/marshal boundaries — it returns a stringified identity you can pass and reconstitute. Useful when you genuinely need cross-resource callbacks.


Tags: exports, plug-in-api, msgpack, marshalling, lua, gotcha
Category: uncategorized
Source: scandi_dynamic_events bring-up
Created: 2026-05-27T19:41:23.968Z

Back to documentation