跳到主要内容

ZLua 函数 / Delegate Marshal 规范

本文档描述 C# Delegate 与 Lua 函数 之间的双向编组,适用于 Il2Cpp(Player)Mono(Editor)

相关文档:

文档内容
../../marshal/index.md参数编组总览
../../marshal/class.md引用类型 userdata、ObjectRegistry
../../type-system-spec.md委托类型表、__call
../../lib-spec.mdzlua 标准库(可选 to_delegate
../../architecture/il2cpp-architecture.mdLuaInvokeRuntimeMethodBridge、Codegen
../../marshal/struct.md[LuaMarshalAs] 与非默认 marshal

平台原则: Mono 与 Il2Cpp 的 Lua 可见语义一致;实现路径可不同:

方向Il2Cpp(Player)Mono(Editor)
C# delegate → Lua普通 class userdata + Invoke + IMT.__call相同
Lua function → C# delegateLuaMethod + 构建期 C++ DelegateBridgesLuaMethod + DynamicMethod bridge

1. 问题与目标

方向需求
C# delegate → Lua当作普通 class 传入 Lua;可调用 Invoke;并支持 d(...) 语法
Lua function → C# delegateLua 调用带 delegate 形参的 C# 方法时,隐式将 Lua function marshal 为对应类型 delegate
目标说明
无感与普通参数一样:Lua 传 function,由 方法调用的 marshal 层 完成转换,脚本无需 to_delegate
性能Player 侧零反射;按 Invoke 签名 复用 C++ bridge;Editor 侧 DynamicMethod 按签名缓存
统一[LuaInvoke] 共用 push/pcall/pop 规则;delegate 形参走 ReadValue / ReadDelegate 同一套入口
安全funcRef 生命周期与 delegate 绑定;禁止悬空调用
可控Il2Cpp 未 codegen 的签名在运行时明确报错;Mono 不支持签名明确报错

2. 总体架构

核心组件:

组件职责
LuaMethodLua→C# delegate 的 closed target;持有 funcRef(registry ref)
DelegateBridges(Il2Cpp)构建期生成的 C++ bridge,按 Invoke 签名注册
DynamicBridgeFactory(Mono)运行时按 Invoke 签名 DynamicMethod emit,按签名缓存
LuaDelegateBinder创建 delegate:LuaMethod + bridge;由隐式 marshal / to_delegate 调用
ReadDelegate栈上 Lua function → delegate;与 ReadValue 并列,供方法调用路径使用
LuaCallInvokerfuncRef + push + pcall + pop;Delegate bridge 与 [LuaInvoke] / LuaFunc 共用

3. C# delegate → Lua

本方向不做单独的 delegate marshal。 delegate 实例与普通引用类型 class 相同:经现有 class userdata 路径进出 Lua。

3.1 承载形态

  • delegate 实例为 普通托管对象 userdata(Il2Cpp:ObjectRegistry + Il2CppMulticastDelegate*;Mono:GCHandle + ValueTypeMarshaling.PushBoxedInstance 同类路径)。
  • 类型表与普通 class 相同(含闭合泛型 delegate,如 System.Action\1[System.Int32]`)。
  • 静态成员走类型表 SMT实例成员(含 Invoke)走 IMT,由 RegisterInstanceMethods 注册,无需为传递 delegate 单独生成 bridge。

3.2 Lua 调用方式

写法说明
d:Invoke(a, b)显式调用已暴露的实例方法 Invoke(与普通 class 方法相同)
d(a, b)IMT.__call → 收集参数并转发到 Invoke
local handler = someObj.Handler -- C# event / 字段 / 属性返回的 delegate
handler(42) -- __call
handler:Invoke(42) -- 显式 Invoke

3.3 实例元表 __call

MulticastDelegate 子类的实例元表额外注册 __call

栈布局:[delegate_ud, arg1, arg2, ...]
→ 收集 args
→ MulticastDelegate.Invoke / DynamicInvoke(args)
→ PushReturn
规则
Multicast保持 C# 多播语义(调用整条链)
nullC# null → Lua nil;对 nil 调用报错
开放 delegatetarget == null 的 open delegate:MVP 不支持
方向本路径 进入 Lua VM;除非 delegate 本身绑定到 Lua 回调(见 §4)

3.4 实现要点

平台要点
MonoPushInstanceMetatable 判定 typeof(MulticastDelegate).IsAssignableFrom(type) 时注册 __callInvoke 已由 TypeMethodRegistration 暴露
Il2Cpp同上语义;__callInvoke 可共用 MethodBridge 调用 invoke_impl

禁止: 为「仅 C#→Lua 传递 delegate」单独生成 Lua 回调或额外 marshal 层。


4. Lua function → C# delegate

4.0 主路径:方法参数隐式 marshal(默认)

一般不需要在 Lua 里显式创建 delegate。当 Lua 调用 C# 方法,且某形参类型为 delegate 时,由 方法调用的 marshal 层 在入参阶段完成转换。

-- C#: void RegisterCallback(Action<int> onValue)
obj:RegisterCallback(function(v) print(v) end)

流程:

1. 按 MethodInfo 解析第 N 个形参类型为 delegateType
2. 栈上该位置为 Lua function(或 nil → null delegate)
3. ReadDelegate(L, index, delegateType)
→ luaL_ref 得到 funcRef
→ LuaDelegateBinder.Create(delegateType, funcRef)
4. 将生成的 Delegate 填入 C# 调用参数
5. 方法返回后:若 delegate 未被 C# 长期持有,随 GC 回收 LuaMethod 并 unref(§6)
Lua 实参C# delegate 形参
function ... endLuaDelegateBinder.Create(形参类型, funcRef)
nilnull
delegate userdata直接传递(已是 C# delegate)
其它类型报错(与 int/string 等形参类型不匹配同理)

类型来源:C# 方法声明的形参类型 为准(已闭合的 Func<int,int> 等),不需要 Lua 侧再传 delegate 类型。

intstringclass 形参共用 方法桥接 → ReadValue / ReadDelegate 规则;详见 ../../marshal/index.md

4.1 设计结论(不采用的路径)

方案结论
为每种 delegate 签名预生成 C# 静态 shim 类LuaDelegateShimsMono 不采用;维护成本高、程序集膨胀
运行时拼装 MethodInfo 作为主路径不推荐
每次 Invoke 全反射调用 Lua不推荐;仅作调试

4.2 共同约定:LuaMethod + closed delegate

LuaMethod

namespace ZLua
{
/// <summary>
/// Lua→C# delegate 的 target;持有 Lua registry 中的函数引用。
/// </summary>
public sealed class LuaMethod : IDisposable
{
public int RefIndex { get; } // luaL_ref(L, LUA_REGISTRYINDEX)
// ...
}
}
  • 所有 Lua→C# delegate 的 target 均为 LuaMethod 实例
  • 伪造「任意 C# 对象的成员方法」作为 target。

绑定形态(概念)

字段
delegate targetLuaMethod(closed)
delegate 入口平台相关的 bridge(见 §4.2.1 / §4.2.2)

LuaDelegateBinder.Create§4.0 隐式 marshal§4.3 显式 API 共用。

4.2.1 Il2Cpp:构建期 C++ DelegateBridges

对每种需要的 delegate Invoke 签名,在构建期生成 C++ closed delegate 入口(与 MethodBridges.cpp 同源扫描策略):

// 示例:System.Func<int, int>
static int32_t Bridge_Func_int32__int32(Il2CppObject* target, int32_t a)
{
const LuaMethod* m = reinterpret_cast<LuaMethod*>(target);
lua_State* L = LuaEnv::GetState();
const int top = lua_gettop(L);

lua_rawgeti(L, LUA_REGISTRYINDEX, m->funcRef);
Marshaling::PushDefault<int32_t>(L, a);
Marshaling::LuaPCall(L, 1, 1);
const int32_t ret = Marshaling::PopDefault<int32_t>(L, -1);
lua_settop(L, top);
return ret;
}

绑定:

Il2CppDelegate* LuaDelegateBinder::Create(Il2CppClass* delegateClass, int funcRef)
{
Il2CppObject* luaMethod = ObjectRegistry::Alloc<LuaMethod>();
luaMethod->funcRef = funcRef;

Il2CppDelegate* del = AllocMulticastDelegate(delegateClass);
Il2CppMethodPointer bridge = DelegateBridges::Resolve(delegateClass);
il2cpp_codegen_set_closed_delegate_invoke(del, luaMethod, bridge);
return del;
}
  • void 返回Action 等):LuaPCall(..., 0),无 pop。
  • delegate Invoke 上的 ref / out[LuaMarshalAs] 非默认语义:生成专用 bridge 或报错(§9)。普通 C# 方法 的 ref/out 见 ../../marshal/index.md §3。

4.2.2 Mono:DynamicMethod bridge

Mono 预生成 LuaDelegateShims;运行时按 delegateType.GetMethod("Invoke")Expression 树编译 bridge(按 Invoke 签名缓存 Func<LuaMethod, Delegate> 工厂)。不要DynamicMethod 使用 Delegate.CreateDelegate(Unity Mono 下可能 SIGSEGV)。

// 生成的 bridge 形态(概念):
// Func<LuaMethod, Action<int>> factory = target => a => LuaCallInvoker.InvokeVoidWithArgs(target, paramTypes, new object[] { a });
// 每个 Lua function 调用 factory(luaMethod) 得到 C# delegate
说明
emit 时机首次遇到某 Invoke 签名时生成
缓存ConcurrentDictionary<MethodInfo, MethodInfo>,同签名只 emit 一次
marshalpush/pop 与 TypeMethodRegistration / LuaCallInvoker 同一套规则
适用范围仅 Mono / Editor;不进入 Il2Cpp Player 程序集
不支持签名ref/out、无法 marshal 的参数类型 → 明确 NotSupportedException

4.3 可选:显式 zlua.to_delegate

仅在需要 先构造 delegate 再传递 时使用。非常规路径。

local d = zlua.to_delegate(function(a) return a end, closedFuncIntIntType)
obj:RegisterCallback(d)
zlua.to_delegate(func, delegateTypeTable) → delegateUserdata
参数说明
funcLua function
delegateTypeTable已闭合的 delegate 类型表(无方法形参上下文时须显式给出类型)

实现调用同一 LuaDelegateBinder.Create;返回 delegate userdata(普通 class 实例)。

Native: __zlua_to_delegate(可选 API)

4.4 示例:Func<int, int>(隐式)

-- C#: void Run(Func<int,int> f) { f(1); }
obj:Run(function(a) return a + 1 end)

回调链(两平台语义相同,bridge 实现不同):

Run(delegate, ...)
→ ReadDelegate 创建 delegate(target = LuaMethod)
→ delegate.Invoke(1)
→ [Il2Cpp] Bridge_Func_int32__int32
[Mono] DynamicMethod bridge
→ lua_rawgeti(funcRef) + push(1) + pcall + pop

5. 与 [LuaInvoke] 的关系

[LuaInvoke]Delegate bridge
绑定时机构建期 module + name → LuaInvokeSite运行时 luaL_reffuncRef
入口InternalCall / 生成 IC(Il2Cpp);C# 包装(Mono)closed delegate bridge
Marshalpush / pcall / pop同一套LuaCallInvoker
目标固定 lua 模块函数任意 Lua closure

可抽取公共实现:

// Mono 概念 API
internal static class LuaCallInvoker
{
public static void InvokeVoid(LuaMethod target, Action<IntPtr> pushArgs);
public static T Invoke<T>(LuaMethod target, Action<IntPtr> pushArgs);
}
// Il2Cpp 概念 API
struct LuaCallContext { int funcRef; };
template<typename Ret, typename... Args>
Ret InvokeFromRegistry(const LuaCallContext& ctx, Args... args);

LuaInvokeRuntime::CallDelegateBridges(Il2Cpp)、DynamicBridgeFactory(Mono)均调用上述公共路径。


6. 生命周期与 GC

事件行为
隐式 marshal / to_delegatefuncRef = luaL_ref(REGISTRY);delegate 持有 LuaMethod
delegate 被 C# GCLuaMethod 终结 / Dispose → 排队 luaL_unref
Lua 函数无其他引用registry ref 仍持有,直至 delegate 释放
delegate 存活期间调用正常 pcall
funcRef 已失效仍调用报错(不应发生若 unref 仅随 delegate 释放)

Mono 注意: luaL_unref 须在持有 lua_State主线程执行。~LuaMethod / DisposeAddPendingRefLuaEnv.ProcessPendingRefReleases()(或等价钩子)在主线程批量 ClearPendingRefs

C# delegate → Lua 方向:delegate userdata 由 __gc 释放 GCHandle; pin Lua 函数。


7. Mono(Editor)与 Il2Cpp(Player)

Il2Cpp(Player)Mono(Editor)
C# delegate → Lua普通 class userdata + Invoke + IMT.__call相同
Lua→C# bridge构建期 C++ DelegateBridges.cpp运行时 Expression 编译(按 Invoke 签名缓存工厂)
delegate 绑定SetClosedDelegateInvokeExpression.Lambda(...).Compile()使用 Delegate.CreateDelegate + DynamicMethod
Codegen需要(§8)不需要 delegate shim 预生成
未支持签名运行时查表失败,提示 CodegenExpression 编译失败 / 明确 NotSupportedException
Lua 可见语义权威必须与 Il2Cpp 一致

8. Codegen 与签名表(Il2Cpp)

Mono 不使用本节 Codegen;Editor 侧由 DynamicBridgeFactory 在运行时处理。Il2Cpp Player 仍依赖构建期生成。

8.1 生成范围

MethodBridges.cpp 共用或同源扫描:

  • 所有带 delegate 形参 的 public 方法(从 MethodInfo 推导 Invoke 签名)
  • 构建配置中的 delegate 白名单(可选)

输出:generated/DelegateBridges.h/cpp(可与 MethodBridges 合并为 BridgeRegistry)。

8.2 签名键

以 delegate 类型的 Invoke 方法 为准(非 C# 委托类型名):

void(System.Int32) → Action<int>
System.Int32(System.Int32) → Func<int,int>
void(System.Int32, System.String) → Action<string> 等

Mono DynamicBridgeFactory 缓存键与上述规则对齐,便于两平台行为一致。

8.3 未注册签名(Il2Cpp)

运行时:

unsupported delegate signature for Lua callback: System.Func<...>

提示重新执行 ZLua Codegen。


9. 边界情况

场景MVP 策略
Action / Func<> / 自定义 delegate统一按 Invoke 签名 解析 bridge
C# delegate → Lua当普通 class;Invoke + __call;无单独 marshal
ref / out / in 参数(Lua→C# 方法../../marshal/index.md §3:StructUserData 真 ref,否则拷贝语义
ref / out 参数(delegate bridge不支持DynamicBridgeFactory / Codegen 报错
params可不支持
开放 delegate(无 target)可不支持
Multicast 的 Lua 回调隐式 / 显式创建均为 单播;C# += 仍由 BCL 组合
协变 / 逆变精确 delegate 类型匹配
LuaMarshalAsIl2Cpp:完整生成 bridge;Mono:不支持则报错

10. TYPE_SYSTEM 与 MARSHAL 衔接

  • 委托 类型表../../type-system-spec.md §10。
  • §3 规范 C# delegate → Lua单独 marshal;普通 class + IMT.__call
  • §4.0 规范 Lua function → C# delegate默认路径:并入方法参数 ReadDelegate
  • §4.3 to_delegate 仅用于无方法调用上下文的显式构造。

11. 实现清单

11.1 共用

  • LuaMethod 类型(RefIndexDispose / 终结器)
  • LuaCallInvokerfuncRef + push + pcall + pop)
  • LuaDelegateBinder.Create(delegateType, funcRef)
  • ReadDelegate(方法调用隐式路径,必做
  • delegate 实例 IMT.__callMulticastDelegate 子类)
  • LuaMethod 释放时排队 luaL_unref + 主线程 ProcessPendingRefReleases
  • (可选)zlua.to_delegate + __zlua_to_delegate

11.2 Il2Cpp(Player)

  • generated/DelegateBridges.* 与签名注册表
  • LuaDelegateBinder::Create + SetClosedDelegateInvoke
  • LuaCallContext / 与 LuaInvokeRuntime 共用 marshal
  • Codegen 扫描 delegate 签名

11.3 Mono(Editor)

  • DynamicBridgeFactoryExpression 编译 + 按 Invoke 签名缓存工厂)
  • LuaDelegateBinder.Createfactory(luaMethod),无 Delegate.CreateDelegate
  • TypeMethodRegistration delegate 形参分支(function / nil / userdata)
  • 实现 LuaDelegateShims 预生成

11.4 文档

  • ../../marshal/index.md / ../../lib-spec.md 交叉引用