Skip to main content

04 — Special-type metatable behavior

This document summarizes metatable-layout and member-index special cases for enum, Nullable<T>, struct, arrays, and delegates. Push/Pop of values, ByVal/ByObj payload layout, and other Marshal details are in the ../marshal/ volumes; this document only describes Lua-script-visible table structure, metamethods, and index entry points.

Related docs: common layout → 01-LAYOUT.md; index algorithm → 02-INDEX.md; binding rules → 03-BINDING.md.


1. Enums

1.1 Type table and static access

An enum resolves via CSharp[assembly][typeFullName] to type table E, with __enum : true, SMT, and ByObj IMT. There is no __byval_instance_mt.

At Bind time, all public static literal fields of the enum are written onto E itself as integer (preferred on Lua 5.4+) or integral number, using the C# underlying integer value. They are not userdata. Reading E.Red does not trigger __index when the key is already on E.

SMT provides __index / __newindex (static three tables) but no __call and no _default. EnumType(...) construction is forbidden; hanging _ctor on E or SMT is also forbidden.

Assigning to enum constants: __newindex strict error, same as static readonly.

1.2 Boxed instances

When a boxed enum (ByObj userdata) is needed, use zlua.box (../05-LIB.md); there is no type-table construction entry:

local Color = CSharp.AC['MyGame.Color']
local redBox = zlua.box(Color, Color.Red)

The product attaches E.__byobj_instance_mt, with __zlua_ud_kind "byobj". Instance three tables are usually empty or nearly empty (enums have no public instance fields/methods); __tostring is recommended as something like EnumFullName(value).

Default cross-boundary argument passing still uses integer/number (../marshal/08-ENUM.md); zlua.box is only for scenarios that need object parameters / boxing semantics.

1.3 Comparison with class / struct (metatables)

Itemenum
Type-table constantsinteger/number keys
SMT.__callnone
_defaultnone
Instance userdataonly zlua.box → ByObj
Inheritance flatteningnone (enums do not merge an inheritance chain)

2. Nullable<T> (closed value type)

System.Nullable\1closed viazlua.make_generic_typebecomes type tableN with **__nullable : true**, mutually exclusive with __struct/__enum`.

2.1 Layout special cases

  • No __byval_instance_mt, no __byobj_instance_mt, no IMT.
  • SMT contains only __call and optional __tostring; no __index / __newindex (Nullable's own static members are not exposed as bindings).

2.2 SMT.__call semantics

N(...) constructs a valued representation of element type T, not a Nullable wrapper instance. Native binds __call to the element type's construction logic (consistent with T(...) / primitive conversion of T):

local NullableInt = zlua.make_generic_type(
CSharp.mscorlib['System.Nullable`1'],
zlua.types.int32
)
local n = NullableInt(42) -- Lua integer, not userdata

local NullablePoint = zlua.make_generic_type(
CSharp.mscorlib['System.Nullable`1'],
Point2D
)
local p = NullablePoint(1, 2) -- Point2D ByVal userdata
Kind of TReturn value of N(...)
Primitivecorresponding Lua primitive (boolean / integer / number)
structT's ByVal userdata
enumnot supported via this entry (enums have no __call)

null / no value is not expressed via N(...); when passing null Nullable<T> to C#, pass Lua nil directly (../marshal/01-OVERVIEW.md).


3. Value-type structs

A struct type table has __struct : true and attaches both __byval_instance_mt and __byobj_instance_mt (see 01-LAYOUT.md §4).

3.1 Static entry points

EntryLocationSemantics
Type(...)SMT.__callParameterized public constructor → ByVal userdata (matches the spec default construction product)
Type._default()_default on SMT, via static __index → SMT fallbackParameterless zero-initialized instance, equivalent to default(T); does not call user parameterized constructors

_ctor fields are forbidden; _default for enum/Nullable is forbidden.

3.2 Instance members and dual MTs

  • ByVal userdata: metatable = T.__byval_instance_mt; fields/methods indexed via ByVal three tables; this points at the payload (../marshal/05-STRUCT.md).
  • ByObj userdata (boxed struct): metatable = T.__byobj_instance_mt; same member names, ByObj three-table closures.
  • Static members are accessed via T / SMT, same path as classes.

Structs have no C# instance inheritance; Bind time does not merge base instance members (value types have no derived-instance inheritance scenario). Optional zlua.box converts between ByVal and ByObj (marshal volumes).


4. Arrays (szarray / mdarray)

Array type tables resemble ordinary reference types: ByObj IMT only (array objects are Il2CppArray* / equivalent references). SMT.__call is absent (array instances are created via zlua.new_szarray_* / zlua.new_mdarray_*; see ../02-TYPE-SYSTEM.md).

4.1 Instance metamethods

MetamethodBehavior
__lenszarray: #arr = Length; mdarray: #arr = product of GetLength(d) over all dimensions (total addressable element count), not a single-dimension length
__index / __newindexgo through instance three tables; do not implement arr[i] metamethod subscripting

4.2 Element access: get / set

At Bind time, register native methods get / set on the instance methodTable (not get_Item naming):

arr:set(0, 10) -- szarray: 1 index + value
assert(arr:get(0) == 10)

matrix:set(0, 1, 7) -- mdarray: rank indices + value

Argument counts: get must equal rank; set must equal rank + 1 (last arg is value). Indices are C# per-dimension indices (including lowerBound) and must be integers. Out of range → luaL_error.

GetValue / SetValue and other methods bound via the three tables remain available; for primitive assertions prefer get (unboxed). Semantics differ from 1-based Lua tables via zlua.to_table; see ../marshal/07-ARRAY.md.


5. Delegates

Delegate type table + ByObj IMT. Delegate instance userdata additionally registers __call on the ByObj IMT:

local cb = SomeDelegate(function(x) return x * 2 end)
local result = cb(21) -- equivalent to invoke; obj:Invoke(21) is not required

__call argument count must match the Invoke signature; Lua function → delegate Marshal is in ../marshal/09-FUNCTION.md. Static members (if any) still go through SMT three tables; there is no event subtable.


6. Other types (summary)

TypeMetatable notes
classonly __byobj_instance_mt; SMT.__call → instance construction; inherited members Bind-time flattened
interfaceresolvable type table; usually no public constructor, so SMT.__call is unavailable
abstract classonly public constructors may __call; protected constructors are invisible to Lua
static classstatic three tables only; no __call, no IMT

7. Marshal cross-references

TopicDocument
Enum default integer and box../marshal/08-ENUM.md
Struct ByVal / ByObj../marshal/05-STRUCT.md
Class / reference facade../marshal/06-CLASS.md
Array creation and get/set../marshal/07-ARRAY.md
Delegate ↔ Lua function../marshal/09-FUNCTION.md
Nullable null / valued../marshal/01-OVERVIEW.md

The metatable layer only guarantees that entry points and index semantics match the table above; concrete stack-type checks and GC behavior follow the marshal volumes.