捕获 Lua 异常 |
|
Lua 版本: 5.0 (版本说明)
本文介绍了一组补丁,用于在 C 和 C++ 中实现 Lua 异常处理。
在 Lua 脚本中生成的异常有两个可能的结局:
Lua 的 C API 提供了 `lua_pcall()` 函数来执行保护调用 [1]。
当程序“抛出”异常时,它是在执行一次“非局部跳转”。它跳转到代码中的一个点,这个点可能不在同一个代码块、同一个函数,甚至不在同一个模块中。Lua 手册将这些非局部跳转的目标称为“恢复点”。
由于 Lua 运行时是用 C 编写的,它无法使用 C++ 异常,因此 Lua 使用 C 函数 `setjmp()` 和 `longjmp()` 来实现此行为 [2]。
注意:从 Lua 5.1 开始,当编译为 C++ 时,Lua 使用原生的 C++ 异常而不是 `setjmp()` 和 `longjmp()`。
`setjmp()/longjmp()` 惯用法非常高效且易于使用。它被许多 C 库用于提供异常处理支持。然而,它并非没有问题。
`setjmp()` 将 CPU 执行环境(寄存器、堆栈指针等)的当前状态保存到一个缓冲区中。调用带有该缓冲区的 `longjmp()` 会恢复保存的执行环境。本质上,`longjmp()` 会“时光倒流”回到 `setjmp()` 调用处。程序将从该点继续执行,其执行环境看起来好像没有改变过。
jmp_buf jb;
char* mystr = "Original Value";
/* start */
int i_except = setjmp( jb );
if( 0 == i_except ) /* try */
{
mystr = "New Value";
/* Throw an exception.
Jumps back to "start", and setjmp() returns 33
*/
longjmp( jb, 33 );
}
else /* catch */
{
/* Handle the exception */
}
print( mystr );
许多编译器会通过将局部变量的值缓存在寄存器上来优化对局部变量的引用。当代码执行是顺序的时,这没问题。如果编译器没有足够的寄存器,它可以将值存储到内存中,稍后再加载。然而,调用 `longjmp()` 会 *恢复* `setjmp()` 保存的寄存器状态。如果编译器缓存了一个变量的值在寄存器中,并且在 `setjmp()` 之后但 `longjmp()` 之前其值发生了变化,那么当 `longjmp()` 恢复其旧值时,新值将会丢失。
在上面的示例中,变量 `mystr` 容易出现这个问题。根据编译器如何优化代码,当调用 `print()` 时,`mayvar` 可能包含“New Value”,也可能包含“Original Value”。
-- *实际上,上面示例中 setjmp 的调用上下文是不符合规范的,其结果行为是未定义的。下面的示例在这一点上是 OK 的。* -- Wim Couwenberg
这个问题可以通过谨慎的编程来缓解。解决方案是:
在上面的示例中,将 `mystr` 声明为 `volatile` 将阻止编译器将其值缓存在寄存器中。
char* volatile mystr;
声明一个 volatile 整数
volatile int myint;
请注意,函数参数必须包含在 `volatile` 变量集合中。
int func( int* volatile pInt ) /* pointer declared volatile */
{
jmp_buf jb;
if( 0 == setjmp( jb ) )
{
pInt++;
longjmp( jb, 97 );
}
else
{
}
}