Skip to main content

01 — Host API

LuaAppDomain (including GetFunction), [LuaMarshalAs], [LuaAlias]. C#→Lua / Lua→C# Marshal details: marshal/.


1. LuaAppDomain

1.1 Responsibilities

ZLua.LuaAppDomain is the only recommended host initialization façade. Common does not reference Mono/Il2Cpp; on Initialize / GetFunction it reflectively creates the backend nested type Runtime : ILuaRuntime for the current environment:

EnvironmentBackend host typeAssemblyCreation
EditorLuaMonoAppDomainZLua.MonoActivator.CreateInstance(…+Runtime)
PlayerLuaIl2CppAppDomainZLua.Il2CppSame (#if !UNITY_EDITOR branch)
public interface ILuaRuntime
{
void Initialize(Func<string, object> moduleLoader);
void Reset(Func<string, object> moduleLoader);
void ProcessPendingRefReleases();
Delegate GetFunction(Type delegateType, string luaModule, string luaMethodName);
}

public static class LuaAppDomain
{
public static void Initialize(Func<string, object> moduleLoader);
public static void Reset(Func<string, object> moduleLoader);

public static T GetFunction<T>(string luaModule, string luaMethodName)
where T : MulticastDelegate;

internal static void ProcessPendingRefReleases(); // Driven by LuaFramePump
}

Does not use RuntimeInitializeOnLoadMethod / SetRuntime for implicit registration; the backend is resolved on the first Initialize (or GetFunction). The host surface does not expose Shutdown; tearing down and rebuilding the domain goes only through Reset.

1.2 Domain reset (Reset)

Used for hot reload or clearing the script world: tear down the single main lua_State, then recreate it with the given loader.

APIBehavior
Reset(loader)Schedule only: store the loader; actual teardown + rebuild runs at this frame’s EndOfFrame (LuaFramePump / WaitForEndOfFrame). Multiple schedules keep the last loader. When applied: drain pending refs → shut down Registry / module cache → lua_close → new lua_State and install loader. Il2Cpp process-level Bridge / XML tables / InternalCall are kept.
Initialize(loader)Only on first use (or when there is no main state yet) create lua_State and install the loader. Calling again when already initializedthrows (use Reset; no longer supports “swap loader only”).

Contract:

  1. Reset does not tear down the state at the call site; after EndOfFrame applies, invoking old GetFunction delegates → throws a C# exception.
  2. Old delegates are all invalid; the host must re-GetFunction after Reset takes effect and discard cached Action / Func fields.
  3. Because real teardown is at EndOfFrame, calling Reset mid C#↔Lua call is allowed (queue only); do not keep using old delegates after that frame’s EndOfFrame.
  4. Editor Emmy: teardown stops the debugger with lua_close; after rebuild, Settings decide whether to restart.

See 10-LIFETIME.md §7.

1.3 Module loader

moduleLoader(moduleName) is provided by the host and returns Lua module source (usually a string). Native integrates it with __zlua_load_module and package.searchers.

Conventions:

  • Module names match the luaModule string passed to GetFunction
  • Loader failures should throw a clear exception; avoid silent nil

1.4 Frame pump

LuaAppDomain.Initialize registers LuaFramePump: LateUpdate drains pending refs; WaitForEndOfFrame runs a scheduled Reset. See 10-LIFETIME.md.

1.5 Editor debugger (optional)

Editor Mono only: if Settings enableDebugger is true, LuaMonoAppDomain.Initialize calls LuaEnv.StartDebugger after the normal init completes (inject emmy_core, tcpListen, optional waitIDE). Spec: build/04-EMMYLUA-DEBUGGER.md. Does not change GetFunction / Marshal semantics; Il2Cpp Player does not use this entry point.


2. GetFunction — C# calling Lua

The only formal entry for C#→Lua: obtain a bound Delegate by module and method name, then the caller Invokes (or calls it directly).

2.1 Signature

public static T GetFunction<T>(string luaModule, string luaMethodName)
where T : MulticastDelegate;
ParameterDescription
luaModuleNon-empty; module name passed to moduleLoader / require
luaMethodNameNon-empty; key in the module’s return { ... } table
TConcrete delegate type (e.g. Action, Action<float>, Func<int,int,int>)

2.2 Behavior

  1. Load (or hit cache for) the module table for luaModule
  2. Read module[luaMethodName]; must be a Lua function
  3. Marshal the function to a closed delegate matching T’s signature (same rules as marshal/09-FUNCTION.md)
  4. Return that T instance

Caching: The API does not guarantee reusing the same delegate instance across calls; hot paths should cache locally (field / local). Call only after Initialize (e.g. Awake); do not put it in a static field initializer of the same kind as RuntimeInitializeOnLoadMethod. After Reset takes effect, old delegates are invalid — re-GetFunction.

2.3 Examples

// One-shot / startup obtain
var add = LuaAppDomain.GetFunction<Func<int, int, int>>("app", "add");
int sum = add(10, 20);

var onTick = LuaAppDomain.GetFunction<Action<float>>("game", "OnTick");
onTick(0.016f);
-- app.lua
local function add(a, b) return a + b end
return { add = add }

2.4 Errors

ConditionBehavior
Not Initialized / loader not configuredThrow C# exception
Module load failure / missing key / not a functionThrow C# exception (with diagnostic info)
T cannot bind from that function (incompatible signature, etc.)Throw C# exception

2.5 Invoke and Marshal

When Invokeing the returned delegate:

2.6 Flow (conceptual)

GetFunction<T>(module, method)
→ require / get module table
→ get Lua function
→ Marshal to T
→ return T

Thereafter: T.Invoke(...)
→ marshal args (including ref → OpaqueValue)
→ lua_pcall
→ marshal returns / write back refs
→ exception boundary translation (§6)

Il2Cpp C# init remains a thin shell (unrelated to GetFunction):

public static class LuaIl2CppAppDomain
{
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void InitializeInternal(Func<string, object> moduleLoader);

public static void Initialize(Func<string, object> moduleLoader)
=> InitializeInternal(moduleLoader);
}

3. [LuaMarshalAs] — Marshal annotation

3.1 Scope

Annotatable siteDescription
ParameterControls Push/Pop for that formal parameter Lua↔C#
Return valueControls C#→Lua return Push
Field / propertyControls marshal on member read/write (consumed by codegen)

Forbidden on methods (LuaMarshalAsConfigurationException at bind time).

Full options: marshal/02-MARSHAL-AS.md.

3.2 Common options (summary)

LuaMarshalTypeUse
DefaultType default rules
OpaqueValueForce opaque lightuserdata C#→Lua (by-val reference types / struct)
Table / UnpackedValuesstruct / closed generic struct only; Table also allows Nullable<struct> (requires Members)

Default rules summary:

  • C#→Lua ref/in/outOpaqueValue (no attribute needed)
  • by-val primitives / enum → Lua boolean / integer / number / string
  • class → ByObj userdata; struct → ByVal or Handle (see struct volumes)
  • params T[] → same as szarray single stack slot (table / userdata / nil); does not collect trailing multi-slot args; no dedicated LuaMarshalType

3.3 Validation timing

  • Mono Attribute: illegal combinations → error log + fall back to Default (see marshal/02-MARSHAL-AS.md §4.1); does not fail the bind.
  • Il2Cpp Generate / MarshalAs XML: configuration errors may hard-fail (§4.2).

4. [LuaAlias] — method Lua aliases

[LuaAlias("run_i32")]
public void Run(int value) { ... }

[LuaAlias("Foo")] // Allowed to collide with existing method names / other aliases
public void Bar(string s) { ... }
  • Defined in ZLua.Common
  • Equivalent to using that string as the method’s sole final Lua name (replaces the default MethodInfo.Name; not dual-registered)
  • Prebuilt DLLs may use a separate XML (Settings luaAliasXmlPaths, root element ZLuaAlias); must not be written into MarshalAs XML. See 04-METHOD-OVERLOAD.md §5.4
  • Allowed to collide with other aliases or existing method names; on collision, multiple candidates share that final name and calls use overload dispatch (see 04-METHOD-OVERLOAD.md §5)
  • If a final name has only this one candidate (e.g. a unique run_i32), it is a direct closure

Full rules: 04-METHOD-OVERLOAD.md §3, §5.


5. Lua→C#: no [MonoLuaCallback] required

When Lua calls C# members, native generates bridge closures for each public member during EnsureBinding and writes them into the three tables. App code does not need and is not given a [MonoLuaCallback] marker.

Each ReducedType (Il2Cpp) or full signature (Mono Emit) maps to a unique bridge entry.


6. Exception boundary

6.1 C# calling Lua

DirectionBehavior
Lua error()Caught as a C# exception (LuaException or wrapper); does not leak unhandled native longjmp past the managed stack
C# exception entering nativeConverted at the boundary to a Lua error or recorded then rethrown (implementation is unified)

Scripts should not depend on the exact type string of a C# exception caught inside pcall; only “failure is detectable” is guaranteed.

6.2 Lua calling C#

DirectionBehavior
C# throwsConverted to a luaL_error-equivalent message; Mono / Il2Cpp wording is identical or equivalent
Lua sideUse pcall to catch the error string

6.3 Opaque and the boundary

An Opaque handle is valid only until the C#→Lua call that produced it returns; saving it across pcall and reusing it → error. See marshal/04-OPAQUE.md, 10-LIFETIME.md.


7. Codegen constraints (summary)

ItemConstraint
[LuaAlias]May collide with default names / other aliases; group by final name (see overload §5)
[LuaMarshalAs]Forbidden at method level; illegal Members → bind failure
Mono EmitSignatures that cannot Emit must fail explicitly; silent Method.Invoke on the hot path is forbidden
Il2Cpp stubUncovered signatures → build-time or first-bind failure (MethodBridge, etc.; see impl/codegen/)

C#→Lua does not rely on IL weave / dedicated stubs: it goes through GetFunction → Delegate bridge.


DocContent
00-OVERVIEW.mdDual runtime, initialization
04-METHOD-OVERLOAD.mddispatch, register_method
marshal/01-OVERVIEW.mdMarshal overview
marshal/09-FUNCTION.mdDelegate ↔ Lua function
10-LIFETIME.mdGC, single lua_State
reference/csharp/lua-app-domain.mdProgrammer API page