C 语言没有原生支持异常处理,但是可以使用setjmp
和longjmp
函数实现类似try ... except
的功能。
本文主要参考:
-
C Interfaces and Implementation
setjmp.h
setjmp.h
是 C 标准函数库中提供“非本地跳转”的头文件:控制流偏离了通常的子程序调用与返回序列。互补的两个函数setjmp
与longjmp
提供了这种功能。
setjmp
和longjmp
的典型用途是异常处理机制的实现:利用longjmp
恢复程序或线程的状态,甚至可以跳过栈中多层的函数调用。
重要函数
-
int setjmp(jmp_buf env)
:建立本地的jmp_buf
缓冲区并且初始化,用于将来跳转回此处。这个子程序保存程序的调用环境于env
参数所指的缓冲区,env
将被longjmp
使用。如果是从setjmp
直接调用返回,setjmp
返回值为 0 。如果是从longjmp
恢复的程序调用环境返回,setjmp
返回非零值。 -
void longjmp(jmp_buf env, int value)
:恢复env
所指的缓冲区中的程序调用环境上下文,env
所指缓冲区的内容是由setjmp
子程序调用所保存。value
的值从longjmp
传递给setjmp
。longjmp
完成后,程序从对应的setjmp
调用处继续执行,如同setjmp
调用刚刚完成。如果value
传递给longjmp
零值,setjmp
的返回值为 1 ;否则,setjmp
的返回值为value
。
重要类型
jmp_buf
:用于保存恢复调用环境所需要的信息。
浅探异常处理
下面一段代码给出了使用setjmp
和longjmp
处理异常的基本思路:
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <assert.h>
int Allocation_handled = 0;
jmp_buf Allocate_Failed;
void *allocate(unsigned n)
{
void *new = malloc(n);
if (new)
return new;
if (Allocation_handled)
longjmp(Allocate_Failed, 1);
assert(0);
}
int main(void)
{
char *buf;
Allocation_handled = 1;
if (setjmp(Allocate_Failed)) {
fprintf(stderr, "couldn't allocate the buffer\n");
exit(1);
}
buf = allocate(4096);
Allocation_handled = 0;
return 0;
}
在上面的程序中,第一次调用setjmp
时函数返回 0 ,因此不进入if
后的分支而是继续执行后续操作,在调用函数allocate
时,如果内存分配失败,将会调用longjmp
函数,这会导致setjmp
函数第二次返回,此时的返回值是longjmp
函数第二个参数 1 。
因此我们可以这样理解,setjmp
函数一般会返回两次,第一次调用是为了给之后的跳转标记位置和保存上文的环境,第二次返回是由调用longjmp
函数引起的,此时的返回值不是 0 ,因此可以进入if
分支中处理异常。
流程如下:
>>>>>>
________return_a_non-zero_number ---> exception_handling ---> ...
/
/
normal_control_flow ---> setjmp --- first_called --- return_0 ---> do_something ---> longjmp ---> normal_control_flow
\ /
\__________________call_second_time_______________________/
<<<<<<
利用宏实现类似try ... except
的功能
实现效果
TRY
S
EXCEPT(E1)
S1
EXCEPT(E2)
S2
...
EXCEPT(En)
Sn
ELSE
S0
ENDTRY
// or
TRY
S
FINALLY
S1
ENDTRY
代码实现
重要结构体
except
结构体表示异常类型
struct except {
const char *reason;
};
except_frame
是一个链表的节点,包含了异常所在位置的程序环境、代码文件和行数,以及异常类型。
struct except_frame {
struct except_frame *prev;
jmp_buf env;
const char *file;
int line;
const struct except *exception;
};
重要全局变量
except_stack
是一个异常栈,每次处理栈顶的异常,直至栈空。因此这个结构是实现异常嵌套处理的关键。
struct except_frame *except_stack;
重要函数
except_raise
:用于抛出一个异常并跳转到setjump
,然后进入到异常处理分支。
void except_raise(const struct except *e, const char *file ,int line)
重要的宏
其实根据except_raise
函数和全局变量就可以实现所有的功能了,但下文中的宏也并不是多此一举,正是它们让我们可以实现TRY ... EXCEPT ... ELSE ... END_TRY
或者TRY ... FINAL ... END_TRY
这样的语法。
#define TRY do { \
volatile int except_flag; \
struct except_frame except_frame; \
except_frame.prev = except_stack; \
except_stack = &except_frame; \
except_flag = setjmp(except_frame.env); \
if (except_flag == EXCEPT_ENTERED) {
#define EXCEPT(e) \
if (except_flag == EXCEPT_ENTERED) \
except_stack = except_stack->prev; \
} else if (except_frame.exception == &(e)) { \
except_flag = EXCEPT_HANDLED;
#define ELSE \
if (except_flag == EXCEPT_ENTERED) \
except_stack = except_stack->prev; \
} else { \
except_flag = EXCEPT_HANDLED;
#define FINALLY \
if (except_flag == EXCEPT_ENTERED) \
except_stack = except_stack->prev; \
} { \
if (except_flag == EXCEPT_ENTERED) \
except_flag = EXCEPT_FINALIZED;
#define END_TRY \
if (except_flag == EXCEPT_ENTERED) \
except_stack = except_stack->prev; \
} if (except_flag == EXCEPT_RAISEED) RERAISE; \
} while (0)
#define RAISE(e) except_raise(&(e), __FILE__, __LINE__)
#define RERAISE except_raise(except_frame.exception, except_frame.file, except_frame.line)
#define RETURN switch (except_stack = except_stack->prev , 0) default: return
使用
先全局定义几种异常类型,可以如下定义:
struct except allocate_fail = {"Allocate failed"};
struct except div_by_zero = {"divided by 0"};
struct except unknown_exception = {"unknown exception"};
抛出异常:
if (something_happened)
RAISE(a_except);
else (another_thing_happened)
RAISE(another_except);
else
RAISE(unknown_exception);
捕捉异常:
TRY
do_something();
EXCEPT(allocate_fail)
printf("catch a exception of %s\n", allocate_fail.reason);
EXCEPT(div_by_zero)
printf("catch a exception of %s\n", div_by_zero.reason);
ELSE
printf("catch a exception of %s\n", unknown_exception.reason);
RERAISE;
END_TRY;
总结
在 C 语言中很多时候对异常的处理其实没有这么麻烦,直接使用 if else 几个分支就搞定了,而且也不会有因为跳转导致资源无法释放的问题。
但是通过setjmp.h
和一些宏实现try ... exception
同样是有价值的,除了锻炼思维和写宏读宏的能力外,它还有很多用处,尤其是在一些由 C 编写的其他语言中,它就能提供异常处理甚至是协程的功能(例如 Lua )。
因此研究setjmp.h
实现异常对我们设计架构乃至编程语言都大有裨益。