Skip to main content

Class / Interface Marshal

Normative: Default Marshal rules for reference types (class, interface, string, array instances, delegate instances, etc.) between Lua and C#. Related: Type tables and member access → ../02-TYPE-SYSTEM.md; ref/out/in overview → 03-BYREF.md; [LuaMarshalAs]02-MARSHAL-AS.md; zlua.cast../05-LIB.md.

Platform principle: Mono and Il2Cpp share the same Lua-visible semantics; class instances default to GCHandle + full userdata (Il2Cpp: ObjectRegistry + Il2CppObject*).

1. Default Marshal (summary)

Without [LuaMarshalAs], reference types follow the overview matrix in 01-OVERVIEW.md; this section adds class / interface semantics.

DirectionLua shapeNotes
C# → LuaClassUserData (ByObj full userdata)Reference identity; IMT façade = declared type (see §2)
Lua → C#ClassUserData or nilMust be assignable to target declared type; nilnull
stringLua string or nilOnly when declared type is string; declared as object → still Object userdata
interfaceSame as classFaçade = interface declared type (not implementing class); see §2, §4
ArrayByObjUserDataSee 07-ARRAY.md
Delegatefunction or DelegateUserDataSee 09-FUNCTION.md

UserData shape: ClassUserData is full userdata from lua_newuserdata + instance metatable IMT; scripts access members via : / .. Differs from OpaqueValue in 04-OPAQUE.md (lightuserdata, no metatable) and Pointer in 10-POINTER.md.

Field, method, and static member access details: ../02-TYPE-SYSTEM.md and ../metatable/.

2. Declared-type façade (View vs actual type)

For all reference type parameters, return values, and fields/properties (class / interface / object / arrays / delegates, etc.), Marshal distinguishes Identity and View:

ConceptMeaning
IdentityManaged object reference in userdata payload (runtime actual instance)
View / façadeIMT and member visibility on userdata; sole source = declared type of this Marshal

2.1 Rules

  1. C# → Lua: Always choose default marshal shape and ByObj IMT by declared type; do not reattach a more specific runtime type's mt, and do not switch to special Marshal such as string (e.g. a string instance on an object parameter remains Object userdata, not Lua string).
  2. Value types: No inheritance-façade issue; follow 05-STRUCT.md etc.
  3. Virtual methods: Member lookup uses MethodInfo on the declared type; call does vtable dispatch on the real this (override still lands on the implementing class).
  4. Non-virtual / new hide: Use declared-type slot (calling new-hidden Bar via Base façade yields Base.Bar).
  5. Downcast: Only zlua.cast (IsAssignableFrom(targetType, obj.klass)); returns new userdata (same identity, new façade).
  6. Object cache: Key is (identity, viewType); one instance may have multiple view userdata.

2.2 Example

Base CreateChild() => new Child();
local o = ObjectFactory.CreateChild() -- façade Base: Child.y invisible; new Bar → Base.Bar
local c = zlua.cast(o, Child) -- façade Child (requires IsAssignableFrom)

Virtual methods look up MethodInfo via Base façade, then virtual-dispatch on the real instance.

2.3 object parameters

ItemRules
PushClassUserData, façade System.Object
PopAccepts boolean / number / string / userdata
Runtime typeDoes not rewrite Marshal by runtime type (runtime string stays Object userdata)

For Nullable<T> where T is a reference type, nullnil; when present, same class rules as T. See 01-OVERVIEW.md.

3. Table / UnpackedValues (not for class / interface)

class and interface disallow [LuaMarshalAs(Table)] / [LuaMarshalAs(UnpackedValues)] (legal set: 02-MARSHAL-AS.md §3). Reference types keep default ByObjUserData and member access.

On misuse: Mono error-logs and falls back to Default; Il2Cpp Generate / XML may hard-fail.

4. Interface Marshal

ItemRules
Default C# ↔ LuaByObjUserData (ClassUserData); façade = interface declared type (not implementing class)
nilnull
[LuaMarshalAs]UserData, OpaqueValue (no Table / UnpackedValues)
Member access (default userdata)Only members visible on the interface + inherited interfaces; implementing-class-only members invisible

5. ref / out / in reference-type parameters (Lua → C#)

Overview: 03-BYREF.md. Reference types (including string) points:

Lua argumentBehavior
OpaqueValue (compatible with A)Pass handle address (can write back to original slot)
ByObjUserData / Lua string (only when A is string) / nil / other Pop-able shapesObtain managed pointer (or null) → write stack temporary → pass temp address

5.1 Write-back: temp slot ⇒ no rebind

C# operationLua side (temp-slot path)
refParam = otherObject (rebind)Not visible
In-place mutation of a mutable objectVisible (same managed object)
Assign new string to ref stringNot visible (same as rebind)
local s = "hello"
CS.Demo.ChangeString(s) -- void ChangeString(ref string s) { s = "world"; }
-- s is still "hello"

local sb = StringBuilder("hi")
CS.Demo.Append(sb, "!") -- shared reference; content mutable

5.2 C# → Lua

ref/out/in default Push OpaqueValue; see 04-OPAQUE.md.

6. string Marshal notes

Declared typeC# → LuaLua → C#
stringLua stringLua string or nil
object (runtime string)Object userdata (façade System.Object)object Pop rules
[LuaMarshalAs(UserData)] on stringByObjUserData (managed System.String object)Force userdata path

[LuaMarshalAs(Bytes)] is for byte[] ↔ Lua string, not System.String; see 07-ARRAY.md.

7. Mono / Il2Cpp consistency

ItemRequirement
Default Push / PopClassUserData; nilnull
Façade (identity, viewType)Same on both platforms
zlua.castSame identity, new view
C#→Lua byrefOpaqueValue (04-OPAQUE.md)
Error messagesSame or equivalent
DocContents
01-OVERVIEW.mdDefault Marshal matrix
03-BYREF.mdref / in / out overview
04-OPAQUE.mdC#→Lua byref default OpaqueValue
07-ARRAY.mdArray ByObjUserData
09-FUNCTION.mdDelegate Marshal
../02-TYPE-SYSTEM.mdType tables, IMT, member access
../05-LIB.mdcast, box