如何在setjmp()与longjmp()间传递超出int类型的数据?
用setjmp/longjmp实现简易try/catch异常系统的实践与踩坑指南
刚好我之前折腾过用setjmp和longjmp手搓简易异常处理,踩了不少坑,来跟你详细唠唠这块的细节:
先搞懂C99对setjmp的严格限制
你提到的C99规范限制是实打实的,官方文档里明确规定:
严格C99规范不允许读取setjmp的结果并存入变量
为啥会有这个奇怪的限制?因为setjmp本质是个宏,不是普通函数——首次调用它会保存当前执行上下文并返回0,而当longjmp跳转回来时,它其实是“模拟”返回一个你传入的非0值。如果把它的返回值存到变量里,编译器的优化逻辑可能会破坏上下文恢复的正确性,直接导致行为未定义。
合法的用法是直接把setjmp的返回值用在条件判断里,比如:
if (setjmp(exception_buf) == 0) { // 正常执行逻辑(对应try块) } else { // 异常处理逻辑(对应catch块) }
而下面这种写法在严格C99里绝对是违规的:
int ret = setjmp(exception_buf); // 行为未定义!
实现简易try/catch的核心思路
我们可以用宏来封装这个逻辑,把setjmp的条件判断藏在宏里,完美规避直接存返回值的问题。比如定义TRY、CATCH和THROW宏:
#include <setjmp.h> #include <stdio.h> #include <stdlib.h> // 线程局部的异常上下文,用来传递自定义数据(多线程安全) _Thread_local struct { int ex_type; // 异常类型标识 const char* msg; // 异常描述信息 void* data; // 自定义数据指针(支持任意类型) } current_exception; jmp_buf exception_buf; #define TRY if (setjmp(exception_buf) == 0) { #define CATCH } else { #define THROW(type, msg, data) do { \ current_exception.ex_type = type; \ current_exception.msg = msg; \ current_exception.data = data; \ longjmp(exception_buf, 1); \ } while(0)
解决不同类型数据的传递问题
正如你所说,longjmp只能传递一个int值,没法直接传字符串、结构体这些复杂类型。解决办法就是用上面的current_exception结构体:
- 在调用
THROW抛出异常前,先把要传递的异常类型、描述、自定义数据塞进这个上下文结构体 - 跳转到
CATCH块后,直接读取这个结构体里的内容就行
下面是一个完整的可运行示例:
// 定义自定义异常类型 #define EXCEPTION_DIV_BY_ZERO 1 #define EXCEPTION_OUT_OF_MEM 2 #define EXCEPTION_INVALID_ARG 3 void risky_calculation(int a, int b) { if (b == 0) { THROW(EXCEPTION_DIV_BY_ZERO, "Division by zero detected", NULL); } if (a < 0) { // 传递自定义数据(比如错误的参数值) int* invalid_arg = malloc(sizeof(int)); *invalid_arg = a; THROW(EXCEPTION_INVALID_ARG, "Negative argument not allowed", invalid_arg); } int* big_buf = malloc(1024 * 1024 * 100); if (!big_buf) { THROW(EXCEPTION_OUT_OF_MEM, "Failed to allocate large buffer", NULL); } free(big_buf); printf("Calculation succeeded: %d/%d = %d\n", a, b, a/b); } int main() { TRY { printf("Running risky calculation...\n"); risky_calculation(-5, 2); } CATCH { switch(current_exception.ex_type) { case EXCEPTION_DIV_BY_ZERO: printf("Caught error: %s\n", current_exception.msg); break; case EXCEPTION_OUT_OF_MEM: printf("Caught error: %s\n", current_exception.msg); break; case EXCEPTION_INVALID_ARG: printf("Caught error: %s (invalid value: %d)\n", current_exception.msg, *(int*)current_exception.data); free(current_exception.data); // 记得释放自定义数据 break; default: printf("Caught unknown exception\n"); } } return 0; }
额外的注意事项
- 线程安全:如果是多线程程序,一定要用线程局部存储(C11的
_Thread_local,或者GCC的__thread扩展)存放异常上下文,否则多个线程会互相干扰 - 资源清理:这个简易实现没有
finally块,如果需要在异常发生时清理资源(比如关闭文件、释放内存),得手动在CATCH块里处理,或者再加一个FINALLY宏来封装 - 编译器优化:编译时最好关闭一些激进的优化(比如
-O0或者-fno-omit-frame-pointer),避免编译器破坏setjmp/longjmp的上下文恢复逻辑
内容的提问来源于stack exchange,提问作者Qqwy




