Skip to main content

13 — C# extension methods

How C# extension methods are exposed on the extended type’s Lua instance methodTable (colon calls). Applies to Il2Cpp (Player) and Mono (Editor). Member Bind → metatable/03-BINDING; overloads → 04-METHOD-OVERLOAD; aliases → same doc §5. User guide: Extension methods.


1. Goals and non-goals

1.1 Goals

ItemConvention
Lua UXobj:ExtFoo(...) calls configured-visible C# extensions
Discovery modelExtended type → list of extension classes; Bind reflects only those extension classes
Attribute[LuaExtension] on the extended type (may list multiple extension classes)
Call semanticsstatic-as-instance: enter IMT; self → CLR parameter 0
Same nameMerged competition with real instance methods (no “instance first”)

1.2 Non-goals

ItemStance
Globally scanning all ExtensionAttributeNo
Putting [LuaExtension] on the extension class then reverse-looking up the extended typeNo (would require scanning whole assemblies for discovery)
Calling only as static methods on the extension class SMT as “extensions supported”Insufficient; must be IMT + colon
Il2Cpp Player reading XML at runtimeNo (same as LuaAlias / MarshalAs)
Open generic extension methods (unclosed)Unsupported

1.3 Locked decisions summary

ItemDecision
Config keyExtended type; value is extension-class list (not a scattered method list)
Method filterExtensionAttribute + public static + first parameter assignable from the target type (including inheritance)
Attribute siteOnly the extended type
OverloadsMerged competition

2. Configuration sources (discovering extension-class lists)

When Binding type T, the extension-class list = Attribute union ∪ XML union (see §2.3).

2.1 [LuaExtension] (on the extended type)

using ZLua;

[LuaExtension(typeof(TransformExt), typeof(TransformTweenExt))]
public class MyBehaviour : MonoBehaviour { }

// Cannot edit third-party type sources: do not try to annotate the extension class; use §2.2 XML
ItemConvention
TargetType (class / struct / interface and other Bindable types)
ArgumentsOne or more System.Type, each an extension class (usually static)
AllowMultipleAllowed; type lists from multiple Attributes are unioned
Inheritance metadataWhen Binding T, walk the BaseType chain and collect [LuaExtension] on T and bases (no need to scan unrelated types)
InterfacesMethods enter T’s instance tables only when extension classes are configured for interface type U and the T being Bound matches this U (see §3); do not auto-inject extension-class lists merely because “T implements some interface” without configuration on T/bases

Forbidden to use [LuaExtension] on the extension class as a discovery mechanism.

2.2 XML (luaExtensionXmlPaths / ZLuaExtensions)

Separate files and Settings fields from LuaAlias:

ExtensionAlias
SettingsluaExtensionXmlPathsluaAliasXmlPaths
Root elementZLuaExtensionsZLuaAlias
<?xml version="1.0" encoding="utf-8"?>
<ZLuaExtensions version="1">
<Assembly name="UnityEngine.CoreModule">
<Type fullName="UnityEngine.Transform">
<Extension assembly="Assembly-CSharp" fullName="MyGame.TransformExt"/>
<Extension assembly="Assembly-CSharp" fullName="MyGame.TransformTweenExt"/>
</Type>
</Assembly>
</ZLuaExtensions>
AttributeNotes
Assembly/@nameShort name of the assembly containing the extended type
Type/@fullNameCLR full name of the extended type
Extension/@assemblyShort name of the assembly containing the extension class
Extension/@fullNameCLR full name of the extension class

This file only allows the structure above; do not write Method / MarshalAs / alias, etc.

2.3 Merge and platforms

ItemConvention
Same extended typeAttribute list ∪ XML list (union; unlike Alias’s “single-method rename override”)
MonoInitialize loads luaExtensionXmlPaths; resolve at Bind
Il2CppGenerate writes a static table (like AliasCodegen); Player does not read XML; re-Generate after XML changes
Extension class unresolvableGenerate hard-fails; Mono Initialize/Bind errors (no silent drop)

3. Collecting extension methods at Bind time

In EnsureBinding(T), after collecting the type’s own (and inheritance-flattened) real instance methods:

  1. Obtain the extension-class list per §2 (dedupe).
  2. For each extension class, take public static methods that all satisfy:
    • Have System.Runtime.CompilerServices.ExtensionAttribute;
    • At least one parameter; call the first parameter type P0, which must be assignable from T (P0.IsAssignableFrom(T) or Il2Cpp equivalent), so this Base works for Derived;
    • Not an open generic method (unclosed generic extensions unsupported).
  3. Passing methods join as instance-domain candidates into byobjInstanceMap; if T is a struct, also write byvalInstanceMap (same dual-shape as 03-BINDING §5).
  4. Final Lua names still follow [LuaAlias] / Alias XML / MethodInfo.Name (extensions may also be renamed).
  5. Group with real instance methods by final name → §5 merged competition.

Extension classes with no methods matching this: allowed (empty contribution); implementations may Warning. No ExtensionAttribute or not static: ignore. Already-EnsureBinding’d T does not auto-rebind because assemblies load later (same as Alias).


4. static-as-instance

On the CLR, extensions are static; on Lua they must appear as instance methods.

ItemRule
TableOnly instance methodTable (IMT); this mechanism does not hang extension methods on the extended type’s SMT
CallStatic Call / equivalent Invoke; stack slot 1 = receiver → CLR parameter 0; remaining args from slot 2 align with formals after this
luaArity= CLR formal count minus 1 (drop this)
Full-signature keyMethodName(ParamTypeFullNames…) includes only parameter type full names after this, aligned with real instance method keys (see 04 §3.7)
Mono / Il2CppEmit and MethodBridge must both recognize the “extension candidate” flag

Forbidden:

  • Generating via the ordinary static path (no receiver / requiring scripts to pass this explicitly);
  • Generating via the ordinary instance path (virtual/instance this resolution against the extension method’s declaring type).

5. Overloads: merged competition

  • If an extension and a real instance method share the same final Lua name, they enter the same overload group (same is_static=false domain, same ByVal/ByObj shape).
  • Single candidate → direct; multiple → dispatch; on collision also hang full-signature keys — same rules as 04-METHOD-OVERLOAD §3.
  • Do not insert a “instance method beats extension” tie-break.
  • Equal-score tie-break still uses §3.2 (codegen / declaration order, etc.).
  • When the same effective Lua signature is ambiguous or wrong: disambiguate with a full-signature key or [LuaAlias].

When matching: an extension candidate’s CLR formal sequence used for scoring is the sequence after dropping this, aligned with Lua arg slots the same way as real instance methods.


6. Script-visible behavior (examples)

public static class TransformExt
{
public static void ResetLocal(this Transform t)
{
t.localPosition = Vector3.zero;
}
}

// When you can edit sources:
[LuaExtension(typeof(TransformExt))]
public class /* some wrapper or business type */ { }

// Or XML: Type=UnityEngine.Transform → Extension=TransformExt
local t = go.transform
t:ResetLocal() -- IMT; equivalent to TransformExt.ResetLocal(t)

Extension methods do not appear merely because they exist in the project; if not configured for that extended type (or base Attribute / XML), Lua sees nil.


7. Implementation hints (non-normative filenames)

SideHints
CommonLuaExtensionAttribute; LuaExtensionXmlLoader / Registry; Settings luaExtensionXmlPaths
MonoMetaBinding collects extensions into instance groups; MethodEmitter static-as-instance
Il2CppMetaBinding + Invoke* extension path; ExtensionCodegen → generated table (like AliasCodegen)