Lua 和异常的黑客笔记

lua-users home
wiki

这是 2005 年一次黑客会议的记录,待主编。 -- ThomasLefort

当前算法

截至 5.1 版本,以 C++ 构建。

数据结构

“长跳转缓冲区”是一个易失状态的链表。
struct lua_longjmp {
  struct lua_longjmp *previous;
  int dummy;
  volatile int status;  
};
它的作用是跟踪受保护的调用堆栈。我们从一个空的全局缓冲区开始。

捕获错误

// chain a new local buffer, status = 0
// global buffer now points to it
try {
  // call
}
catch (...) {
  // force local status to non 0 (e.g. -1)
}
// restore global buffer
// return local status

抛出错误

// if empty global buffer then panic
// store status in global buffer
// throw its address !

Discussion

当前算法使得可以从 Lua 的另一侧抛出异常,但它们永远无法到达另一端,这是有原因的。

如果它们根本没有在 Lua 层面被捕获,那么稍后捕获它们将导致 Lua 调用堆栈的暴力展开,使我们处于一个不一致的 Lua 状态。这正是我调整 Lua 5.0.2 时发生的情况。

为了确保 Lua 状态的一致性,同时将异常传递到另一端,我们需要将其副本存储在一个安全的地方,正常展开到顶层,最后抛出副本。然而,由于 catch 会匹配类型(Stroustrup 14.3),我们在 `catch (...)` 时会丢失类型信息,从而失去复制通用异常的能力。

部分解决方案

要实现上述复制机制,我们可以将外部异常的类型范围缩小到标准的 `std::exception`。如果程序员需要捕获其他异常,他可以将其包装到 `std::exception` 的子类中。其余的异常仍将被静默忽略。

此解决方案的优点是简单,并且与分层异常类(每个根类一个包装器)配合良好。然而,它仅限于双方能够就 `std::exception` 包装达成一致的情况。

注意:这意味着需要为此方案调整包装器生成器。

作为临时解决方案,也可以将异常空间缩小到一组自定义异常,即在 Lua 构建时手动完成。

待办事项:检查 `std::exception` 的副本

替代方案:扩展 tolua++

上述解决方案并非真正完善。如果我们使用像 [tolua++] 这样的自由软件绑定生成器,我们可以扩展它,从而保护 Lua 免受绑定 C++ 代码抛出的异常影响。这样做的好处是使 Lua 的源代码保持整洁。

回想起来,这似乎是我们自身情况的合适解决方案,对于偶尔使用的应用程序也应该足够了。然而,这需要不同错误系统之间的翻译,因此存在冗余。

如果 Lua 源代码变得(或被分叉)异常安全,我们可以使其能够捕获 Lua 中的 C++ 异常,无论是显式的还是通过绑定到最终的 Lua 异常方案。下一节概述了一个解决方案。

完整解决方案

大纲

catch (struct lua_longjump * p_lj) {
  // force local status to non 0
}
catch (...) {
  // do soft unwinding
  throw
}

注意:`p_lj` 应该指向当前的 `lj`。

待办事项:断言这一点。

这应该可以保证我们不会吞掉外部异常,除非有人出于恶意目的在任何地方都从其库中抛出 `(struct lua_longjump)*`。

软展开

为了设计我们的算法,我们需要识别 `pcall` 获取和释放的关键资源。

pcall 堆栈大纲

当 pcall 嵌套时,顶层和底层会进行包装。

D_pcall 清理

if (status != 0) {  /* an error occurred? */
  StkId oldtop = restorestack(L, old_top);
  luaF_close(L, oldtop);  /* close eventual pending closures */
  luaD_seterrorobj(L, status, oldtop);
  L->nCcalls = oldnCcalls;
  L->ci = restoreci(L, old_ci);
  L->base = L->ci->base;
  L->savedpc = L->ci->savedpc;
  L->allowhook = old_allowhooks;
  restore_stack_limit(L);
}
L->errfunc = old_errfunc;

它恢复之前获取的状态

// ptrdiff_t old_top, ptrdiff_t ef
unsigned short oldnCcalls = L->nCcalls;
ptrdiff_t old_ci = saveci(L, L->ci);
lu_byte old_allowhooks = L->allowhook;
ptrdiff_t old_errfunc = L->errfunc;
L->errfunc = ef;

现在,我们可以使用“**资源获取即初始化**”模式,在对象销毁时自动执行清理代码。这是与异常集成的自然方式(Stroustrup 14.4.1)。

编码路线图

最终说明

Stroustrup 14.9 是基础。


RecentChanges · preferences
编辑 · 历史
最后编辑于 2007 年 10 月 26 日下午 7:48 GMT (diff)