Skip to main content

Return view type

By default the return view is the declared type. With LuaMarshalType.ActualTypeUserData, the value is still ByObj userdata, but the view = runtime actual type, so Lua can see derived members. Authoritative details: 02-MARSHAL-AS, 06-CLASS.

Why ActualTypeUserData​

Lua member access follows the userdata view, not a guess of the runtime type. On default Push the view is the declared return type of the method/property — even if the heap instance is more specific, ZLua does not switch to the derived type automatically:

Base Create() => new Child(); // Actual Child; Lua view stays Base
local o = factory:Create()
o:BaseOnly() -- OK: on Base view
-- o.childField -- fail: Child-only members are not on the view

Defaulting to the declared view is intentional, to avoid:

RiskNotes
Unstable “type” across callsScripts cannot assume a fixed API surface for the return
Entangling special Marshale.g. declared object must not silently become a Lua string when the instance is string
Member-table bloat / instabilityIMT bound to the declared type stays predictable and cacheable

Therefore mark return-side ActualTypeUserData only when the API is intentionally wide (base / interface / Component / object) but scripts almost always need derived members — so the view becomes obj.GetType() and you skip a routine zlua.cast.

Typical cases:

ScenarioDeclared returnOften actualPain without the attribute
Factory / polymorphic createBase / interfaceConcrete ChildMust cast every time for derived members
Unity GetComponentComponent / interfaceButton, etc.Stuck on the declared type, or script-side zlua.cast. Or use the master switch Unity optimizations
Wide object / boxed returnobjectConcrete classView is always System.Object

Not needed when the declared type already exposes the members you call, or downcast is rare (zlua.cast is enough).

Default vs ActualType​

Default PushActualTypeUserData
ShapeByObjStill ByObj
ViewDeclared typeActual type
TypicalCreateChild() → BaseCreateChildActual() → Child
Base CreateChild() => new Child();

[return: LuaMarshalAs(LuaMarshalType.ActualTypeUserData)]
Base CreateChildActual() => new Child();
local o = ObjectFactory.CreateChild() -- view Base
local c = zlua.cast(o, Child) -- script changes view
local a = ObjectFactory.CreateChildActual() -- view already Child on return

How to annotate​

ApproachNotes
Attribute[return: LuaMarshalAs(LuaMarshalType.ActualTypeUserData)]
XMLReturn=ActualTypeUserData (on the declaring type; inheritance works)
Hot-update assembliesAttribute only; XML is not supported

Return values only. On parameters / fields / properties / type-level → illegal (Mono Attribute: fall back to Default + log; Il2Cpp Generate / XML: may fail).

Allowed types (summary)​

Managed reference types that default to ByObj: class / interface / object / arrays, etc.

Not for ordinary struct, or string (default is not ByObj), etc. Full allow-list: 02-MARSHAL-AS §3.

Complementary to zlua.cast​

ActualTypeUserDatazlua.cast
Who changes the viewAutomatically on C# returnScript does it
WhenFixed APIs that should “exit as the real type”Occasional downcast

With generic methods​

For GetComponent, after close the declared return may be Component; annotate ActualType on the open method’s return slot so the view is the actual component type. See Generics.

Common misconceptions​

MythReality
Turns string into a Lua stringNo; actual string stays ByObj (view System.String)
Changes virtual dispatch rulesNo; look up on view, then virtual-call on real this
Same as Opaque / TableNo; shape remains ByObj UserData

Learning path​

PreviousLuaMarshalAs
Next0GC Marshal