C 语言没有原生支持异常处理,但是可以使用setjmplongjmp函数实现类似try ... except的功能。

本文主要参考:

setjmp.h

setjmp.h是 C 标准函数库中提供“非本地跳转”的头文件:控制流偏离了通常的子程序调用与返回序列。互补的两个函数setjmplongjmp提供了这种功能。

setjmplongjmp的典型用途是异常处理机制的实现:利用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传递给setjmplongjmp完成后,程序从对应的setjmp调用处继续执行,如同setjmp调用刚刚完成。如果value传递给longjmp零值,setjmp的返回值为 1 ;否则,setjmp的返回值为value

重要类型

  • jmp_buf:用于保存恢复调用环境所需要的信息。

浅探异常处理

下面一段代码给出了使用setjmplongjmp处理异常的基本思路:

#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

代码实现

GitHub 仓库

重要结构体

  • 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实现异常对我们设计架构乃至编程语言都大有裨益。