跳到主要内容

Delegate / 函数编组

规范性: C# Delegate 与 Lua 函数之间的双向编组。
相关: class ByObj 基础 → 06-CLASS.md[LuaInvoke]../01-HOST-API.md;C#→Lua byref → 04-OPAQUE.md;Lua→C# byref → 03-BYREF.mdto_delegate../05-LIB.md

平台原则: Mono 与 Il2Cpp 的 Lua 可见语义一致;实现路径可不同(Il2Cpp:构建期 C++ bridge;Mono:运行时 Expression Emit)。

明确不写 Event 专用子表: ZLua Event 特殊编组;订阅/退订使用普通方法 add_* / remove_*(见 ../02-TYPE-SYSTEM.md)。


1. 问题与目标

方向需求
C# delegate → Lua默认 当作普通 class(DelegateUserData)传入;可 Invoke / d(...)。若 delegate 由 Lua function marshal 而来targetLuaMethod),再 Push 时须还原为 Lua function(§3)
Lua function → C# delegateLua 调用带 delegate 形参的 C# 方法时,隐式将 Lua function marshal 为对应类型 delegate
目标说明
无感与普通参数一样:Lua 传 function,由 方法调用的 marshal 层 完成转换
性能Player 零反射;按 Invoke 签名 复用 bridge;Editor 按签名缓存 Emit
统一[LuaInvoke] 共用 push/pcall/pop 规则
安全funcRef 生命周期与 delegate 绑定;禁止悬空调用
可控Il2Cpp 未 codegen 的签名运行时明确报错;Mono 无法 Emit 的签名明确 NotSupportedException(禁止静默降级为反射/Method.Invoke 热路径)

2. 总体架构

核心组件:

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

3. C# delegate → Lua

3.1 分流规则(必读)

Push delegate 实例到 Lua 栈时,按 target 是否为 LuaMethod 分流:

判定C# → Lua 形态说明
targetLuaMethodLua functionlua_rawgeti(REGISTRY, luaMethod.funcRef);脚本得到 原 Lua 闭包
其它(原生 C# 多播、target 为普通对象等)DelegateUserData与普通 class 相同:Invoke + IMT.__call
PushDelegate(d):
if d == null → push nil; return
if IsLuaBoundDelegate(d): // target is LuaMethod (closed)
PushRef(d.LuaMethod.funcRef) // → Lua function
return
PushByObjUserData(d) // → DelegateUserData
规则
识别两平台:target is LuaMethod(或等价标志)
多播仅当 整条 invocation list 可还原为单一 Lua 源 时走 function 路径;否则 ByObjUserData,保持 C# 多播语义
往返Lua function → C# delegate → 再 Push:同一 funcRef 对应 function
[LuaMarshalAs(UserData)] 覆盖分流:Lua 回调源仍 Push function
nullnil

设计理由: 脚本传入的 Lua function 经 C# 中转后再取回时,应仍是 function,而不是「包着同样回调的 C# userdata」。

3.2 承载形态(非 Lua 回调源)

仅适用于 §3.1 的 DelegateUserData 分支。

  • delegate 实例为 普通托管对象 userdata(Il2Cpp:ObjectRegistry;Mono:GCHandle)。
  • 类型表与普通 class 相同(含闭合泛型 delegate)。
  • 静态成员SMT实例成员(含 Invoke)走 IMT无需为传递原生 delegate 单独生成 bridge。

3.3 Lua 调用方式(DelegateUserData)

写法说明
d:Invoke(a, b)显式调用 Invoke
d(a, b)IMT.__call → 收集参数并转发到 Invoke
local handler = someObj.Handler -- 原生 C# delegate
handler(42) -- __call
handler:Invoke(42)

-- 往返:Lua→C#→Lua 时得到 function
obj:RegisterCallback(function(v) end)
local f = obj.Callback
print(type(f)) -- "function"
f(1)

3.4 实例元表 __call

MulticastDelegate 子类的实例元表额外注册 __call仅 DelegateUserData;function 分支无 metatable):

栈布局:[delegate_ud, arg1, arg2, ...]
→ 收集 args
→ MulticastDelegate.Invoke
→ PushReturn
规则
Multicast保持 C# 多播语义
nullC# nullnil;对 nil 调用报错
开放 delegatetarget == null 的 open delegate:MVP 不支持
方向ByObjUserData 再走 §4 bridge;绑定到 Lua 的回调在 Push 时已按 §3.1 变为 function

4. Lua function → C# delegate

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

一般不需要显式创建 delegate:

-- 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(§7)
Lua 实参C# delegate 形参
function ... endLuaDelegateBinder.Create(形参类型, funcRef)
nilnull
delegate userdata直接传递
其它类型报错

类型来源:C# 方法声明的形参类型 为准;Lua 侧 不需要 再传 delegate 类型。

4.2 共同约定:LuaMethod + closed delegate

所有 Lua→C# delegate 的 target 均为 LuaMethod 实例 伪造任意 C# 对象的成员方法作为 target。

字段
delegate targetLuaMethod(closed)
delegate 入口平台 bridge(§4.3 / §4.4)

4.3 Il2Cpp:构建期 C++ DelegateBridges

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

// 示例: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;
}
  • void 返回Action 等):LuaPCall(..., 0),无 pop。
  • Invoke 上的 ref / out / in支持;C#→Lua(bridge 调脚本)默认 OpaqueValue,与 [LuaInvoke] 相同(04-OPAQUE.md)。
  • [LuaMarshalAs] 非默认语义:与普通方法 / [LuaInvoke] 同一套解析。

未注册签名: 运行时查表失败 → 明确报错,提示重新 Codegen。

4.4 Mono:Expression Emit bridge(失败须显式)

Mono 预生成 LuaDelegateShims;运行时按 delegateType.GetMethod("Invoke")Expression 树编译 bridge(按 Invoke 签名缓存工厂)。

说明
emit 时机首次遇到某 Invoke 签名时生成
缓存同签名只 emit 一次
marshalpush/pop 与 LuaCallInvoker 同一套规则
不支持签名无法 marshal 的参数 / 返回类型 → NotSupportedException禁止 静默 Method.Invoke / object[] 热路径)
ref/out/in支持(不因含 byref 而拒绝)

禁止:DynamicMethod 使用 Delegate.CreateDelegate(Unity Mono 下可能 SIGSEGV);使用 Expression.Lambda(...).Compile()

Mono rewrite 原则: 若某 Invoke 签名无法 Expression Emit,须 绑定期或首次使用时明确失败,不得 invent 临时慢路径并可能流入 Player。

4.5 可选:显式 zlua.to_delegate

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

local d = zlua.to_delegate(function(a) return a end, closedFuncIntIntType)
obj:RegisterCallback(d)
参数说明
funcLua function
delegateTypeTable已闭合的 delegate 类型表

实现调用同一 LuaDelegateBinder.Create;返回 delegate userdata

Native: __zlua_to_delegate


5. 与 [LuaInvoke] 的边界

[LuaInvoke]Delegate bridge
调用方向C# → Lua(固定模块函数)C# → Lua(经 closed delegate 回调脚本)
绑定时机构建期 module + name → LuaInvokeSite运行时 luaL_reffuncRef
入口InternalCall / 生成 IC(Il2Cpp);C# 包装(Mono)closed delegate bridge
Marshalpush / pcall / pop同一套LuaCallInvoker
ref/out/in(C#→Lua)默认 OpaqueValue默认 OpaqueValue04-OPAQUE.md
params不支持不支持
目标固定 lua 模块函数任意 Lua closure

共用实现: LuaCallInvoker(Mono)/ InvokeFromRegistry(Il2Cpp)被 LuaInvokeRuntime::CallDelegateBridgesDynamicBridgeFactory 共同调用。

不属于 [LuaInvoke] Lua 调用 C# 方法时的 delegate 形参隐式 marshal(§4.1)走 MethodBridge → ReadDelegate,不是 [LuaInvoke]


6. 生命周期与 GC

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

Mono 注意: luaL_unref 须在持有 lua_State主线程 执行;~LuaMethod / DisposeAddPendingRefLuaEnv.ProcessPendingRefReleases() 在主线程批量释放。

C# delegate → Lua:

  • Lua 回调源(§3.1):Push function 新建 registry ref
  • 原生 C# delegate:ByObjUserData 由 __gc 释放 GCHandle; pin Lua 函数

7. Mono(Editor)与 Il2Cpp(Player)

Il2Cpp(Player)Mono(Editor)
C# delegate → Lua§3.1 分流相同
Lua→C# bridge构建期 C++ DelegateBridges.cpp运行时 Expression Emit(按签名缓存)
delegate 绑定SetClosedDelegateInvokeExpression.Lambda(...).Compile()
Codegen需要(§8)不需要 delegate shim 预生成
未支持签名运行时查表失败 + Codegen 提示NotSupportedException(明确失败)
Lua 可见语义权威必须与 Il2Cpp 一致

8. Codegen 与签名表(Il2Cpp)

Mono 不使用本节;Editor 由 DynamicBridgeFactory 运行时处理。

8.1 生成范围

MethodBridges.cpp 共用或同源扫描:

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

输出:generated/DelegateBridges.h/cpp

8.2 签名键

以 delegate 类型的 Invoke 方法 为准:

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

Mono Emit 缓存键与上述规则 对齐

8.3 未注册签名(Il2Cpp)

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

提示重新执行 ZLua Codegen。


9. 边界情况

场景MVP 策略
Action / Func<> / 自定义 delegate统一按 Invoke 签名 解析 bridge
C# delegate → Lua§3.1:Lua 回调源 → function;原生 C# → DelegateUserData + __call
ref/out/in(Lua→C# 调 delegate)03-BYREF.md
ref/out/in(delegate bridge C#→Lua)OpaqueValue;见 04-OPAQUE.md
params(bridge / [LuaInvoke]不支持
开放 delegate可不支持
Multicast 的 Lua 回调隐式 / 显式创建均为 单播
协变 / 逆变精确 delegate 类型匹配
LuaMarshalAsIl2Cpp:完整生成 bridge;Mono:不支持则 明确报错
Event 专用支持;用 add_* / remove_* 普通方法

10. 相关文档

文档内容
06-CLASS.mdDelegateUserData、门面
03-BYREF.mdLua→C# byref
04-OPAQUE.mdC#→Lua byref / bridge 回调
02-MARSHAL-AS.md[LuaMarshalAs] 合法集合
../01-HOST-API.md[LuaInvoke] 约束
../02-TYPE-SYSTEM.md委托类型表、__call
../05-LIB.mdto_delegate
../../impl/codegen/EMIT-MONO.mdMono Expression Emit