跳到主要内容

构建 — Editor Mono:EmmyLua 调试器

约定 ZLua Editor(ZLua.Mono 如何接入 EmmyLua emmy_core,使 VS Code / JetBrains 等 IDE 可对运行中的 Lua 断点调试。 上游仓库: EmmyLua/EmmyLuaDebugger(构建选项、用法以该仓库 README / docs 为准)。 改变 Lua 可见互操作语义;覆盖 Il2Cpp Player。 异常边界见 03-MONO-LUAJIT-CALLBACK-GATE.md;宿主入口见 01-HOST-API.md;多版本见 11-MULTI-VERSION.md


1. 目标与非目标

1.1 目标

约定
宿主Unity Editor + ZLua.Mono,单一 lua_State
调试库EmmyLua emmy_coreEmmyLuaDebugger);按 Lua 系列 + OS/Arch 分目录
IDEEmmyLua 协议客户端(VS Code EmmyLua 扩展、Rider 等)
连接Lua 侧 tcpListen;IDE Attach 到约定端口
开关Project Settings(ZLua.Settings)显式开启;默认关闭
随附范围Windows / macOSlua51lua55luajitLinux:目前仅 lua55linux-x64

1.2 非目标

态度
Il2Cpp Player / 真机调试本规范不覆盖(后续若做须另文)
WebGL不支持(无可用 TCP attach 模型)
自研 DAP Adapter不做;协议与 UI 由 EmmyLua IDE 扩展承担
C#↔Lua 混合调用栈美化不做(仅 Lua 栈由 Emmy 呈现)
业务脚本手写 require('emmy_core')非必需;由宿主统一注入(仍允许高级用户手动调用)
为 Linux 预编译全部系列不做(除 lua55);其它系列须按上游文档自建(见 §3.2)

2. 架构

IDE (EmmyLua)
↕ Emmy 调试协议 / TCP
emmy_core(Lua C 模块,由 require 加载)
↕ debug.sethook / 调试 API
ZLua 唯一 lua_State(Editor Mono)
职责
SettingsenableDebugger、端口、是否 waitIDE
LuaMonoAppDomain.Initialize初始化完成后若开启则调用 LuaEnv.StartDebugger
LuaEnv.StartDebugger拼接 package.cpathrequire('emmy_core')tcpListen → 可选 waitIDE
Plugins/emmylua/**仅作为 磁盘上的原生模块文件禁止由 Unity PluginImporter 自动加载

3. 包内布局、自建与 PluginImporter

3.1 系列目录命名(强制)

一级目录名与 Editor 原生库逻辑名一致(见 11-MULTI-VERSION.md):

引擎目录名规则示例
PUC-Rio(官方 Lua)lua{major}{minor}Lua 5.5.x → lua55;5.4.x → lua54;5.3.x → lua53
LuaJITluajit(不区分 2.0 / 2.1)任意 luajit-2.xluajit/

同一 大版本系列 共用一份 emmy_core(例如所有 lua-5.3.* 共用 lua53/所有 LuaJIT 2.x 共用 luajit/),不必按 patch / JIT 小版本分别构建。

平台子目录(二级):

Editor子目录文件
Windows x64win32-x64emmy_core.dll
macOS arm64darwin-arm64emmy_core.dylib
macOS x64darwin-x64emmy_core.dylib
Linux x64linux-x64emmy_core.so

完整路径示例(本包当前随附情况):

Packages/com.code-philosophy.zlua/Plugins/emmylua/
├── lua51/{win32-x64,darwin-arm64,darwin-x64}/…
├── lua52/{win32-x64,darwin-arm64,darwin-x64}/…
├── lua53/{win32-x64,darwin-arm64,darwin-x64}/…
├── lua54/{win32-x64,darwin-arm64,darwin-x64}/…
├── lua55/{win32-x64,darwin-arm64,darwin-x64,linux-x64}/…
└── luajit/{win32-x64,darwin-arm64,darwin-x64}/… # Emmy -DEMMY_LUA_VERSION=jit;2.0/2.1 共用

运行时按 当前 Editor 编译 define 选择目录:PUC → lua{major}{minor};任意 LuaJIT → luajit不是探测 DLL 内嵌 ABI。Editor 宿主 DLL 仍可为 luajit20.dll / luajit21.dll(与 emmy 目录名无关)。

3.2 本包随附范围与自建

系列Windows win32-x64macOS darwin-arm64 / darwin-x64Linux linux-x64
lua51lua55随附随附lua55 随附;其余自建
luajit(2.0/2.1 共用)随附随附不随附;自建

上游仓库:EmmyLua/EmmyLuaDebugger(本地亦可对照 3rd/EmmyLuaDebugger)。

Windows 本包构建约定(维护者):

cmake -G "Visual Studio 17 2022" -A x64 ^
-DCMAKE_USER_MAKE_RULES_OVERRIDE=<repo>/cmake/flags_override.cmake ^
-DEMMY_LUA_VERSION=<51|52|53|54|55|jit> ^
-DEMMY_CORE_VERSION=zlua ..
cmake --build . --config Release --target emmy_core

emmy_core/Release/emmy_core.dll 拷到 Plugins/emmylua/<series>/win32-x64/,并为 DLL / 目录编写 Unity .metaPluginImporter 全平台 enabled: 0(含 Editor)。 jit 产物放入唯一目录 luajit/win32-x64/(Emmy 区分 JIT 2.0 / 2.1;不要再编一份「luajit21」专用 emmy_core)。

macOS 本包构建约定(维护者): 在 Mac 上按上游文档对 EMMY_LUA_VERSION=51…55|jit 分别编 arm64 / x86_64,产物放入 darwin-arm64 / darwin-x64;PluginImporter 同样全禁用。源码模式可用 -DEMMY_USE_LUA_SOURCE=ON。Lua 5.1 源码模式下若缺 LUA_NUMTAGS,须在 Emmy 侧兼容(本包已随附的 lua51 darwin 二进制已处理)。

Linux、或需更换 Emmy 版本时: 阅读上游 README「Build Options」,对目标 OS/Arch 自建后放入 §3.1 对应目录;PluginImporter 同样全禁用。本包 Windows 随附二进制可与官方 CI 一致(默认 EMMY_USE_LUA_SOURCE,运行时动态解析宿主 Lua API);macOS 随附多为源码模式构建。

ZLua Install 自动编译 EmmyLuaDebugger。

3.3 缺失目录

StartDebugger 在注入脚本 之前 检查 Plugins/emmylua/<series>/ 及当前 OS/Arch 子目录是否存在:

  • 不存在Debug.LogError 说明期望路径(可提示 Linux 非 lua55 等需自建),跳过调试器,不抛异常(不中断 Initialize
  • 存在 → 再 require('emmy_core')require/listen 失败同样只打日志,不抛到宿主

3.4 PluginImporter(强制)

平台enabled
Editor0
Win / Win64 / OSX / Linux / WebGL / 其它0

理由: emmy_coreLua C 模块(经 package.cpath + require / luaopen_* 加载),不是 Unity 原生插件。若 Editor 勾选启用,Unity 会先 LoadLibrary,再与 Lua require 二次加载,易冲突或行为未定义。

可选更严布局:移出 Plugins/(例如 Editor/EmmyLua/),避免被当作插件扫描;若保留在 Plugins/emmylua必须以 meta 全平台禁用 满足本条。


4. Settings

ZLua.SettingsProjectSettings/ZLua.asset)新增(字段名以实现为准,语义如下):

字段类型默认说明
enableDebuggerboolfalse为 true 时,Initialize 末尾调用 StartDebugger
debuggerPortint9966tcpListen 端口
debuggerWaitIDEboolfalse为 true 时调用 dbg.waitIDE()(见 §7)

UI(SettingsProvider)须标明:

  • Editor Mono 生效
  • waitIDE == true 时会在 Unity 主线程阻塞无超时(见 §7)
  • 须与当前系列对应的 emmy_core 目录匹配(见 §6);IDE 侧须配置 sourcePaths(见 §10)

5. 启动流程

5.1 时机

LuaMonoAppDomain.Initialize 中,于下列步骤 全部完成之后 再启动调试器:

  1. 创建 / 复用 LuaEnv(含 luaL_openlibs
  2. SetModuleLoader
  3. LoadBuiltinGlobals、MarshalAs XML(若有)、AssemblyRegistryZLuaLibEnsureBuiltinZLuaLibDelegateBridges.Warmup 等现有初始化

这样断点可覆盖 CSharpzlua 与业务 require 模块。

早退路径_luaEnv != null 仅刷新 loader):不得再次 waitIDEStartDebugger幂等(已 listen 则跳过或仅确保监听,见实现)。

5.2 LuaEnv.StartDebugger(规范行为)

伪代码(Lua 片段由 C# DoString 注入;路径 / 端口 / 是否 wait 由 C# 代入):

package.cpath = package.cpath .. ";<absDir>/?.<ext>"
local dbg = require('emmy_core')
dbg.tcpListen('127.0.0.1', <port>)
-- 仅当 debuggerWaitIDE == true:
dbg.waitIDE()
约定
Host使用 127.0.0.1(避免部分环境下 localhost → IPv6 导致连不上)
路径绝对路径;Lua 字符串中目录分隔优先 /
失败require / listen 失败须在 Editor 打出明确错误(含 Lua 错误对象),不得静默吞掉
重复 append多次调用不得无限拉长 cpath;实现应检测已注入标记或已存在该目录项

5.3 平台宏与 cpath 映射

条件目录(相对包根 Plugins/emmylua/<ext>
UNITY_EDITOR_WINwin32-x64dll
UNITY_EDITOR_OSX + ARM64darwin-arm64dylib
UNITY_EDITOR_OSX + x64darwin-x64dylib
UNITY_EDITOR_LINUXlinux-x64so

包根解析:使用包内已知相对路径经 Path.GetFullPath(或与 CommonDirs 同类工具)得到绝对目录;禁止写死机器相关盘符。

Arch 判断以实现为准(如 RuntimeInformation / Unity 已有 Editor arch API),须覆盖 Apple Silicon。


6. ABI / Lua 版本匹配

6.1 事实:二进制不会自报 Lua 版本

官方/社区随附的 emmy_core 没有可靠的「读文件头即可知道面向 5.3 还是 JIT」的契约。EmmyLuaDebugger 在 编译期 用 CMake 选项选定 ABI,例如:

cmake .. -DEMMY_LUA_VERSION=53 # 或 51/52/54/55/jit

(见 EmmyLuaDebuggerEMMY_LUA_VERSION 决定宏与头文件。) 因此 ZLua 不能、也 不必 对裸 emmy_core.dll 做启发式 ABI 探测。

6.2 ZLua 怎么做「校验」(按系列目录,非探测式)

原则:用目录布局声明目标系列;运行时只检查对应目录是否存在。

做法说明推荐
B. 按系列分目录(本包采用)Plugins/emmylua/{lua55|…|luajit}/<platform>/emmy_core.*;PUC 系列名 = Editor DLL 逻辑名;JIT 统一 luajit
A. 单系列 + 常量/清单仅当不分目录时的退化方案否(已被 B 取代)
C. 仅 try requireABI/路径错误时常 native 崩溃,Lua pcall 拦不住禁止当作唯一校验

流程:

series = 当前编译 define → lua55 / luajit / …
dir = Plugins/emmylua/<series>/<platform>/
若 dir 不存在
→ LogError(写明 series 与期望路径)并 return;不抛异常、不 require
否则
→ 拼接 cpath 并 require('emmy_core')

Windows / macOS 已随附 lua51lua55luajit(见 §3.2);Linux 目前仅随附 lua55/,其它系列需自建。

6.3 与 ZLua 多版本的关系

luaVersionId → 系列目录自建时 EMMY_LUA_VERSION
lua-5.1.*lua5151
lua-5.2.*lua5252
lua-5.3.*lua5353
lua-5.4.*lua5454
lua-5.5.*lua5555(上游默认)
luajit-2.0 / luajit-2.1luajitjit

目录名规则再次强调:官方 Lua → lua{major}{minor};LuaJIT → 统一 luajit不要luajit20 / luajit21 拆 emmy 目录)。

Windows / macOS 上各系列应使用对应目录下的随附 emmy_core;Linux 或缺目录时按 EmmyLuaDebugger 自建并放入上表路径。不得把错误系列目录的二进制挪到另一系列目录凑合使用。

LuaJIT 2.0 / 2.1 均用上游 jit 构建同一 emmy_core,只放在 luajit/

ZLua Install 自动编译 Emmy。

6.4 LuaJIT 与 gate

调试期 hook 会抑制 JIT,性能下降可接受。emmy_core 为 native 模块,仍须遵守 03-MONO-LUAJIT-CALLBACK-GATE.md不得在托管 reverse-P/Invoke 帧内直接 lua_error)。


7. waitIDE 与主线程

debuggerWaitIDE行为
false(默认)tcpListen;IDE 稍后连接;阻塞 Editor
truewaitIDE()Unity 主线程同步等待 IDE 连接

上游无超时: EmmyLuaDebugger 文档中的 dbg.waitIDE() 不接受超时参数;未连上会一直阻塞。因此 Settings 默认关闭 wait。

推荐工作流:保持 debuggerWaitIDE = false → Unity Play(已 listen)→ IDE F5 连接 → 再触发业务 Lua。仅当必须「断在第一行业务前」且已先开好 IDE 时再开 wait。


8. 源码路径映射(chunk ↔ 磁盘)

ZLua 经 moduleLoader 加载的模块,chunk 名为:

@<module/path>.lua

(模块名中的 ./。)物理文件由宿主 loader 决定,例如本仓库测试工程:

requirechunk(调试器可见)磁盘文件
luatest/initluatest.init@luatest/init.lua{project}/Tests/Lua/luatest/init.lua
cases.foo.bar@cases/foo/bar.lua{project}/Tests/Lua/cases/foo/bar.lua

IDE 必须把 Lua 源码根(上例为 Tests/Lua)配进 Emmy 的 sourcePaths,否则会出现「能连上但 Could not load source / 断点不生效」。

约定
映射规则sourcePaths = moduleLoader 使用的源码根目录(可多个)
工作区用 IDE 打开工程根(含 Packages / Tests 的那一层),不要只打开 Tests/Lua
内置 chunkglobals.lua / zlualib.lua 等无稳定工程路径;不保证可断点
运行时改写 source本规范不强制;若实现改写,不得破坏现有 traceback 可读性

9. 与现有 Editor 约束的关系

机制调试器侧要求
Callback gate调试逻辑在 emmy_core / Lua 内完成;禁止为调试在托管回调里直接 lua_error
LuaPrintBuffer调试输出走 Emmy 通道;勿依赖回调帧内带堆栈的 Debug.Log
lua_State一个 listen 会话即可;不引入第二 state
LuaFramePump本阶段 不要求 为 Emmy 增加泵(默认不 waitIDE);若日后改为非阻塞协议,再与帧泵协作

10. IDE / EmmyLua 插件配置(VS Code · Cursor)

上游协议与扩展以 EmmyLuaDebugger、VS Code / Cursor 的 EmmyLua 扩展为准。ZLua 侧为 tcpListen游戏先听,IDE 再连

10.1 前置条件

  1. Install / Settings 使 Editor 运行 emmy_core 同系列 的 Lua(默认 lua-5.5.0lua55;其它系列见 §3.2 随附矩阵)。
  2. Project Settings → ZLua:enableDebugger = truedebuggerPort 与 IDE 一致(默认 9966),debuggerWaitIDE = false(推荐)。
  3. 安装 EmmyLua 扩展;用 IDE 打开 Unity 工程根目录
  4. Play / 触发 LuaAppDomain.Initialize;Console 出现 EmmyLua debugger listening on 127.0.0.1:…
  5. IDE 启动下方调试配置,再在源码根下的 .lua 文件打断点并触发对应 require

10.2 .vscode/launch.json(推荐模板)

type 使用扩展提供的 EmmyLua New Debug(常见值为 emmylua_new;若列表名不同以扩展为准)。

{
"version": "0.2.0",
"configurations": [
{
"type": "emmylua_new",
"request": "launch",
"name": "ZLua EmmyLua (Unity Editor)",
"host": "127.0.0.1",
"port": 9966,
"sourcePaths": [
"${workspaceFolder}/Tests/Lua"
],
"ext": [".lua"],
"ideConnectDebugger": true
}
]
}
字段说明
host与 ZLua 注入一致,用 127.0.0.1(避免 localhost → IPv6)
port与 Settings debuggerPort 相同
sourcePaths必填且对准 Lua 根目录;上例为仓库 Tests/Lua。业务工程改为自己的 LuaScripts
ext源文件后缀;仅 .lua 时写 [".lua"];若还有 .lua.txt 一并列出
ideConnectDebuggertrue:IDE 主动连已 tcpListen 的进程(匹配 ZLua 注入方式)
request扩展常见为 launch(New Debug);语义仍是「连到已 listen 的宿主」,勿与「IDE 替你启动 lua.exe」混淆

多源码根:sourcePaths 中追加多项,例如 "${workspaceFolder}/LuaScripts""${workspaceFolder}/Packages/xxx/Lua"

10.3 .emmyrc.json(语言服务,可选)

用于补全 / 诊断,不替代 launch.jsonsourcePaths。工程根示例:

{
"workspace": {
"library": []
},
"diagnostics": {
"disable": ["undefined-global"]
}
}

ZLua 大量使用 CSharp 等全局时,可按需关闭 undefined-global,减少噪音。

10.4 推荐操作顺序

Unity:enableDebugger +(可选)确认系列为 lua55
→ Play / Initialize → 日志 listening
Cursor / VS Code:打开工程根 → F5(上述配置)
→ 在 sourcePaths 下的 .lua 下断点
→ 触发 require / GetFunction 跑到该模块

10.5 常见问题

现象排查
Console:EmmyLua debugger skipped / 缺目录当前系列无 Plugins/emmylua/<series>/<platform>/;Win/macOS 各系列应已随附,Linux 非 lua55 按 §3.2 自建
DllNotFoundException: lua55(或其它系列)Settings / define 已切对应系列,但 Editor 原生库未就绪:确认 Plugins/lua/<series>/ 下存在对应 luaXX.dll / .dylib,PluginImporter Editor 启用,改版本后已 Install / 域重载
IDE 连不上 / 超时Unity 是否已 listen;端口是否一致;host 是否 127.0.0.1;防火墙
能连接,断点灰色 / Could not load sourcesourcePaths 未指向真实 Lua 根(少写了 Tests/Lua);或工作区不是工程根
一开 Play Editor 假死误开了 debuggerWaitIDE;关掉,或先 F5 再 Play
waitIDE 想设超时上游不支持;保持默认关 wait
扩展里没有 emmylua_new安装/启用 EmmyLua 扩展后重载窗口;以扩展实际提供的 Debug type 为准
仅 Cursor、无 VS Code同一套 .vscode/launch.json 与 EmmyLua 扩展即可

11. 验收清单

  • emmylua/** 下所有 emmy_core 的 PluginImporter 全平台 disabled
  • Windows / macOS 随附 lua51lua55luajit(各平台子目录);Linux 随附 lua55/linux-x64;其余按 EmmyLuaDebugger 自建
  • 系列目录命名:lua{major}{minor} / 统一 luajit(不按 2.0/2.1 拆分)
  • enableDebugger == false 时无 listen、无 cpath 注入、无阻塞
  • 当前系列目录缺失时:LogError 后 Initialize 成功完成(不抛)
  • Win / macOS (arm64+x64) / Linux Editor + 匹配系列:开启后 require('emmy_core') 成功且 IDE 可连接
  • debuggerWaitIDE == false 时 Initialize 立即返回;文档已说明 waitIDE 无超时
  • Initialize 早退路径不重复 waitIDE
  • launch.jsonsourcePaths 对准业务/测试 Lua 根;业务模块断点可命中
  • 开启调试时,现有 Mono gate / pcall 错误路径仍不崩溃

12. 相关文档

文档关系
EmmyLua/EmmyLuaDebugger上游构建、EMMY_LUA_VERSION、用法(权威)
01-HOST-API.mdInitialize 门面;调试在其后插入
10-LIFETIME.md单 state、异常边界
11-MULTI-VERSION.mdluaVersionId 与 Editor DLL 逻辑名
03-MONO-LUAJIT-CALLBACK-GATE.mdEditor 回调与 lua_error
02-LUAJIT.mdJIT 下 hook 性能预期
包内 Plugins/README.md目录与自建速查
05-NATIVE-MODULES.md第三方 C 模块(socket/cjson)通用约定;同用 cpath / 禁用 PluginImporter