Skip to main content

Il2Cpp Dispatch* Indexer

Source: mt/TypeRegistryCommon.cpp, mt/MetaBinding.cpp Semantic authority: ../../spec/metatable/02-INDEX.md

1. Path

Il2Cpp Player member indexing uses native Dispatch* C closures: __index / __newindex with one upvalue (TypeBinding* lightuserdata).

TypeRegistryCommon.cpp fills IMT/SMT by calling FillInstanceMetatable(..., DispatchInstanceIndex, DispatchInstanceNewIndex).

Static type table SMT uses symmetric DispatchStaticIndex / DispatchStaticNewIndex (lookup binding->staticMap).

2. Core data structures

2.1 TypeBinding (MetaBinding.h)

FieldUse
Il2CppClass* klassDeclared type
NameMetaMap staticMapStatic members: name → MetaInfo
NameMetaMap byobjInstanceMapReference / struct ByObj instance members
NameMetaMap byvalInstanceMapstruct ByVal instance members
MethodGroups* ctorGroupsConstructor overload (not via indexer)

NameMetaMap = AppendOnlyStringHashMap<MetaInfo>: written at bind time, read-only at runtime; key is member name string (Il2Cpp persisted name).

2.2 MetaInfo

enum class MetaKind { Method, Field, Property }; // Event removed

struct MetaInfo {
MetaKind kind;
union {
struct { int closureRef; } method;
FieldMarshalCtx field;
PropertyMarshalCtx property;
};
};
  • Method: closureRef is a registry direct bridge or dispatch closure.
  • Field: FieldMarshalCtx (offset + MarshalMetaInfo* + cs2luaWriter).
  • Property: PropertyMarshalCtx (getter/setter function pointers + sealed flags).

3. DispatchInstanceIndex algorithm

Template parameters:

  • ResolveInstanceMethodTargetFn — resolve this (ByVal / ByObj value / reference type).
  • MetaTableKind — choose byvalInstanceMap or byobjInstanceMap.
template <ResolveInstanceMethodTargetFn resolveMethodTarget, MetaTableKind kind>
static int DispatchInstanceIndex(lua_State* L)
{
TypeBinding* binding = (TypeBinding*)lua_touserdata(L, lua_upvalueindex(1));
const char* key = luaL_checkstring(L, 2);
const MetaInfo* info = LookupMeta(map, key);
if (info == nullptr) {
lua_pushnil(L);
return 1;
}
switch (info->kind) {
case MetaKind::Method:
return LuaUtil::PushRef(L, info->method.closureRef);
case MetaKind::Field:
writer(L, fieldPtr, meta); return 1;
case MetaKind::Property:
PropertyBridge::InvokeGetter(L, target, &info->property); return 1;
}
}

Spec alignment:

Spec requirementDispatch* behavior
method returns closure directlyPushRef(closureRef), no call
field/property auto-invoke getterCall writer/getter inside C closure
__index misslua_pushnil
method preferred over same-named fieldResolved when MetaBinding writes map; single-table lookup

3.1 Instance variant entry points

C closureMapthis resolution
InstanceByValIndexbyvalInstanceMapResolveByValMethodTarget
InstanceByObjIndexbyobjInstanceMap (boxed value type)ResolveByObjValueTypeMethodTarget
InstanceReferenceIndexbyobjInstanceMapResolveByObjReferenceMethodTarget

FillInstanceMetatable binds the matching Index/NewIndex pair when creating IMT.

4. DispatchInstanceNewIndex

const MetaInfo* info = LookupMeta(map, key);
if (info == nullptr)
return luaL_error(L, "zlua: member not found: %s", key);
// Field → lua2csWriter; Property → PropertyBridge::InvokeSetter
// Method / no setter → error

miss → strict error (per spec). Read-only field (cs2luaWriter == nullptr on write attempt) errors in the field branch.

5. Bind time: MetaBinding

MetaBinding::EnsureBinding(Il2CppClass*) on first import_type / type touch:

  1. Flatten public instance/static members into three maps.
  2. Create MethodMarshalCtx per method; single overload → CreateDirectMethodClosureRef; multi → CreateMethodDispatchClosureRef + MethodGroups.
  3. Write Field/Property into MetaInfo; Dispatch* looks up the same map at runtime.

Event: No MetaKind::Event; add_* / remove_* enter the map as ordinary methods.

Direct method closure upvalue layout:

  • [1] ClosureKind (number)
  • [2] TypeBinding*
  • [3] MethodMarshalCtx*

Dispatch closures also carry MethodGroups* upvalue.

6. Cooperation with TypeRegistryCommon

FunctionResponsibility
WriteCommonTypeFields__fullname, __typeid, __klass, etc.
RegisterStaticLiteralFieldsenum/const written directly on type table T
FillInstanceMetatable__type, __udkind, __gc, Dispatch*
AttachStaticMetatableSMT + __call (ctor dispatch)
PushTypeTableEnsure MetaBinding::EnsureBinding has run

MetaTableCache caches (Il2CppClass* → IMT ref, SMT ref) to avoid rebinding tables.

7. Hot-path cost

Each obj.field / obj:Method member fetch via Dispatch* has fixed overhead:

  1. Lua: look up mt → call C __index closure
  2. C: lua_touserdata + luaL_checkstring + LookupMeta (string hash)
  3. Method extra: lua_rawgeti(REGISTRY, closureRef)

Note: This path does not change Lua-visible semantics in ../../spec/metatable/02-INDEX.md; only the implementation mechanism differs.

8. Debugging

  • LookupMeta miss returning nil does not call C# reflection fallback (spec forbids).
  • When comparing with Mono, Il2Cpp and Mono miss/member sets should match (Mono uses Lua three-table indexer; see INDEXER-MONO.md.
FileContents
mt/TypeRegistryCommon.cppDispatch* templates, FillInstanceMetatable
mt/MetaBinding.cppMap fill, closure ref creation
mt/InstanceTarget.cppthis / field addresses
bridge/PropertyBridge.cppgetter/setter invoke
marshal/MarshalMeta.cppField writer function pointers