- 游戏开发
- 图形学
【免费下载链接】Piccolo
Piccolo (formerly Pilot) – mini game engine for games104
本指南以 sol2-3.3.0 官方文档《errors》为核心骨架,系统讲解在 Piccolo 引擎(内置 sol2 与 Lua 5.4)中如何检测脚本运行错误、处置 panic 崩溃、规避 C++/Lua 混合编程下的编译与链接陷阱,并给出可直接复制的代码与编译参数。读完你将掌握
safe_script家族 API、script_default_on_error/script_pass_on_error两个内置错误处理器、SOL_USING_CXX_LUA与SOL_EXCEPTIONS_SAFE_PROPAGATION等关键宏的使用边界,以及一套从"报错"到"定位"的完整排查方法论。
一、sol2 在 Piccolo 引擎中的实际角色
Piccolo 引擎(原 Pilot)在engine/3rdparty/目录下以源码形式内置了 Lua 5.4.4、sol2-3.3.0 以及 sol2 所需的绑定代码,并封装出一个 Lua 组件供游戏对象使用。在 lua_component.cpp 中可以看到典型的 sol2 集成方式:
// 打开 base 库,向 Lua 暴露 C++ 函数与对象 m_lua_state.open_libraries(sol::lib::base); m_lua_state.set_function("set_float", &LuaComponent::set<float>); m_lua_state.set_function("get_bool", &LuaComponent::get<bool>); m_lua_state.set_function("invoke", &LuaComponent::invoke); m_lua_state["GameObject"] = m_parent_object; // 每帧执行 Lua 脚本 m_lua_state.script(m_lua_script);其中m_lua_state的类型是 sol::state(见 lua_component.h)。这里直接调用了script,而官方文档 errors.rst 正是围绕"脚本可能出错"这一现实展开的:脚本可能语法错误、可能从文件系统加载失败、可能运行期抛错,如果不做区分和处理,轻则静默失败,重则直接拖垮整个 Lua VM。下面逐一展开。
二、脚本运行错误的三层排查:safe_script 与内置错误处理器
2.1 为什么会分 safe / unsafe 两套
sol2 在 state_view.hpp 中为sol::state/sol::state_view提供了两组脚本执行接口:
- safe 系:
safe_script、safe_script_file。加载并执行脚本,出错时走错误回调,返回sol::protected_function_result,可检查valid()。 - unsafe 系:
unsafe_script、unsafe_script_file。出错时直接调用lua_error(进而触发 panic),不做任何检查,性能最快但最危险。
默认的script/script_file在定义SOL_SAFE_FUNCTION_OBJECTS(或SOL_ALL_SAFETIES_ON)时等价于 safe 版本,否则等价于 unsafe 版本。官方文档给出的建议很明确:只要不是刻意追求极致速度,请优先使用safe_script系列。
2.2 内置错误处理器:default / pass / throw
safe_script的最后一个参数是"仅当出错时才被调用"的错误回调。sol2 预置了两个默认处理器(定义见 state_handling.hpp):
sol::script_pass_on_error(lua_State*, protected_function_result):原样透传protected_function_result,不抛异常、不触发 panic,把错误判断完全交给调用方;sol::script_default_on_error(lua_State*, protected_function_result):默认处理器。在未定义SOL_DEFAULT_PASS_ON_ERROR时实际转发给script_throw_on_error——它会拼装一条包含call_status、异常类型与what()文本的错误信息,然后抛出sol::error;若定义了SOL_DEFAULT_PASS_ON_ERROR则退化为 pass 行为(官方在 safety.rst 中强烈警告该宏"非常危险",除非有极其充分的理由否则不要开启)。
2.3 三种错误处理方式完整对照
官方示例 script_error_handling.cpp 用同一段坏脚本(bad&$#*$syntax)演示了三种策略:
方式一:default_on_error + try/catch(异常可用时)
try { int value = lua.script(code, sol::script_default_on_error); // 脚本错误,这里永远不会到达 std::cout << value << std::endl; } catch (const sol::error& err) { std::cout << "Something went horribly wrong: thrown error" << "\n\t" << err.what() << std::endl; }方式二:pass_on_error + 结果检查(不依赖异常)
sol::protected_function_result result = lua.script(code, sol::script_pass_on_error); if (!result.valid()) { sol::error err = result; sol::call_status status = result.status(); std::cout << sol::to_string(status) << " error" << "\n\t" << err.what() << std::endl; }方式三:load + 分步执行(最低层、最显式)
sol::load_result loaded_chunk = lua.load(code); if (!loaded_chunk.valid()) { // 语法错误在此暴露 sol::error err = loaded_chunk; sol::load_status status = loaded_chunk.status(); std::cout << "loading error: " << sol::to_string(status) << "\n\t" << err.what() << std::endl; } else { sol::protected_function script_func = loaded_chunk.get<sol::protected_function>(); sol::protected_function_result result = script_func(); if (!result.valid()) { // 运行期错误在此暴露(nil 访问、内存错误等) sol::error err = result; sol::call_status status = result.status(); std::cout << "running error: " << sol::to_string(status) << "\n\t" << err.what() << std::endl; } }2.4 自定义错误回调:只会在出错时被调用
示例 state_script_safe.cpp 展示了自定义回调的写法。回调收到protected_function_result,你可以自由处理(甚至再次抛出),但若选择不抛出,就必须把结果return回去:
auto result2 = lua.safe_script("123 bad.code", [](lua_State*, sol::protected_function_result pfr) { sol::error err = pfr; std::cout << "An expected error occurred: " << err.what() << std::endl; // 不抛异常就必须返回结果 return pfr; });底层实现(state_view.hpp)也很直白:safe_script先调用do_string/do_reader/do_file加载执行,若结果!pfr.valid()才调用on_error(L, std::move(pfr)),否则原样返回。这保证了回调只在真正出错时触发,正常情况下没有任何额外开销。
三、编译器错误与警告:一份可对照的排查清单
sol2 是重度模板库,错误信息往往又长又吓人。官方文档给出了 8 条实战经验,逐一展开如下:
3.1 先检查 C++ 标准开关
如果报错堆里满是std::index_sequence、type traits 等std::成员相关错误,十有八九是没有开启 C++14。Visual Studio 2015 起默认开启,但 g++ / clang++ 需要显式指定:
--std=c++14 # 或早期编译器使用 --std=c++1y3.2 非基本类型入栈报出 initializer list / luaL_Reg 错误
向 Lua 推送非基本类型时若出现奇怪的 initializer list 或无法初始化luaL_Reg的错误,多半与 sol2 的自动注册机制(automagical registration,为 usertype 自动生成常见运算符与成员访问方法)有关。可尝试通过#define SOL_DEFAULT_AUTOMAGICAL_USERTYPES 0关闭默认的 automagical 行为(相关开关定义见 version.hpp),关闭后自行显式注册需要的成员。
3.3 大量成员绑定导致的调试符号截断警告
当单个 usertype 绑定了大量成员函数时,生成的代码可能触发"debug symbols cut off"或__LINE_VAR超长等警告。这类警告在多数编译器下可以安全屏蔽(例如 MSVC 的/wd系列或对应 warning level 调整)。
3.4 模板深度错误
早期 clang++ / g++ 上常出现模板实例化深度超限。用-ftemplate-depth把深度提到足够高即可:
-ftemplate-depth=2048 # 甚至更高3.5 MSVC C1128:需要 /bigobj
大量 usertype 模板实例化会让 MSVC 触发compiler error C1128(目标文件段数超限),解决办法是加编译标志/bigobj。
3.6 move-only 类型必须 readonly
如果绑定的是 move-only 类型(如std::unique_ptr成员),当它作为 usertype 的成员变量、或用state_view::set_function绑定时,需要包一层sol::readonly。示例见 usertype_advanced.cpp:
player_type.set("bullets", sol::readonly(&player::bullets));此时 Lua 侧只能读不能写,写入会报错。sol::readonly的详细说明见 readonly.rst。
3.7 std::string / std::pair 的 operator= 赋值错误
std::string或std::pair<T1, T2>在构造后用operator=赋值,在与sol::function及其返回值交互时可能产生编译错误(官方在 issue 中给出的修复方式是对这类返回值显式取类型或改用显式构造,避免隐式 operator= 路径)。
3.8 __stdcall(x86)与 /std:c++latest 的坑
- 在 32 位 (x86) 环境下,VC++ 的编译器 bug 可能导致
__stdcall函数绑定失败。sol2 已带初步修复;若仍有问题,把函数塞进std::function再绑定通常能绕开。 - 若使用
/std:c++latest并出现大量针对函数noexcept说明符的报错,说明当前 VC++ 编译器版本对最新标准支持尚不完善,需要等待编译器更新或向编译器厂商反馈。
四、Mac OSX 下 LuaJIT 的随机崩溃
在 Mac OSX 上使用 LuaJIT 时,代码可能在看似随机的位置崩溃。官方建议按 LuaJIT 官网说明在构建中加入如下链接标志:
-pagezero_size 10000 -image_base 100000000这两个标志能让 LuaJIT 的 JIT 代码在 macOS 的地址空间布局下正常运行,避免任意崩溃。注意:本项目默认内置的是 PUC-RIO Lua 5.4.4(见 lua-5.4.4 目录),此条主要面向自行替换为 LuaJIT 的场景。
五、"compiler out of heap space":让 VS 编译器使用 x64 工具链
这是 Visual Studio 的典型问题:VS 默认的编译器本身是 32 位可执行程序(它照样能产出 x86 / x64 / ARM 二进制),因此宿主进程的堆空间受限。按官方指引(源自社区 issue 中 OrfeasZ 的修复方案),在.vcxproj的<Import .../>语句之后加入:
<PropertyGroup> <PreferredToolArchitecture>x64</PreferredToolArchitecture> </PropertyGroup>让编译器默认使用 64 位工具,堆空间上限随之提升到 64 位 Windows 机器可承载的水平。如果内存仍不足(例如机器不足 4GB RAM),就要考虑拆分 usertype:
- 把一个大 usertype 分散到多个 C++ 文件;
- 不要在单个
new_usertype(...)调用里塞全部函数,改用下标赋值逐个注册:
auto my_usertype = lua.new_usertype<my_class>("my_class"); my_usertype["talkin"] = &about; my_usertype["this"] = &my_class::that; my_usertype["and_the"] = sol::property(&my_class::third);逐条my_usertype["func"] = func;的方式能显著降低单次模板实例化的压力,是官方文档明确推荐的写法。
六、链接错误:C 与 C++ 调用约定之争
6.1 extern "C" 的默认假设
sol2 假定 Lua 库按 C 约定编译,因此在其包含代码里用extern "C"包裹了 Lua 头文件(静态/动态链接的 Lua 通常都导出 C 符号)。但如果目标 Lua 库是用 C++ 编译的,就必须去掉extern "C"块、切换到 C++ 的名称修饰(name mangling),否则链接阶段找不到符号。
6.2 SOL_USING_CXX_LUA / SOL_USING_CXX_LUA_JIT
在包含sol/sol.hpp之前(或加入编译命令行)定义:
#define SOL_USING_CXX_LUA即可告知 sol2:Lua 是 C++ 编译的。若用的是以 C++ 模式构建的 LuaJIT,还需额外定义SOL_USING_CXX_LUAJIT(官方原话:基本不会有人这么干,通常不需要)。标准构建的 Lua / LuaJIT 都不应该定义这些宏,否则反而会破坏 C 链接假设。
这两个宏的实际效果记录在 safety.rst 的 Linker Config 一节:取消extern "C"包裹、自动打开SOL_EXCEPTIONS_SAFE_PROPAGATION等。
七、"caught (...) exception":异常被吞掉的问题
有时你期望捕获到精心构造的错误,实际却只收到一个"catch(...)"级别的通配异常。这通常意味着:
- 你把 Lua 构建成了 C++;或
- 你正在使用对 C++ 异常有完整互操作支持的框架(LuaJIT 在特定平台:LuaJIT 2.0.5 的 x64,LuaJIT 2.1.x-beta 及之后的 x86 与 x64)。
此时请在包含 sol2 前定义:
#define SOL_EXCEPTIONS_SAFE_PROPAGATION该宏的语义(见 safety.rst):不再在 C 函数 trampoline 与safe_/protected_function内部使用 try/catch 包裹,让 C++ 异常直接穿透 Lua API 传播回来,从而在 C++ 侧用catch (...)捕获到真正的异常。注意 exceptions.rst 中的警告:当检测到 LuaJIT 时 sol2 不会自动定义该宏,必须由你确认平台支持后自行定义。
实现层面,该宏最终映射为SOL_PROPAGATE_EXCEPTIONS(见 lua_version.hpp),并在两处改变行为:函数 trampoline(trampoline.hpp)与protected_function的调用核心(protected_function.hpp)——后者在开启时跳过try/catch分支,让异常原样上抛。
八、panic 与崩溃:捕获错误不等于一切安好
8.1 default_at_panic 做了什么
sol2 在创建状态时会自动安装default_at_panic处理器(见 state_handling.hpp):异常未关闭时,它取出栈顶错误消息,若定义了SOL_PRINT_ERRORS会打印到std::cerr,然后throw sol::error(err)给 C++ 侧一个恢复机会。完整的状态初始化流程封装在set_default_state中(state_handling.hpp),它会依次设置 panic 函数、默认 traceback 处理器、异常处理器并注册主线程。
8.2 为什么官方强烈建议"捕获 + 崩溃"
关键警告来自 errors.rst 的Catch and CRASH!一节:Lua 调用lua_atpanic意味着 VM 已经处于不可预测甚至死亡的状态。Lua 本质上是 C API,异常从它里面"冒出来"属于最后的、终止性的行为。在默认处理器抛出的异常上做try { ... } catch (...) {}后继续运行,等于在一个状态不明的 VM 上继续执行——release/优化模式下会出现难以排查的诡异 bug。
官方给出的推荐流程是:记录日志 → 尽快终止当前 Lua VM → 若应用无法新建一个 Lua state 则直接崩溃。set_panic的签名在 state.rst 中有说明:它是void set_panic(lua_CFunction panic),必须匹配int(lua_State*)签名。
8.3 自定义 panic 函数示例
若你不想让默认处理器抛异常(例如异常被关闭时),可用sol::state lua(my_panic_function)构造、lua_atpanic(L, fn)或state_view::set_panic自行注册。官方示例 my_panic.cpp:
inline void my_panic(sol::optional<std::string> maybe_msg) { std::cerr << "Lua is in a panic state and will now abort() the application" << std::endl; if (maybe_msg) { std::cerr << "\terror message: " << maybe_msg.value() << std::endl; } // 函数返回后 Lua 将按默认行为 abort() } int main() { sol::state lua(sol::c_call<decltype(&my_panic), &my_panic>); // 等价写法: // lua_atpanic(L, sol::c_call<decltype(&my_panic), &my_panic>); // lua.set_panic(sol::c_call<decltype(&my_panic), &my_panic>); return 0; }九、析构函数与安全:为什么建议把 Lua 构建为 C++
Lua 是 C API,出错时依赖setjmp/longjmp跳出代码。longjmp不会触发 C++ 析构函数——如果你错误地使用了库或底层 Lua VM,栈上的 RAII 对象可能被直接跳过,造成资源泄漏与未定义行为。
官方建议:把 Lua 构建为 C++。此时lua_error以 C++ 异常的形式上抛,触发正常的栈展开(stack unwinding),析构函数得以按标准语义执行。这与第七节SOL_USING_CXX_LUA的设定是配套的——用 C++ 构建 Lua 并定义该宏,既解决链接约定,又获得正确的异常展开语义。
十、受保护函数与安全取值:日常开发的默认姿势
10.1 默认 API 的"乐观假设"
errors.rst 明确指出:默认情况下sol::function假设代码运行完美无缺,sol::state(_view)::script(_file)同样如此——它们追求简单与速度,出错时以抛异常"暴力"终止 VM。因此在生产代码里应改用:
sol::protected_function:调用结果可检查(valid()/status()/ 错误消息);sol::optional<T>:从 Lua 安全取值,取不到时为nullopt;do_string/do_file/load/load_file:安全加载并拿到结果。
10.2 protected_function 实战
示例 protected_functions.cpp 展示了带错误处理函数的受保护调用:
lua.script(R"( function handler (message) return "Handled this message: " .. message end function f (a) if a < 0 then error("negative number detected") end return a + 5 end )"); sol::protected_function f(lua["f"], lua["handler"]); sol::protected_function_result result = f(-500); if (result.valid()) { int x = result; std::cout << "call succeeded, result is " << x << std::endl; } else { sol::error err = result; std::cout << "call failed, what(): " << err.what() << std::endl; // what() 将得到 "Handled this message: negative number detected" }其实现核心是lua_pcall(见 protected_function.hpp),并针对const char*、std::string、std::exception等类型做分类捕获,把 C++ 函数里抛出的异常转成 Lua 错误再传回结果。
10.3 exception_handler:捕获从 C++ 函数泄漏的异常
sol2 还提供sol::exception_handler_function:当绑定到 Lua 的 C++ 函数抛出异常并冒泡出来时,被注册的处理器会被调用,且必须向栈上压入 1 个元素(通常是一个字符串,用于lua_error)。官方示例 exception_handler.cpp:
int my_exception_handler(lua_State* L, sol::optional<const std::exception&> maybe_exception, sol::string_view description) { // 打印异常信息…… // 必须压入 1 个元素作为 Lua 侧的错误对象 return sol::stack::push(L, description); } void will_throw() { throw std::runtime_error("oh no not an exception!!!"); } int main() { sol::state lua; lua.open_libraries(sol::lib::base); lua.set_exception_handler(&my_exception_handler); lua.set_function("will_throw", &will_throw); sol::protected_function_result pfr = lua.safe_script( "will_throw()", &sol::script_pass_on_error); sol_c_assert(!pfr.valid()); sol::error err = pfr; std::cout << err.what() << std::endl; }十一、受保护函数不是万能:加载失败的边界情况
sol::protected_function只能保护"函数调用"环节。如果问题出在文件加载本身——例如脚本文件路径错误、文件内容无法解析、执行前的加载步骤就失败——那么单纯包一层protected_function救不了你。
正确的兜底姿势是:
- 注册自己的 panic 处理器,捕获到达 panic 的错误;
- 或遵循第八节的"捕获 + 崩溃"策略;
- 或在极端场景下放弃 sol2 的内置保护,直接绑定一个裸
lua_CFunction,完全自己掌控错误流。
十二、表迭代的注意点
对 Lua 表做迭代时有两个常见陷阱:
- 表上可能挂着其他"杂物"(非数字键、
__pairs等),导致for-each风格的循环或 sol 的for_each在处理数字段时行为奇怪。此时应改用显式的数值循环(for i = 1, #t)来遍历数字段。 - 迭代本身没有任何定义好的顺序。依赖顺序的代码必须显式处理(排序或改用数组语义),官方在 table.rst 中对此有专门说明。
十三、本指南相关文件索引
| 主题 | 仓库路径 |
|---|---|
| 错误处理官方文档 | errors.rst |
| 异常专题文档 | exceptions.rst |
| 配置与安全宏文档 | safety.rst |
| state 自动处理器说明 | api/state.rst |
| 内置错误处理器实现 | state_handling.hpp |
| safe/unsafe 脚本实现 | state_view.hpp |
| 受保护函数实现 | protected_function.hpp |
| 异常传播宏解析 | lua_version.hpp |
| 官方错误处理示例 | script_error_handling.cpp |
| safe_script 示例 | state_script_safe.cpp |
| panic 函数示例 | my_panic.cpp |
| 异常处理器示例 | exception_handler.cpp |
| Piccolo 引擎集成点 | lua_component.cpp |
总结:sol2 的错误处理体系可以概括为三条主线——脚本层用safe_script+ 内置/自定义错误回调兜底;调用层用sol::protected_function检查每次调用的成败;VM 层用 panic 处理器 + 异常传播宏控制崩溃边界。而编译/链接期的各类陷阱,绝大多数都能通过正确设置 C++ 标准、SOL_USING_CXX_LUA、SOL_EXCEPTIONS_SAFE_PROPAGATION及拆分 usertype 等针对性手段化解。建议在接入 Piccolo 引擎的 Lua 组件时,把m_lua_state.script(m_lua_script)换成带错误检查的safe_script变体,让每帧脚本执行的结果可观测、可诊断。
- 游戏开发
- 图形学
【免费下载链接】Piccolo
Piccolo (formerly Pilot) – mini game engine for games104
相关推荐
sol2 错误处理与异常排查完全指南:脚本安全执行、编译/链接错误、panic 与崩溃防护
sol2 错误处理与异常排查完全指南:脚本安全执行、编译/链接错误、panic 与崩溃防护 导读 sol2(sol v3.0)是 C++ 与 Lua 之间的高性
开发工具ta-lib-python异常处理最佳实践:从编译到运行时错误
ta lib python异常处理最佳实践:从编译到运行时错误 一、编译安装阶段异常处理 1.1 平台支持性检查 ta lib python在编译前会进行平台兼
金融科技数据分析Hyperf Exception Handler 实战指南:异常处理链、传播控制、Whoops 集成与错误监听器
Hyperf Exception Handler 实战指南:异常处理链、传播控制、Whoops 集成与错误监听器 本文以 Hyperf 协程框架内置的 hype
后端微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考