Skip to main content

04 — Method overloads

How C# method overloads are resolved and invoked from Lua. Applies to Il2Cpp (Player) and Mono (Editor). Inheritance and Bind rules: 02-TYPE-SYSTEM.md §5; zlua API: 05-LIB.md.


1. Problem and goals

C# allows same-named methods overloaded by parameter types/arity; Lua has no static types, so obj:Run(x) cannot pick an overload at compile time.

GoalNotes
Usabilityobj:Run(10) should work in common cases
PrecisionOn name collisions, Bind auto-hangs full-signature keys; scripts may also use [LuaAlias] / register_method for short names
PerformanceHot paths prefer single-candidate direct (full-signature key, alias, or locally cached closure); avoid repeated dispatch
ConsistencyMono and Il2Cpp select the same overload; error text is consistent

2. Three-layer mechanism (priority)

  1. Group by final name (§3, §5): at bind time each method enters a group under its final Lua name (C# default name, [LuaAlias] / XML alias); multiple candidates under one name are allowed (bind-time alias mechanism only).
  2. Single candidate → direct; multiple → dispatch (§3.6).
  3. On multi-candidate same name, also hang full-signature keys (§3.7): each colliding overload also registers a direct key MethodName(ParamTypeFullNames…) (excluding return type) so scripts can name an overload precisely without register_method first.
  4. Runtime (§6): register_method may only hang onto a final name that does not yet exist (§6.1), attaching an existing direct closure (often from a full-signature key or [LuaAlias]) as a short name for obj:alias(...) colon calls.

3. Default names and dispatch

3.1 Registration rules (group by final name)

Within the same type, same is_static domain, and same instance shape (ByVal / ByObj), collect each method’s final Lua name set (§5), then aggregate by name:

Candidates under that final nameMetatable key binding
1That candidate’s direct method closure
≥ 2dispatch closure (call-time pick per §3.6)

Sources may be:

  • Multiple C# same-name overloads (same default name); or
  • [LuaAlias] / XML hanging different methods on the same final name (colliding with default names or other aliases is allowed); or
  • C# extensions (injected into IMT per 13-EXTENSION-METHODS) and real instance methods with the same final namemerged competition (no “instance beats extension”); full-signature keys and matching both use formals after dropping this.

Except zlua.register_method: runtime registration forbids an already-existing final name (see §6.1), so that API cannot enlarge an existing overload group.

Static and instance live in separate tables (staticMap vs byvalInstanceMap / byobjInstanceMap). C# may have both static void Foo() and void Foo(); they do not interfere.

3.2 Candidate order

Dispatch walks candidates and ranks applicable overloads by §3.6 better function member; it must not pick ImplicitBoxing merely because metadata declaration order is earlier and skip a better Identity overload.

Candidate walk order (used only for tie-break):

  1. Codegen declaration order (order in Il2Cpp / Mono generated metadata)
  2. Reflection fallback: deterministic sort (e.g. full-signature lexicographic)

3.3 Parameter matching

When the Lua stack slot count is acceptable, walk CLR formals from the stack cursor and decide bindability (UnpackedValues may occupy multiple slots; see table and marshal/02-MARSHAL-AS.md §5.6). Rules align with marshal/ ReadValue / TryPop, including but not limited to:

Lua argC# formalRule
integerint / long, etc.Within target type range
number (non-integer)intNo match
numberfloat / doubleAllowed
stringstringAllowed
nilreference type / Nullable<T>Allowed
nilvalue type (non-Nullable)No match
userdatareference typeRuntime type assignable
primitive / stringobjectAllowed; ImplicitBoxing or ImplicitReference
ByVal value typeobject / implemented interfaceImplicitBoxing when implicit box is possible
primitiveclass / interface (not object)No match
multi-arg + params T[]paramsSingle stack slot; same as szarray (table / userdata / nil); no multi-slot implicit collect
UnpackedValues formalconsecutive N slotsThat CLR formal occupies N Lua arg slots (N = Members.Length); matching accumulates stack slots; see marshal/02-MARSHAL-AS.md §5.6

Optional / default parameters: if Lua supplies fewer args than formals, remaining formals with C# defaults may still match.

Constructors: Type(...) / SMT.__call use the same dispatch logic as instance methods.

3.4 Performance notes

Each dispatch call walks candidates and recomputes matches — a slow path. If a hot path needs a fixed overload, use:

  • That overload’s full-signature key (§3.7, already direct); or
  • A final name with only one candidate (e.g. a unique [LuaAlias("run_i32")]); or
  • register_method short name then colon call; or
  • Locally cache a direct closure in script (e.g. local run = demo['Run(System.Int32)']).

3.5 Failure errors

On no matching overload, luaL_error listing candidate signatures, e.g.:

no overload for Demo.Run matching (number); candidates: Run(System.Int32), Run(System.String)

Candidate names in the message match §3.7 full-signature keys so scripts can rewrite accordingly.

3.6 Implicit conversion kinds and best overload

Overload dispatch must match C# better function member.

3.6.1 Design principles

  1. ConversionKind describes only C# implicit conversion categories, not Lua userdata payload shape.
  2. Ranking rules match C#.
  3. Mono / Il2Cpp must select the same C# overload for the same Lua args.

3.6.2 ConversionKind

KindC# counterpartMeaning
Identityidentity conversionSame type
ImplicitNumericimplicit numericWidening only
ImplicitEnumimplicit enuminteger → enum
NullLiteralnull literalnil → reference / Nullable
ImplicitReferenceimplicit referencesubclass→base; stringobject
ImplicitBoxingimplicit boxingvalue type→object / interface
NoneNo match

Kind preference chain:

IdentityImplicitNumericImplicitEnumNullLiteralImplicitReferenceImplicitBoxing

3.6.3 Better function member

  1. Per formal compute GetConversionKind; any None → not applicable.
  2. M beats N: some parameter i where M is better, and no j where N is better.
  3. No strictly better → §3.2 declaration-order tie-break.

Examples:

Lua callResult
Run(10), Run(int) vs Run(object)Pick Run(int) (Identity ≻ Boxing)
SetValue(10, 0), SetValue(object,int) vs SetValue(object,long)Pick (object,int) (p1 Identity ≻ Numeric)

3.6.4 Implicit Box at invoke

Only when Kind is ImplicitBoxing, Object::Box inside TryPop of the already selected overload. Forbidden to Box inside the GetConversionKind loop.

3.7 Same-name collisions: full-signature keys (automatic at Bind)

When a final name (usually the C# default method name) has ≥ 2 candidates, besides binding that name to a dispatch closure, also register one direct metatable key per candidate:

<MethodName>(<Type0.FullName>,<Type1.FullName>,…)
RuleNotes
WhenOnly when that final name has a name collision (≥ 2 candidates); single-candidate methods are not required to hang a full-signature key
Method nameC# MethodInfo.Name (same as default final name; not a [LuaAlias] short name)
Parameter listSame signature string as §4.1: parentheses + comma-separated Type.FullName; no return type; byref / array / generic spelling matches metadata
Bound valueThat candidate’s direct method closure (same as single-overload direct)
Static / instanceSame domain as the method; written into the corresponding staticMap or instance map

Example: Foo has int Run(int) and int Run(string):

Metatable keyBinding
Rundispatch (runtime pick per §3.6)
Run(System.Int32)direct → Run(int)
Run(System.String)direct → Run(string)
local demo = CSharp.AC.Foo()

demo:Run(5) -- dispatch → Run(int)
demo:Run("hi") -- dispatch → Run(string)

-- No register_method needed; precise name (dot + explicit self)
demo['Run(System.Int32)'](demo, 5)
demo['Run(System.String)'](demo, "hi")

Colon syntax: keys containing ( / ) cannot be written as demo:Run(System.Int32)(...); use bracket keys + dot call. For short-name colon calls like demo:run_i32(5), use [LuaAlias] or register_method (§5, §6).

For multi-overload constructors, full-signature keys live on the type-table side (implementation conventions paired with _ctor / __call dispatch); parameter-list format is the same as this section.


4. Signature string rules

4.1 zlua.signature

local sig = __zlua_create_signature(zlua.types.int32)
-- sig == "(System.Int32)"

local sig0 = __zlua_create_signature()
-- sig0 == "()"

Conventions:

  • Arguments are C# types: type tables, zlua.types.*, or mscorlib strings (same typeArg rules as 05-LIB.md)
  • Does not include the method name (caller concatenates; see §3.7)
  • Format: parenthesized, comma-separated Type.FullName list
  • Generic / array formats match codegen metadata

Native callback: __zlua_create_signature (ZLuaLib.cpp). Projects should wrap it as zlua.signature(...) in a local zlualib extension.

4.2 Full-signature key = method name + §4.1

"Run" + "(System.Int32)" → Lua metatable key "Run(System.Int32)"

That key is exposed as a methodTable __index string key when there are multiple same-name candidates (§3.7). Forbidden to treat only the parameter parentheses (e.g. "(System.Int32)") as a method key; the method name is required.


5. Alias mechanism ([LuaAlias])

5.1 Model: rename registration + group by final name

[LuaAlias] / XML assigns the method a sole final Lua name, replacing (not appending) the C# default MethodInfo.Name. With an alias, the method is no longer registered under the default name.

For each public method, the final Lua name is (highest priority first):

SourceCondition
[LuaAlias("…")]Attribute present and non-empty → that string
XML Method/@aliasNo Attribute; XML has a rule → that string
C# default MethodInfo.NameNeither of the above

Then within the same binding domain:

Aggregate candidates by finalName → count 1 → direct; ≥ 2 → dispatch (§3.6)

Therefore:

  • Aliases may collide with other methods’ default names or aliases (collision merges into one overload group);
  • Not allowed: “hang both alias and keep this method’s default name” — alias means rename;
  • Calling that colliding key uses the same function overload rules as ordinary C# overloads.

5.2 Collisions allowed (examples)

public class Demo
{
public void Run(int value) { }
public void Run(string value) { } // same default name → "Run" naturally groups

public void Foo(int x) { }

[LuaAlias("Foo")] // Allowed: collides with existing Foo → merges into "Foo"; this method no longer hangs "Bar"
public void Bar(string s) { }

[LuaAlias("print")]
public void LogA(int x) { }

[LuaAlias("print")] // Allowed: aliases collide → "print" groups
public void LogB(string s) { }

[LuaAlias("run_i32")] // Only candidate under this final name → usually direct; no longer hangs default "Run"
public void Run(long value) { }
}
local d = CSharp.AC.Demo()

d:Run(10) -- "Run" group (int/string; excludes long) → dispatch
d:Foo("hi") -- "Foo" group has Foo(int) and Bar(string) → dispatch → Bar(string)
d:print(1) -- "print" group → dispatch
d:run_i32(10) -- "run_i32" single candidate → direct → Run(long)
-- d:Bar("x") -- unavailable: Bar renamed to Foo

5.3 C# Attribute

[LuaAlias("run_i32")]
public void Run(int value) { ... }
  • Defined in ZLua.Common.
  • vs XML: on the same method Attribute wins over XML (Attribute present → Attribute is the final name, ignore that XML slot; XML only if no Attribute). Both are rename, not “default name + alias” dual hang.

5.4 XML configuration (independent of MarshalAs)

[LuaAlias] and [LuaMarshalAs] have different goals (final Lua name vs parameter/return/member Marshal); they rarely intersect. XML may share similar Assembly / Type / Method locating style, but must:

ConstraintNotes
Separate path listEditor Settings field luaAliasXmlPaths (alongside marshalAsXmlPaths, configured separately)
Separate root elementZLuaAlias; must not use ZLuaMarshalAs
Separate filesAlias and MarshalAs are different files (may share Assembly/Type/Method locating style)
<?xml version="1.0" encoding="utf-8"?>
<ZLuaAlias version="1">
<Assembly name="Assembly-CSharp">
<Type fullName="Demo">
<Method name="Run" signature="(System.Int32)" alias="run_i32"/>
</Type>
</Assembly>
</ZLuaAlias>
Element / attributeMeaning
versionRequired; currently only "1". Unknown version → fail
Assembly/@nameAssembly.GetName().Name (same as MarshalAs XML)
Type/@fullNameCLR full name (nested Outer+Inner; open generic as Foo`1 mount container)
Method/@nameCLR MethodInfo.Name
Method/@signatureParameter type list in parentheses: () / (T1,T2); byref suffix &; array T[]; no return type (same Method locate convention as MarshalAs)
Method/@aliasRequired; non-empty; the method’s sole final Lua name (§5.1, replaces default)

Allowed content: only AssemblyTypeMethod with @alias. Forbidden: MarshalAs / Param / Return / Field / Property children, or Method missing @aliasfail.

Other constraintsNotes
RenameWith @alias, do not also register under MethodInfo.Name
vs AttributeIf the same method has Attribute, Attribute wins (§5.3); that XML entry may be recorded unused (optional diagnostics)
PlatformsMono parses luaAliasXmlPaths at runtime; Il2Cpp Generates a static alias table; Player does not read XML (load/Generate modes align with marshal/02-MARSHAL-AS.md §9.6–§9.7, but separate registry / artifacts)
DuplicatesAfter merging all alias XML, multiple @alias for the same (assembly, type, methodName, signature)fail (later files must not override). Different methods colliding on the same final name are allowed (merge into overload group, §5.1)

5.5 Static / instance

  • Instance final names → byvalInstanceMap / byobjInstanceMap (same domain as the closure)
  • Static final names → staticMap

Grouping must not cross static/instance or ByVal/ByObj.

5.6 Same name as field / property

If a final method name collides with a field / parameterless property, __index still prefers methodTable (see metatable/02-INDEX.md). This is a different layer from method–method overload grouping.


6. Runtime API

When naming a specific overload explicitly, preference order:

  1. Full-signature key (§3.7, automatic at Bind; no API needed)
  2. Bind-time [LuaAlias] short name
  3. Take a direct closure from a full-signature key / alias, then register_method a custom short name (for colon calls)

zlua.signature(...) (§4.1) builds / checks the parameter-parentheses part; it is not a standalone metatable key.

6.1 zlua.register_method

Must match ZLuaLib.cpp / zlualib.lua: two-parameter form.

zlua.register_method(aliasName, methodOrClosure) → void

Purpose: hang an existing direct closure onto a short name that does not yet exist. After success, instance methods can use colon calls (no bracket key + explicit self).

local demo = CSharp.AC.Demo()

-- 1) Full-signature key: no register needed, but requires dot + self
demo['Run(System.Int32)'](demo, 5)

-- 2) Take direct closure, hang short name
local run_i32 = demo['Run(System.Int32)']
zlua.register_method("run_i32", run_i32)

-- 3) Thereafter any instance of that type may colon-call
demo:run_i32(5)
local Demo = CSharp.AC.Demo
local calc = Demo()

-- Or obtain a direct closure from a single-candidate [LuaAlias] key
local run = calc.run_i32
zlua.register_method("run_custom_i32", run)
calc:run_custom_i32(20)

local add = Demo.add_i32
zlua.register_method("add_custom_i32", add)
assert(Demo.add_custom_i32(3, 5) == 8)
ParameterNotes
aliasNameNon-empty string; written as a new final Lua name into the method table
methodOrClosuredirect method closure (single candidate; MetaBinding::IsDirectMethodClosure, etc.)

Write target (inferred from embedded TypeBinding in the closure):

Closure domainWritten to
Static methodbinding->staticMap + static method index table
Instance ByValbinding->byvalInstanceMap
Instance ByObjbinding->byobjInstanceMap

Relation to existing keys (simplified overload management):

To avoid rewriting existing overload groups at runtime, register_method disallows aliasName that already exists in the target method table (corresponding static/instance map / methodTable) — whether that key is currently:

  • a single direct method; or
  • a dispatch overload group; or
  • a full-signature key or any other occupied method slot.
CaseBehavior
aliasName absentWrite direct closure (sole candidate under that name)
aliasName already present (direct or dispatch, etc.)luaL_error; no overwrite, no merge
Passing a dispatch closureluaL_error (only accept direct closures resolvable to a single candidate)

Difference from §5 [LuaAlias]: aliases at Bind time may collide and form overloads; register_method at runtime only “hangs on an empty slot” and does not merge overloads.

Difference from §3.7: full-signature keys already provide precise Bind-time entries; register_method solves readable short names + colon syntax, not “the only way to name an overload”.

Errors:

ConditionBehavior
Arg count ≠ 2luaL_error
Not recognizable as a legal direct method closureluaL_error
aliasName already occupied on the metatable method sideluaL_error

Native: __zlua_register_method (implemented on Il2Cpp).

Signature note: only two parameters (aliasName, closure); target table comes from the closure’s binding domain — no need to pass a type table or instance.

6.2 zlua.types

Preset mscorlib type-name strings; see 05-LIB.md §4.2.


7. Calling-convention summary

ScenarioSyntax
Default dispatchdemo:Run(10)
Precise overload (full-signature key, §3.7)demo['Run(System.Int32)'](demo, 10)
[LuaAlias] short namedemo:run_i32(20)
After register_methoddemo:run_custom_i32(20)
StaticDemo.Add(3, 5) / Demo['Add(System.Int32,System.Int32)'](3, 5)
Parameter parentheses alone as keydemo['(System.Int32)'](...) forbidden

Instance methods: full-signature keys use dot with explicit self; short aliases ([LuaAlias] / register_method) may use colon.


8. Mono / Il2Cpp consistency

ItemRequirement
Group by final name + dispatch §3 / §5Same
Full-signature keys on multi-candidate §3.7Same
Aliases may collide with default / other aliasesSame
Signature format §4.1Same
dispatch §3.3, §3.6Same
Selected overloadSame args → same C# overload
register_method two args; reject occupied namesSame
Error textIdentical or equivalent

9. Full example

public class Demo
{
public void Run(int value) { }

[LuaAlias("run_str")] // rename to "run_str"; no longer hangs default "Run"
public void Run(string value) { }

public void Foo(int x) { }

[LuaAlias("Foo")] // collides with default Foo → "Foo" group has Foo(int)+Bar(string); Bar no longer hangs "Bar"
public void Bar(string s) { }

public static int Add(int a, int b) => a + b;

[LuaAlias("add_i32")]
public static int Add(int x) => x;
}
local demo = CSharp.AC.Demo()

demo:Run(10) -- "Run" multi-candidate → dispatch → Run(int)
demo:Run("ab") -- dispatch → Run(string)
demo['Run(System.Int32)'](demo, 10) -- full-signature key → direct; no register_method
demo:run_str("x") -- single-candidate alias → direct

demo:Foo("hi") -- "Foo" has Foo(int) and Bar(string) → dispatch → Bar(string)

local run_i32 = demo['Run(System.Int32)']
zlua.register_method("run_cached", run_i32) -- must be an unused new name
demo:run_cached(20) -- short name + colon

local add = CSharp.AC.Demo.add_i32
zlua.register_method("add_one", add) -- OK: new name
-- zlua.register_method("Add", add) -- error: default name / overload group already exists
assert.equal(CSharp.AC.Demo.add_one(7), 7)

10. Implementation touchpoints (reference)

ModuleRole
ValueMarshaling / Mono equivalentConversionKind, GetConversionKind, TryPop
FindMatchingMethodapplicable + better member
MetaBindingdispatch, direct closure, register_method
ZLuaLib.cpp__zlua_create_signature, __zlua_register_method
Codegen[LuaAlias] written into metadata

C# Extension injection into IMT and merged competition: 13-EXTENSION-METHODS.