Skip to main content

[LuaMarshalAs] and LuaMarshalType

Normative: Marshal override rules on parameters, return values, fields, properties, and types (class / struct). Default matrix: When not overridden, see 01-OVERVIEW.md. Source: LuaMarshalAsAttribute, LuaMarshalType in ZLua.Common (enum names in this doc, including OpaqueValue). External config: Precompiled assemblies can use equivalent XML rules; see §9.

1. Overview

[LuaMarshalAs] may annotate:

  • Parameters, return values, fields, properties
  • Types (type-level defaults on class / struct)

Must not annotate methods (annotate each parameter/return separately).

Must not apply to undetermined (open / containing generic parameters) CLR type positions (§1.1); may apply to closed generic type positions (e.g. List<int> parameters). XML rules share the same constraints as Attribute (§9.3.1).

Overrides must be in the §3 legal set; otherwise §4.1 falls back to Default and logs an error in the Editor.

public enum LuaMarshalType
{
Default,
UserData,
Bytes,
OpaqueValue,
UnpackedValues, // struct / closed generic struct: multi Lua stack slots ↔ listed members (excl. Nullable)
Table, // struct / closed generic struct / Nullable<struct>: single Lua table ↔ listed members
}

[AttributeUsage(
AttributeTargets.Parameter | AttributeTargets.ReturnValue |
AttributeTargets.Field | AttributeTargets.Property |
AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class LuaMarshalAsAttribute : Attribute
{
public LuaMarshalType LuaMarshalType { get; }

/// <summary>
/// Required for <see cref="LuaMarshalType.Table"/> / <see cref="LuaMarshalType.UnpackedValues"/>.
/// Elements are CLR field or property names (mixed OK); order is UnpackedValues stack order / Table R/W order.
/// Names ending in '?' mean optional keys for Table, Lua→C# (§6).
/// </summary>
public string[] Members { get; set; }

public LuaMarshalAsAttribute(LuaMarshalType luaMarshalType = LuaMarshalType.Default);
}

1.1 Generics: only “determined” type positions

[LuaMarshalAs] (and equivalent XML) must not be used on types that still contain unbound generic parameters:

ForbiddenAllowed
Type-level on open definitions: [LuaMarshalAs] class MyList<T> / struct Foo`1Type-level on non-generic class / struct
Parameter/return/field/property CLR type is a generic parameter or open construction: void Foo([LuaMarshalAs] T a), List<T> itemsClosed construction: void Foo([LuaMarshalAs] List<int> arr), field List<int> Body
Undetermined slots on generic methods (including ordinary methods on generic classes while the slot still contains T)Slots on the same method whose types are already closed

Notes:

  • C# cannot put type-level attributes on closed instances like List<int>; type-level exists only on type definitions. Thus generic types (open definitions) forbid type-level [LuaMarshalAs] / XML type-level MarshalAs; closed instances also do not inherit type-level rules by “normalizing to the open definition”.
  • Member rules may still hang under an open generic declaring type XML Type (MyList`1) as long as the target Field/Property/Param/Return CLR type is determined.

Violations → Il2Cpp Generate / XML fail; Mono Attribute path: §4.1 (log + fall back to Default).

2. LuaMarshalType enum notes

ValueDirectionNotes
DefaultBidirectionalUse 01-OVERVIEW.md defaults; no extra conversion.
UserDataBidirectionalOnly on managed reference types and struct (§3); not for primitives / enum / IntPtr, etc. Semantics: force UserData shape (ByObjUserData / ByValUserData / ClassUserData, etc.).
Substantively useful mainly for string: default C#↔Lua is Lua string; with annotation → ByObjUserData (managed System.String object)
class / interface / arrays / ordinary struct / object: default marshal already UserData; annotation ≡ Default
Delegate: may annotate but no real effect—still marshals as Lua function or existing bridge per 09-FUNCTION.md
BytesBidirectionalForce conversion between C# byte[] and Lua string.
• C# byte[]: default ByObjUserData → Lua string (raw octets, not UTF-8 text semantics)
• On byte[] parameters/returns, Lua must pass string; Pop does not accept ByObjUserData / table
• On string parameters/returns, dual byte[] ↔ string rules apply
OpaqueValueC# → Lua onlyPush OpaqueValue (lightuserdata, no metatable); see 04-OPAQUE.md.
ref / out / in T (any T) default to this shape (no annotation needed)
by-val may explicitly annotate for struct / managed references / enum / pointers, etc. (see §3) to force Opaque Push; primitives and IntPtr / UIntPtr / nint / nuint must not annotate ( Default only, §3)—already integer/number, no real need, and this enables hot-path optimization
• Scripts R/W via zlua.get_opaquevalue / zlua.set_opaquevalue; pass-back to C# follows 04-OPAQUE.md §6
• Must not persist across calls; annotating this on a Lua → C#-only parameter is illegal (§3.1)
UnpackedValuesBidirectionalOrdinary struct / closed generic struct (§3; excludes Nullable / class / interface). Convert members listed in §5 Members with multiple contiguous Lua stack slots:
Lua → C#: Pop N values from the stack in list order, write corresponding fields/properties
C# → Lua: Push N values in list order (multi-return or expanded push)
Requires Members; missing or illegal target → §4. That parameter/return occupies N Lua stack slots (§5.6)
TableBidirectionalOrdinary struct / closed generic struct / Nullable<struct> (§3; excludes class / interface). Convert members listed in §5 Members with a single Lua table:
Lua → C#: Pop one table (or for Nullable allow nil→no value); read by key name and write fields/properties
C# → Lua: Push one table (Nullable no value → nil)
Requires Members (resolved on the underlying struct); missing or illegal → §4. Occupies 1 Lua stack slot

Each CLR parameter/return/field type may only bind a LuaMarshalType from §2 that is compatible with its semantics (Default is legal for all types). Codegen / Mono reflection must consult the table when parsing annotations; annotations outside the legal set are invalid (§4).

The “legal set” column lists values that may be annotated besides Default; unlisted LuaMarshalType values are illegal for that type.

OpaqueValue: Only types listed in the §3 table may legally annotate it for C#→Lua (§3.1); it is not “any by-val type”. Primitives and IntPtr / UIntPtr / nint / nuint allow Default only (no OpaqueValue / UserData, etc.), so hot paths can omit Opaque branches. ref/in/out (including ref int) already default to Opaque—no need to annotate again; see the “byref” row.

C# type (category)Legal LuaMarshalType (excl. Default)Notes
Primitives (bool, char, byteulong, float, double)(none; Default only)UserData / OpaqueValue both illegal. Default is already integer/boolean/number; forbidding OpaqueValue enables optimization. ref/in/out primitives → already Opaque (see “byref” row)—that is not by-val [OpaqueValue]
IntPtr / UIntPtr / nint / nuint(none; Default only)Same as primitives: Default only (integer numeric Marshal); no OpaqueValue / UserData
stringUserData, Bytes, OpaqueValueUserData: force ByObjUserData; Bytes: octet string; OpaqueValue: C#→Lua only
byte[]Bytes, UserData, OpaqueValueBytes: ↔ Lua string; UserData ≡ default; OpaqueValue: C#→Lua only
T[] (szarray)UserData, OpaqueValueUserData ≡ default; OpaqueValue: C#→Lua only
T[,…] (mdarray)UserData, OpaqueValueSame; Lua→C# does not accept table because of annotation
enumOpaqueValueby-val cannot UserData; boxed via zlua.box (08-ENUM.md. OpaqueValue legal but usually unnecessary. ref/in/out enum → see “byref”
struct (ordinary value type / closed generic struct; not ref struct)UserData, OpaqueValue, Table, UnpackedValuesUserData ≡ default; OpaqueValue: C#→Lua only; Table / UnpackedValues need member list
classUserData, OpaqueValueUserData ≡ default; OpaqueValue: C#→Lua only; no Table / UnpackedValues
interfaceUserData, OpaqueValueDefault ByObjUserData; UserData ≡ default; no Table / UnpackedValues
Delegate and subclassesUserData, OpaqueValueUserData has no real effect; OpaqueValue: C#→Lua only
objectUserData, OpaqueValueOpaqueValue: C#→Lua only
ref / in / out T (any T)(usually no annotation)C#→Lua defaults to OpaqueValue (04-OPAQUE.md. Explicit [OpaqueValue] legal
Nullable<T> (T is struct)UserData, OpaqueValue, TableLegal set does not include UnpackedValues (multi-slot cannot distinguish nil=no value). Table Members target underlying T; nil↔no value, table↔has value. When T is primitive/enum, usually no Table beyond Default/OpaqueValue/UserData (if applicable)
Unmanaged pointers (T*, void*, etc.)OpaqueValueDefault may be Default (Pointer pass-through; 10-POINTER.md; may also annotate OpaqueValue (C#→Lua)
Function pointers (delegate*<…>)OpaqueValueSame as unmanaged pointers
TypedReference(usually no annotation)Default is OpaqueValue (bidirectional only this shape; 10-POINTER.md. Explicit [OpaqueValue] legal and equivalent; UserData / Table etc. illegal
decimalOpaqueValuev1 default by-val may still be unsupported; OpaqueValue (C#→Lua) legal
ref struct (Span<T>, etc.)OpaqueValueCannot use ordinary by-val default marshal; OpaqueValue (C#→Lua) legal
params T[] parametersOpaqueValueSame default as szarray (§7); no dedicated LuaMarshalType; OpaqueValue: C#→Lua only

3.1 Direction filter (stacked on the table above)

LuaMarshalTypeAllowed annotation direction
UserData, Bytes, Table, UnpackedValuesBidirectional (Pop / Push may apply; depends on parameter/return direction)
OpaqueValueC# → Lua only (returns, or push args when C# calls Lua); on a pure Lua→C# parameterillegal

4. Illegal annotations and config errors

4.1 Mono Attribute: always log + fall back to Default

When Mono runtime parses Attribute (including type-level), these cases do not abort binding:

BehaviorNotes
MarshalTreat as Default—same as unannotated
LogEditor only emits error-level logs (member signature, CLR type, reason, fall back to Default)
Player (Mono)Silent fall back to Default

Coverage includes but is not limited to:

  • Type / direction outside §3 legal set (e.g. Table on class, OpaqueValue on a Lua→C# parameter)
  • Table / UnpackedValues: missing/empty Members, nonexistent member names, R/W permission mismatch, ? used outside Table, UnpackedValues on Nullable<T>
  • Attribute on undetermined generic positions (§1.1) also falls back on Mono Attribute path (distinct from hard fail on XML/Generate)

Example:

[ZLua] Invalid LuaMarshalAs: ...EchoInt(int value)
parameter 'value' (System.Int32): LuaMarshalType.UserData is not allowed; falling back to Default.

4.2 Il2Cpp Generate / XML load: config errors may hard-fail

Il2Cpp Codegen (Generate) and MarshalAs XML load may fail and abort on §4.1-class errors (visible in CI / Editor); they do not silently write into Player binding tables. Typical text:

[ZLua] LuaMarshalAs configuration error: ...Foo(MyStruct v)
LuaMarshalType.Table requires non-empty Members.

Runtime arity errors (UnpackedValues arg slots ≠ list length) remain luaL_error, unrelated to config fall-back.

5. Table and UnpackedValues (value types)

Scope:

TargetTableUnpackedValues
Ordinary struct / closed generic struct (not ref struct)
Nullable<T> where T is such a struct✓ (Members relative to T)
class / interface / ref struct / primitives / enum

Default behavior: Does not accept Lua tables or multi-stack args to assemble a whole object; must annotate explicitly and provide Members.

5.1 Member list

  • Type string[]; elements are CLR field names or property names, mixed OK.
  • Nullable<T> + Table: List resolves on T, not the Nullable wrapper.
  • Order is semantic order: UnpackedValues stack-slot order; Table R/W traversal order (keys still looked up by member name, independent of order).
  • Validate: name exists; R/W permissions match current Pop/Push direction (Mono §4.1 / Generate §4.2).

5.2 UnpackedValues example

void Foo([LuaMarshalAs(LuaMarshalType.UnpackedValues, Members = new[] { "Y", "X" })] Vector2 v);
Foo(2.0, 1.0) -- first slot → Y, second → X; not a table
[return: LuaMarshalAs(LuaMarshalType.UnpackedValues, Members = new[] { "X", "Y" })]
Vector2 GetPos();
-- Lua: local x, y = CS.Demo.GetPos()

5.3 Table example

void Foo([LuaMarshalAs(LuaMarshalType.Table, Members = new[] { "X", "Y" })] Vector2 v);
Foo({ X = 1, Y = 2 })
void Bar([LuaMarshalAs(LuaMarshalType.Table, Members = new[] { "X", "Y" })] Vector2? v);
Bar(nil) -- null Nullable
Bar({ X = 1, Y = 2 }) -- has value

5.4 (Removed) class assembly

No longer supports Table / UnpackedValues for class / interface (see §3).

5.5 Nesting limits

If a listed member is itself a struct, it is not auto-expanded to table/multi-slot by default; that member type must annotate itself or use the default userdata path (v1 may restrict lists to scalars / enum / string, etc.).

5.6 Stack-slot occupancy and calling convention

LuaMarshalTypeLua stack slots occupied by this parameter/return
Table (and all types except UnpackedValues)1
UnpackedValuesN (N = Members.Length; ? not applicable)
  • Lua → C#: Call bridge advances by stack cursor: after reading a parameter, slot += that parameter's slot count; must not assume “ith CLR parameter ≡ argStart + i”.
  • Overload dispatch: Candidate “Lua argument count” is the sum of each parameter's slot counts (see ../04-METHOD-OVERLOAD.md, not raw parameters_count).
  • Bind time: Members must resolve to accessible public fields/properties; hot path forbids runtime name-based reflection.
  • C# → Lua returns: Table → Push 1 table (Nullable no value → nil); UnpackedValues → Push N values in list order (multi-return).

6. Optional Table members: ? suffix

Only when LuaMarshalType.Table and direction is Lua → C#:

  • A Members element ending in ? (e.g. "OptionalTag?") means an optional key.
  • Strip trailing ? to get the CLR member name; if the Lua table lacks that key, skip assignment (no error).
  • struct / valued Nullable are zero-initialized / default before writes; skipping keeps the default; whole Nullable as nil skips member writes.
  • Members without ?: missing key → luaL_error.
  • UnpackedValues does not support ?; missing slots are arity errors.
[LuaMarshalAs(LuaMarshalType.Table, Members = new[] { "X", "Y", "Tag?" })]
public struct MyDto { public int X; public int Y; public string Tag; }
Foo({ X = 1, Y = 2 }) -- OK; Tag stays null
Foo({ X = 1, Y = 2, Tag = "a" }) -- OK
Foo({ X = 1 }) -- missing Y → error

7. params T[] parameters

Scope: Only one-dimensional array parameters with params on ordinary C# methods / constructors. params on GetFunction-obtained delegate calls / delegate bridges remains unsupported (see 09-FUNCTION.md.

Relation to szarray: params T[] Marshal matches 01-OVERVIEW.md §4 szarray (C#→Lua ByObjUserData; Lua→C# ByObjUserData or array-shaped table). Differences are Lua call shape and empty / null semantics.

No dedicated LuaMarshalType: No “table-only” or “tail multi-slot collect” annotation; when a table is needed, scripts pass an array-shaped table (same as ordinary szarray).

7.1 Behavior (unannotated or Default; OpaqueValue only affects C#→Lua Push)

DirectionRules
C# → LuaSame as szarray: Push T[] as ByObjUserData (do not unpack to multi slots; do not default Push table)
Lua → C#params occupies a single stack slot; Pop ByObjUserData or array-shaped table, construct / pass T[]

No C#-style implicit expand: Lua does not support auto-collecting multiple contiguous args after the params parameter into T[]. Must pass exactly one argument in the params position:

PassedC# receives
ByObjUserData (T[] instance)That array reference
table {} (empty array shape)T[0] (zero-length)
table { … } (contiguous keys 1…n)T[n] built from elements
nilnull (not empty array)

Examples:

static void Sum(params int[] values) { /* … */ }
static void Prefix(int head, params int[] tail) { /* … */ }
-- ✅ Legal: explicit array userdata
CS.Demo.Sum(arr) -- arr is int[] ByObjUserData

-- ✅ Legal: explicit table
CS.Demo.Sum({ 1, 2, 3 })
CS.Demo.Sum({}) -- zero elements → T[0]
CS.Demo.Prefix(0, { 1, 2 }) -- tail = {1,2}

-- ✅ nil → null (not empty array)
CS.Demo.Sum(nil)

-- ❌ Illegal: multi-slot implicit collect (unsupported)
-- CS.Demo.Sum(1, 2, 3)
-- CS.Demo.Prefix(0, 1, 2)

With overload dispatch: params still counts as one parameter slot (stack slots = 1); does not fold later slots into the params segment (see ../04-METHOD-OVERLOAD.md.

Vs struct Table: Table / UnpackedValues assemble members of a single value-type parameter (§5); not for params T[]. The params table shape is the szarray array segment (integer keys 1…n); see 07-ARRAY.md.

8. Resolution priority

Codegen / Mono reflection resolve Pop / Push fine → coarse; when Attribute and XML both target the same site, Attribute wins:

  1. Parameter / return
    1. [LuaMarshalAs] on that slot (if ≠ Default)
    2. Else corresponding XML Method / Param or Return rule (§9)
  2. Field / property
    1. [LuaMarshalAs] on the member (if ≠ Default)
    2. Else corresponding XML Field / Property rule (§9)
  3. Declared type (class / struct type-level; non-generic only, §1.1)
    1. Type-level [LuaMarshalAs] (if ≠ Default)
    2. Else direct child MarshalAs under that XML Type (§9)
  4. Built-in defaults in 01-OVERVIEW.md

No method-level [LuaMarshalAs] / method-level XML MarshalAs; configure each parameter/return. If the attribute appears on a method: Mono ignores and warns (§4.1); Il2Cpp Generate may hard-fail (§4.2).

When any parameter / return (or legal type-level) override is ≠ Default, Il2Cpp Codegen emits dedicated push/pcall/pop code for that method (or a path with non-Default writers). Table / UnpackedValues expand Members / XML members at bind time; no runtime reflection.

If annotation is illegal or config is wrong during resolve: Mono Attribute always treats as unset and falls back (§4.1); Il2Cpp Generate / XML may fail (§4.2).

ScenarioDefault[LuaMarshalAs] / XML override
C# calls Lua, struct parameterOpaqueValue or userdata depending on contextOpaqueValue → force OpaqueValue
struct parameteruserdataTable / UnpackedValues (Nullable only Table)
C# calls Lua, ref intOpaqueValue (default)No annotation needed; explicit [OpaqueValue] legal (byref row)
C# calls Lua, by-val int + OpaqueValueintegerIllegal (primitives are Default only); Mono fallback / Generate fails (§4)
Lua calls C#, string parameterLua stringUserDataByObjUserData
Lua calls C#, byte[] parameterByObjUserData or tableBytes → Lua string
params T[] parameterSame as szarray (single slot)No dedicated override; may only annotate OpaqueValue (C#→Lua)

9. XML external config (precompiled assemblies)

Motive: Many assemblies are precompiled DLLs and cannot add [LuaMarshalAs]. XML provides equivalent external Marshal rules. Separate from [LuaAlias]: Method aliases use an independent Settings path and root element (see ../04-METHOD-OVERLOAD.md §5.4); do not mix with this file. Platform: Mono (Editor) loads and parses XML at runtime; Il2Cpp (Player) forbids runtime XML parse—build time (ZLua/Generate/All, etc.) generates C++ tables/data from XML; runtime only looks up tables. Semantics: Mono and Il2Cpp Lua-visible results must match.

9.1 Config entry (Settings)

Path lists live in Editor ZLua.Settings (ProjectSettings/ZLua.asset), e.g.:

  • marshalAsXmlPaths: relative-to-project-root or absolute paths; files or directories. Only carries ZLuaMarshalAs rules
  • luaAliasXmlPaths: method-alias path list (configured separately); see 04-METHOD-OVERLOAD.md §5.4
  • Directories: recursively include *.xml under that directory (implementations may use one level only; docs treat “the path set listed in Settings” as authoritative)

No paths configured → no XML Marshal rules (Attribute + defaults only). Missing / unreadable paths → fail (clear Editor error; Generate aborts).

9.2 File format

Root element:

<?xml version="1.0" encoding="utf-8"?>
<ZLuaMarshalAs version="1">
<!-- Assembly / Type / … -->
</ZLuaMarshalAs>
ConstraintNotes
versionRequired; currently only "1". Unknown version → fail
EncodingUTF-8
Multiple filesAll files listed in Settings merge into one rule set; see §9.5 duplicate detection

9.3 Schema (elements)

<ZLuaMarshalAs version="1">
<Assembly name="UnityEngine.CoreModule">
<!-- Type-level: equivalent to [LuaMarshalAs] on the type -->
<Type fullName="UnityEngine.Vector3">
<MarshalAs type="Table" members="x,y,z" />
</Type>

<Type fullName="UnityEngine.Transform">
<Method name="LookAt" signature="(UnityEngine.Vector3)">
<!-- index: 0-based, excludes this; do not use name -->
<Param index="0">
<MarshalAs type="UnpackedValues" members="x,y,z" />
</Param>
</Method>

<Method name="get_position" signature="()">
<Return>
<MarshalAs type="Table" members="x,y,z" />
</Return>
</Method>
</Type>

<Type fullName="MyGame.Net.Packet">
<Field name="Body">
<MarshalAs type="Bytes" />
</Field>
<Property name="Title">
<MarshalAs type="UserData" />
</Property>
</Type>
</Assembly>
</ZLuaMarshalAs>
Element / attributeMeaning
Assembly/@nameAssembly.GetName().Name (not path, not .dll filename)
Type/@fullNameCLR full name: Namespace.Type; nested Outer+Inner; generic declaring types write open definition Foo`1 (member-mount container only; §9.3.1). Forbid closed instance names as Type containers
Method/@nameMethod name (CLR Name, no signature)
Method/@signatureParameter type list in parentheses, same style as 04-METHOD-OVERLOAD.md §5.4: no-arg (); with args (T1,T2); byref types append &; arrays T[]. Excludes return type. Types in the signature must match metadata (closed writes closed full names; parameter T only when the method signature itself is that way—but slots with undetermined types must not have MarshalAs)
Param/@indexRequired; 0-based; excludes instance this. Must not locate by parameter name
ReturnEquivalent to [return: LuaMarshalAs(...)]
Field/@name / Property/@nameCLR member names
MarshalAs/@typeLuaMarshalType name: Default / UserData / Bytes / OpaqueValue / UnpackedValues / Table (same as §1 enum; use OpaqueValue, not historical OpaqueLightUserData). Unknown or removed type names (e.g. historical ParamsTable) → fail
MarshalAs/@membersCorresponds to Members: comma-separated; ? suffix for optional Table keys (§5/§6). Required for Table / UnpackedValues; other types omit or ignore with a warning

Separate from aliases: Method aliases live in ZLuaAlias / luaAliasXmlPaths (see 04-METHOD-OVERLOAD.md §5.4), not this file. Root must be ZLuaMarshalAs.

9.3.1 Generics (same as §1.1)

CorrectIncorrect
void Foo([LuaMarshalAs(Bytes)] List<int> arr) / XML Param pointing at that closed parameter[LuaMarshalAs] class MyList<T>; type-level XML MarshalAs on MyList`1
XML Type fullName="MyList1"` configuring closed Field/Param under itvoid Foo([LuaMarshalAs] T a); Field type T / List<T> still with MarshalAs
Type-level Attribute / XML on non-generic typesExpecting List<int> to inherit type-level rules from List`1

Forbidden:

  • Hanging MarshalAs directly on Method (no “method-level” override)
  • Locating Param with a name attribute
  • XML type using deprecated OpaqueLightUserData string → fail (must be OpaqueValue)
  • XML Type/@fullName as a closed generic instance name → fail (members hang on open definition / non-generic declaring type)
  • Type-level or member-level rules on undetermined types → fail (§1.1)

9.4 Parse and validation

On load (Mono XML parse / Il2Cpp Generate):

  1. XML syntax errors, unknown version, unknown elements / missing required → fail
  2. (Metadata validation timing) Mono: when that assembly is first queried for lazy bind; Il2Cpp: at Generate (runtime no longer reports config errors)—Assembly / Type / Method / Field / Property unresolvablefail
  3. Type resolves to a closed generic instancefail
  4. Type-level MarshalAs on a generic typefail (§1.1)
  5. Field / Property / Param / Return target CLR type undeterminedfail (§1.1)
  6. MarshalAs not in §3 legal set for target CLR type → Generate/XML: fail; Mono runtime Attribute path uses §4.1 instead
  7. Table / UnpackedValues missing members, or UnpackedValues+Nullable → Generate/XML: fail (§4.2)

Il2Cpp generated tables contain names only (assembly / type / method+signature / param index); do not embed metadata tokens (tokens change across stripping).

9.5 Duplicate rules (severe error)

After merging all Settings XML, if any target key has two or more effective MarshalAs rules → entire load / Generate fails (later files must not override):

Target keyComposition
Type-level(assemblyName, typeFullName, kind=Type)
Field(assemblyName, typeFullName, kind=Field, memberName)
Property(assemblyName, typeFullName, kind=Property, memberName)
Parameter(assemblyName, typeFullName, methodName, signature, kind=Param, index)
Return(assemblyName, typeFullName, methodName, signature, kind=Return)

Duplicates within one file also fail. Error messages must include file path and conflicting key for diagnosis.

Note: Attribute and XML targeting the same site is not a “duplicate fail”—§8 Attribute wins; that XML entry may be recorded as unused (optional diagnostics) without aborting load.

9.6 Mono (Editor) runtime

  • Only load / parse XML from Settings paths (including §9.5 duplicate detection), group by Assembly/@name; do not resolve metadata tokens at startup.
  • Lookup tables (lazily created and validated on that assembly's first query):
    • Type / Field / Property: AssemblymemberTokenRule
    • Param / Return: Assembly(methodDefToken, index)Rule
      • Param: index is XML @index (0-based)
      • Return: index = -1
      • Do not depend on ParamDef / ReturnParameter metadata tokens (often absent on Mono)
  • Hot path O(1) table lookup; do not string-match.
  • Lookup API shares resolution layer with Attribute: TryAttribute (may still use param token if present) then TryXml.
  • May provide Editor menu “Reload MarshalAs XML”; changing files does not auto-guarantee hot reload (docs do not require it).

9.7 Il2Cpp: XML → C++ (build time)

  • Generate must not write metadata tokens: DLLs are not stripped yet; tokens would disagree with final AOT metadata; and there is no “post-strip AOT DLL” callback. Artifacts remain name-keyed LuaMarshalAsXmlEntry (assembly / type / method+signature / param index, etc.).
  • Build-time validation: MarshalAsCodegen (or equivalent) runs the same resolution checks as Mono bind at Generate (type exists, §1.1 determined, type-level non-generic, etc.) → fail aborts Generate. Player runtime no longer fails on XML config.
  • Runtime: RegisterMarshalBindingTables() only registers entries; resolve names lazily per assembly into tables isomorphic to Mono (member tokens; Param/Return as methodDef.token + index, Return index=-1).
  • Player must not open or parse XML files.
  • Lookup: after Attribute miss, query XML; type-level Attribute/XML skips generic types (§1.1).

9.8 Sync with source enum names

Docs and XML use LuaMarshalType.OpaqueValue. If implementation C# enums still use historical OpaqueLightUserData, rename to OpaqueValue and replace references globally to match §1.

TopicDoc
Default matrix01-OVERVIEW.md
OpaqueValue04-OPAQUE.md
struct Marshal05-STRUCT.md
Arrays / Bytes07-ARRAY.md
Enum boxed08-ENUM.md
Method-alias XML (style reference)../04-METHOD-OVERLOAD.md §5.4