lua-源码带读-07-错误处理与调试系统
Lua 5.4 源码带读 第7篇:错误处理与调试系统
本篇目标
理解 Lua 的错误处理机制(错误产生、恢复、传播)和调试系统(调试库API、hook机制)。
前置知识
- 了解setjmp/longjmp的概念
- 阅读过第1-4篇
1. 错误处理概述
1.1 错误产生方式
-- 1. 调用 error() 显式抛出
error("something went wrong")
-- 2. 语言错误隐式产生(类型错误、索引越界等)
local t = {}
t + 1 -- 错误:attempt to perform arithmetic on a table value
-- 3. 内存分配失败(极少发生)
1.2 错误恢复
-- pcall - 受保护调用(protected call)
local ok, err = pcall(function()
error("error message")
end)
if not ok then
print("Caught error: " .. err)
end
-- xpcall - 带错误处理函数的受保护调用
xpcall(
function() error("error") end,
function(err) return debug.traceback(err) end
)
2. 错误处理实现:ldo.c
2.1 lua_error
// ldo.c
int luaG_errormsg(lua_State *L) {
if (L->errfunc != 0) {
// 有错误处理函数,调用它
StkId errfunc = restorestack(L, L->errfunc);
TValue func;
setobj(L, &func, errfunc);
// 调用错误处理函数
luaD_callnoyield(L, &func, 1);
}
luaD_throw(L, LUA_ERRRUN);
}
// ldo.c
void luaD_throw(lua_State *L, int errcode) {
if (L->errorJmp) {
// 有恢复点,跳转回去
L->errorJmp->status = errcode;
LUAI_THROW(L, L->errorJmp);
} else {
// 没有恢复点,终止程序
L->status = cast_byte(errcode);
if (G(L)->panic) {
resetstack(L);
G(L)->panic(L);
}
abort();
}
}
2.2 lua_pcall
// ldo.c
int luaD_pcall(lua_State *L, Pfunc func, void *u,
ptrdiff_t old_top, ptrdiff_t ef) {
CallInfo old_ci = L->ci->base_ci;
StkId oldtop = restorestack(L, old_top);
// ...
// 设置恢复点
struct lua_longjmp cJmp;
cJmp.previous = L->errorJmp;
cJmp.status = LUA_OK;
L->errorJmp = &cJmp;
LUAI_TRY(L, &cJmp,
(*func)(L, u); // 执行函数
);
L->errorJmp = cJmp.previous;
// ...
return cJmp.status;
}
2.3 恢复点机制
// ldo.h
typedef struct lua_longjmp {
struct lua_longjmp *previous; // 前一个恢复点
LUAI_CONTEXT ctx; // setjmp上下文
int status; // 错误代码
} lua_longjmp;
// 设置恢复点的宏
#define LUAI_TRY(L, c, a) \
if (setjmp((c)->ctx) == 0) { a }
2.4 错误传播
函数A调用函数B,函数B调用函数C
函数C调用 error()
→ 检查是否有pcall包裹
→ 没有:终止程序
→ 有:longjmp跳回到pcall处
→ pcall返回 false, error_message
3. 调试系统
3.1 调试库API
-- debug库
debug.getinfo(func) -- 获取函数信息
debug.getlocal(func, n) -- 获取局部变量
debug.setlocal(func, n, val) -- 设置局部变量
debug.getupvalue(func, n) -- 获取上值
debug.setupvalue(func, n, val) -- 设置上值
debug.getmetatable(obj) -- 获取元表
debug.setmetatable(obj, mt) -- 设置元表
debug.getuservalue(ud) -- 获取userdata值
debug.setuservalue(ud, v) -- 设置userdata值
debug.traceback(level) -- 获取调用栈
debug.sethook(func, mask) -- 设置调试hook
debug.gethook() -- 获取当前hook
3.2 debug.getinfo 实现
// ldblib.c
static int db_getinfo(lua_State *L) {
lua_Debug ar;
int funcindex = 1;
if (!lua_getinfo(L, what, &ar))
return luaL_error(L, ...);
lua_newtable(L); // 创建结果表
// 填充字段:name, what, source, linedefined, etc.
// ...
return 1;
}
3.3 lua_Debug 结构体
// lua.h
typedef struct lua_Debug {
int event; // 事件类型
const char *name; // 函数名
const char *namewhat; // 名称类型("global", "local", "field", "method")
const char *what; // 函数来源("Lua", "C", "main", "tail")
const char *source; // 源文件名
int currentline; // 当前行号
int linedefined; // 定义行号
int lastlinedefined; // 最后行号
unsigned char nparams; // 参数数量
unsigned char isvararg; // 是否可变参数
unsigned char istailcall; // 是否尾调用
short nups; // 上值数量
short nfreevars; // 自由变量数量
short ntransfer; // 调试传输
struct Upvaldesc *upvalues; // 上值信息
struct LocVar *locvars; // 局部变量信息
} lua_Debug;
3.4 debug.traceback 实现
// ldblib.c
static int db_traceback(lua_State *L) {
int level = (int)luaL_optinteger(L, 1, 1);
luaL_Buffer b;
luaL_buffinit(L, &b);
lua_traceback(L, level, &b);
luaL_pushresult(&b);
return 1;
}
4. Hook机制
4.1 Hook类型
// lua.h
#define LUA_MASKCALL (1 << 0) // 函数调用
#define LUA_MASKRET (1 << 1) // 函数返回
#define LUA_MASKLINE (1 << 2) // 每行执行
4.2 设置Hook
-- 每行执行
debug.sethook(function(event, line)
print(event, line)
end, "l")
-- 函数调用和返回
debug.sethook(function(event, line)
if event == "call" then
print("Entering function")
elseif event == "return" then
print("Leaving function")
end
end, "cr")
4.3 Hook实现
// ldblib.c
static int db_sethook(lua_State *L) {
int mask = luaL_checkinteger(L, 2);
lua_sethook(L, hookf, mask, 0);
return 0;
}
// ldo.c - 在指令执行前检查hook
void luaD_hook(lua_State *L, int event, int line) {
lua_Hook hook = L->hook;
if (hook && L->allowhook) {
// 调用hook函数
luaD_callnoyield(L, &func, 0);
}
}
5. 错误处理最佳实践
-- 使用 xpcall 获取堆栈信息
local ok, err = xpcall(function()
-- 可能出错的代码
end, function(e)
return debug.traceback(e, 2)
end)
-- 使用 error 的 level 参数控制错误位置
error("error", 2) -- 报告调用者的行号,而非error()的行号
-- 使用 assert 快速失败
assert(type(x) == "number", "x must be a number")
6. 本篇小结
| 概念 | 要点 |
|---|---|
| lua_error | 抛出错误,longjmp跳转 |
| lua_pcall | 受保护调用,设置恢复点 |
| 恢复点 | lua_longjmp链表,支持嵌套 |
| 调试API | getinfo, getlocal, traceback等 |
| Hook | call/return/line三种事件 |
| 错误传播 | 错误沿调用栈向上传播直到pcall |
思考题
lua_error和lua_pcall为什么用 longjmp 而不是返回错误码?xpcall的错误处理函数如何获取堆栈信息?- Hook机制如何影响程序性能?
__close变量(5.4新增)和pcall在错误处理上有什么区别?- 如何用调试库实现一个简单的断点功能?
思考题解答
1. 为什么用longjmp而不是返回错误码?
longjmp的优势:
- 不污染API:不需要每个函数都返回错误码,代码更简洁
- 自动传播:错误自动沿调用栈向上传播,不需要每层都检查
- 无法忽略:返回错误码可能被程序员忽略,longjmp会终止当前执行流
- 性能:正常路径没有错误检查开销(
if (err) return err) - 与C标准库一致:
setjmp/longjmp是C标准库的一部分
// 如果用错误码,代码会很繁琐:
int result = functionA();
if (result != OK) return result;
result = functionB();
if (result != OK) return result;
result = functionC();
if (result != OK) return result;
// 用longjmp:
functionA(); // 出错自动跳到pcall处
functionB();
functionC();
代价:longjmp不能释放局部变量(栈回滚不自动执行析构函数),需要额外的清理机制(如Lua的 __gc 和 __close)。
2. xpcall的错误处理函数如何获取堆栈信息?
xpcall(
function() error("error") end,
function(err)
-- err 是错误消息
-- 通过 debug.traceback 获取堆栈信息
return debug.traceback(err, 2) -- 2表示跳过traceback和xpcall本身
end
)
实现原理:
// ldo.c
int luaD_pcall(...) {
// 设置恢复点
// 执行函数
// 如果出错:
// 调用错误处理函数(第二个参数)
// 错误处理函数在错误的上下文中执行
// 可以通过 debug.getinfo 访问调用栈
}
错误处理函数在错误发生时执行,此时调用栈仍然完整,可以使用 debug.traceback、debug.getinfo 等API获取堆栈信息。
3. Hook机制如何影响程序性能?
性能影响:
| Hook类型 | 开销来源 | 影响程度 |
|---|---|---|
call hook |
每次函数调用时检查 | 中等 |
return hook |
每次函数返回时检查 | 中等 |
line hook |
每行执行时检查 | 高 |
实现方式:
// ldo.c
void luaD_hook(lua_State *L, int event, int line) {
lua_Hook hook = L->hook;
if (hook && L->allowhook) {
// 每次检查都涉及:函数指针检查 + 条件分支
luaD_callnoyield(L, &func, 0);
}
}
优化策略:
L->allowhook标志可以临时禁用hook(防止hook中的递归hook)- 使用位掩码只检查启用的事件类型
linehook 的检查频率最高,建议只在调试时启用
量化:没有hook时,VM循环约2-3条指令/操作码;有line hook时,每行增加约5-10条指令。对于计算密集型代码,性能下降可达50%以上。
4. __close 变量和 pcall 在错误处理上的区别
| 特性 | __close (to-be-closed) |
pcall |
|---|---|---|
| 资源清理 | 自动(作用域结束时) | 手动(在错误处理函数中) |
| 错误恢复 | 不恢复(错误继续传播) | 恢复(返回错误信息) |
| 可预测性 | 高(确定性清理) | 低(需要手动处理) |
| 使用场景 | RAII/defer模式 | 错误恢复、异常处理 |
-- __close: 确定性清理,不恢复错误
local fd <close> = io.open("file.txt")
-- 无论是否出错,离开作用域时自动关闭
-- pcall: 错误恢复
local ok, err = pcall(function()
-- 可能出错的代码
end)
if not ok then
-- 处理错误,恢复执行
end
最佳实践:优先使用 __close 进行资源清理(文件、锁、连接等),使用 pcall 进行错误恢复。
5. 如何用调试库实现断点功能?
local breakpoints = {}
function set_breakpoint(file, line)
breakpoints[file] = breakpoints[file] or {}
breakpoints[file][line] = true
end
debug.sethook(function(event, line)
local info = debug.getinfo(2, "S")
if info and info.source then
local file = info.source:match("@(.+)")
if file and breakpoints[file] and breakpoints[file][line] then
print("Breakpoint hit: " .. file .. ":" .. line)
-- 可以在这里查看变量、单步执行等
-- 打印局部变量
local i = 1
while true do
local name, value = debug.getlocal(2, i)
if not name then break end
print(name, value)
i = i + 1
end
-- 暂停执行(等待用户输入)
end
end
end, "l") -- "l" = line hook
-- 使用
set_breakpoint("main.lua", 10)
原理:通过line hook监控每行执行,检查是否在断点位置。如果是,可以查看变量状态、控制执行流程。
局限性:
- line hook的性能开销较大
- 无法设置条件断点(需要在hook函数中实现)
- 无法修改变量值(需要
debug.setlocal) - 无法单步执行(需要更复杂的hook控制)

浙公网安备 33010602011771号