译文 · 原文: Friday Q&A 2012-08-24: Things You Never Wanted To Know About C · 作者 Mike Ash
原文:https://www.mikeash.com/pyblog/friday-qa-2012-08-24-things-you-never-wanted-to-know-about-c.html 发布:2012-08-24 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样
距离我上次写文章已经有一段时间了,但我再次回归,这次将即兴介绍一段极为扭曲的代码集 —— 我用它来假装 C 语言拥有异常处理能力。我将深入探讨鲜为人知的 C 语言扩展、Linux 兼容性,以及最糟糕的 goto 语句,特此提醒!
C 语言中的错误处理 C 语言以其在当今标准下近乎裸机的特性,在错误发生时缺乏任何先进的流程控制机制。不同平台各自发展出了应对策略:有些通过语言特性实现,有些依托框架约定,还有些则选择完全不予处理。
异常 最广为人知、也最具争议的错误流程控制机制当属异常。C++、Java、Objective-C、PHP 等众多语言都提供了某种形式的异常处理构造。在 Objective-C 中,异常的使用方式如下:
@try { if (![someObject doSomethingInteresting]) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Failed to frobulate the glockenspiel!" userInfo:nil]; } return YES; } @catch (NSException *exception) { NSLog(@"Either die in the vacuum of space, or tell me what you thought of my poem."); return NO; } @finally { [prognosticator cleanupAfterSlithyToves]; }幸运的是,Apple 意识到这在 Cocoa 中往往看起来多么荒谬,因此弃用了 exceptions(异常)作为 error handling(错误处理)的手段,转而选择了 NSError 模型。现在,exception handling(异常处理)在 Objective-C 中的使用被保留(至少在理论上)用于真正‘异常’的状况,特别是 programmer error(程序员错误)。C 语言确实在 setjmp/longjmp 函数中拥有一种原始的 exception handling(异常处理)方式。(译注:这些函数在现代 C 语言开发中较少直接用作异常处理机制。)然而,它们非常奇怪,功能不全,且极易出错,因此是一个糟糕的选择。历史上,Objective-C 中的 exceptions(异常)就是用这些函数实现的,但 Apple 相当明智地用一项调用 C++ exception implementation(C++ 异常实现)的 compiler feature(编译器功能)替换了那些笨拙的宏。本质上,Objective-C 中的 @try 和 C++ 中的 try 现在是同一回事,尽管由于 class hierarchies(类层次结构)的分离,它们仍然不能互换。
Grafting exceptions onto C
第一个被问到的问题是,为什么会有人想把 exceptions(异常)放入纯 C 中。我的回答是,这段代码实在丑陋得无法容忍:
struct whatever *make_a_whatever(int alpha, const char *beta, const char *gamma, size_t delta) { struct whatever *brillig = malloc(sizeof(struct whatever)); int fd = -1, r = 0;
if (!brillig) return NULL; brillig->alpha = alpha; brillig->beta = strdup(beta); if (!brillig->beta) { free(brillig); return NULL; } if ((fd = open(gamma, O_RDONLY)) == -1) { free(brillig->beta); free(brillig); return NULL; } brillig->gamma = malloc(delta); if (!brillig->gamma) { close(fd); free(brillig->beta); free(brillig); return NULL; } if ((r = read(fd, brillig, delta)) != delta) { close(fd); free(brillig->gamma); free(brillig->beta); free(brillig); return NULL; } close(fd); return brillig; }诚然,这个例子有些刻意,可以通过重组来减少重复,但在实际工作中我确实遇到过非常类似这样的代码。一种可能的解决方案是简单的 goto:
struct whatever *make_a_whatever(int alpha, const char *beta, const char *gamma, size_t delta) { struct whatever *brillig = malloc(sizeof(struct whatever)); int fd = -1, r = 0;
if (!brillig) goto error; brillig->alpha = alpha; if (!(brillig->beta = strdup(beta))) goto error; if ((fd = open(gamma, O_RDONLY)) == -1) goto error; if (!(brillig->gamma = malloc(delta))) goto error; if ((r = read(fd, brillig, delta)) != delta) goto error; close(fd); return brillig; error: if (fd != -1) close(fd); if (brillig) { free(brillig->gamma); free(brillig->beta); free(brillig); } return NULL; }这当然好一些,但代码里仍然散布着紧密嵌套的 if 分支。提升可读性的下一步是将这些分支宏化:
#define error_if(c) do { if (!(c)) goto error; } #define error_ifnull(p) do { if ((p) == NULL) goto error; } #define error_ifneg1(e) do { if ((e) == -1) goto error; }
struct whatever *make_a_whatever(int alpha, const char *beta, const char *gamma, size_t delta) { struct whatever *brillig = malloc(sizeof(struct whatever)); int fd = -1;
error_ifnull(brillig); brillig->alpha = alpha; error_ifnull(brillig->beta = strdup(beta)); error_ifneg1(fd = open(gamma, O_RDONLY)); error_ifnull(brillig->gamma = malloc(delta)); error_if(read(fd, brillig, delta) == delta); close(fd); return brillig; error: if (fd != -1) close(fd); if (brillig) { free(brillig->gamma); free(brillig->beta); free(brillig); } return NULL; }那么,如果你想知道哪里出了问题呢?你既没有异常(exception)来将信息传递到调用栈(call stack)的上层,除了可能不适合内部函数的打印到标准错误(stderr)之外,也没有其他地方可以放置错误消息。如果你有无论是否出错都必须完成的重要清理工作(cleanup)呢?如果你希望能够提前从函数返回,但清理代码仍然能运行呢?
解决方案:滥用预处理器 101 为了简洁起见,我将省略通往我为此类问题构思的解决方案的过程中的几个步骤,直接展示结果。我需要的错误处理机制应满足:
- 可以轻松应用于任何函数
- 可以将具体错误的信息向上传递至调用链(call chain)
- 无论是否发生错误都能运行清理代码
- 无论函数是否提前返回都能运行清理代码
- 可以同时用于 Clang 和 GCC
最后一点相当棘手。有人可能会认为闭包(block)是同时解决其中几个问题的一个相当简单的方案,但闭包在 GCC 中并不存在 —— 除了苹果公司早已过时的定制版本之外。(译注:现代 GCC 版本已支持闭包特性,此说法基于写作时的历史背景。)
首先,这是应用了我编写的错误处理机制后的 make_a_whatever 函数的样子:
struct whatever *make_a_whatever(int alpha, const char *beta, const char *gamma, size_t delta, error_t **error) { struct whatever *brillig = NULL: int fd = -1;
error_enter() { on_nullerror(brillig = malloc(sizeof(struct whatever)), ENOMEM, "can't allocate our brillig!"); brillig->alpha = alpha; on_nullerror(brillig->beta = strdup(beta), ENOMEM, "can't allocate beta for brilling with alpha %d!", alpha); on_errno(fd = open(gamma, O_RDONLY), "can't open gamma %s!", gamma); on_nullerror(brillig->gamma = malloc(delta), ENOMEM, "can't allocate gamma either!"); on_readerr(fd, brillig, delta, "delta's not in gamma!"); return brillig; } error_handler() { if (brillig) { free(brillig->gamma); free(brillig->beta); free(brillig); } return NULL; } error_finally() { if (fd != -1) close(fd); } error_exit(error); }当然,你是否觉得这种方式更具可读性,这取决于个人看法,但它确实更具功能性。对于任何给定的错误,都能获得格式化的消息,并且错误仅在发生时才会通过引用(by reference)返回。这里是一个示例调用:
error_t *error = NULL; struct whatever *whatever = make_a_whatever(1, "gyre", "gimble", SIZE_WABE, &error);
if (!whatever) error_write(error, STDERR_FILENO, true);为方便起见,error_write 函数可以选择性地释放 error_t 对象(这是其第三个参数)。
这些宏和函数背后有不少精妙设计,下面按出现顺序逐一说明:
error_write函数可以选择性地释放 error_t对象(这是它的第三个参数)。这些宏和函数背后隐藏着不少巧妙机制,以下按出场顺序介绍:
error_write函数可以有选择地释放 error_t对象(这是第三个参数)。
这些宏和函数背后蕴含着相当多的技巧,下面按出现顺序进行说明:
#define error_enter() do { \ __label__ _error_start, _error_exit, _error_finally, _error_done; \ __block error_t *_last_error = NULL; \ do { \ CLEANUP_DECL \ _error_start:;真恶心,是吧?
首先,入口宏声明了几个” 本地” 标签。本地标签仅在其所在作用域内可见,这是 GCC 的扩展特性,Clang 也支持。这四个标签用于宏的其余部分来控制函数流程。注意,由于这些标签被多个宏共享,错误处理作用域无法嵌套。
接下来声明了一个 error_t 类型变量,用于存储此错误处理作用域中可能发生的错误。该变量标记为 __block,因为 Clang 实现的” finally” 子句使用了 block(块),并且必须从 block 内部更新该变量。对于 GCC 构建版本,__block 被宏定义为空操作。
随后进行” 清理声明”(cleanup declaration),并标记” start” 标签。主函数体将放置在此标签下方。
CLEANUP_DECL 及相关宏清理声明和清理实现的其他部分均为宏定义,因为 GCC 与 Clang 的实现存在显著差异:
#ifndef __has_feature #define __has_feature(f) 0 #endif
#if __has_feature(blocks) typedef void (^_error_cleanup_block_t)(void); static inline void _error_cleanup_block(_error_cleanup_block_t *b) { (*b)(); } #define CLEANUP_DECL _error_cleanup_block_t _error_cleanup __attribute__((cleanup(_error_cleanup_block),unused)) = NULL; \ goto _error_finally; #define CLEANUP_ASSIGN _error_cleanup = ^ #define CLEANUP_DONE_ASSIGN goto _error_start #elif __GNUC__ #define __block typedef void (*_error_cleanup_func_t)(void); #define CLEANUP_DECL auto void _error_cleanup_f(_error_cleanup_func_t *f); \ void (*_error_cleanup)(void) __attribute__((cleanup(_error_cleanup_f),unused)) = NULL; #define CLEANUP_ASSIGN void _error_cleanup_f(_error_cleanup_func_t *f) #define CLEANUP_DONE_ASSIGN #endif对于 Clang(以及任何未来可能采用 blocks(块)的 GCC),会为 cleanup block(清理块)声明一个 typedef(类型定义),就是一个基本的接受并返回 void 的块。一个 static inline C 函数(静态内联 C 函数)被定义(在头文件中,记住,不是作为宏展开的一部分)用于运行该块。
Clang 的清理声明使用 cleanup attribute(清理属性)声明一个 _error_cleanup_block_t。这个属性(另一个最初来自 GCC 的扩展)表示 “当此变量超出作用域时运行此 C 函数”。它还被标记为 unused 以避免在开启 -Wunused-variable 时出现警告。接下来,在函数主体运行之前,它跳转到 “finally” 标签。正如我们稍后将看到的,这对于确保在函数可能返回之前正确设置清理块是必要的。
对于没有 blocks 的 GCC,会进行类似的过程,但 __block 被定义为空,并且代码不是声明清理块,而是声明一个 “nested function”(嵌套函数)来充当清理作用。
嵌套函数是 GCC 独有的扩展,Clang 拒绝采用,认为当 blocks(块)提供了更优的作用域函数解决方案时,引入该特性得不偿失。嵌套函数仅存在于其父函数内部,并具备与 block 相同的从父函数词法作用域中捕获变量的能力。由于嵌套函数不能在其父函数外部使用(与 blocks 不同),因此无需特殊限定符来指定被捕获变量的内存管理方式。
最后,GCC 版本不会跳转到 “finally” 标签,因为嵌套函数无需赋值给任何变量即可工作;原型声明就足以使用它。
auto 关键字在 C 语言中本已基本无用,此处却将声明标记为嵌套函数的原型,而非标准函数原型。这是对标准关键字的非标准使用,亦是 GCC 的另一项扩展。
为简洁起见,我再次不会在此粘贴所有 on_*() 宏的代码,因为它们在结构上大同小异。
// Common macros, do not call directly #if DEBUG #define _error_fail(e, msg, ...) do { \ _last_error = error_newf(e, __LINE__, __FILE__, __PRETTY_FUNCTION__, msg, ## __VA_ARGS__); \ goto _error_exit; \ } while (0) #else #define _error_fail(e, msg, ...) do { \ _last_error = error_newf(e, msg, ## __VA_ARGS__); \ goto _error_exit; \ } while (0) #endif #define _error_throw() do { goto _error_exit; } while (0)
// Use e as the error code when (e) < 0. // Intended for POSIX routines that return an error code, such as pthread_*() #define on_error(e, msg, ...) do { \ int _error_code __attribute__((unused)) = (e); \ if (_error_code < 0) \ _error_fail(_error_code, msg, ## __VA_ARGS__); \ } while (0)
// Re-throw an error when (e) == false. // Intended for chaining error-returning methods. #define on_falsethrow(e) do { if (!(e)) _error_throw(); } while (0)每个 on_*() 宏都会检查其特定的错误条件,保存格式化函数所需的相关错误代码,并调用 _error_fail() 宏。
相应地,_error_fail() 宏会根据传入的错误代码、消息(在调试模式下还包括错误发生的文件、行号和函数)将 _last_error 变量设置为新的 error_t 对象。随后,它跳转至 _error_exit,从而进入下方的” catch” 处理器(参见下文)。
这些宏的” throw” 版本旨在沿调用链向上传递错误;现有的 error_t 对象会被原样保留,并直接调用” catch” 处理器。例如:
bool make_many_whatevers(size_t n, struct whatever **list, error_t **error) { struct whatever *mimsy = NULL; size_t i = 0;
error_enter() { for (i = 0; i < n; ++i) { on_nullthrow(mimsy = make_a_whatever(/* etc */, error)); } return true; } error_handler() { for (size_t j = 0; j < i; ++j) free_a_whatever(list[i]); return false; } error_finally() { } error_exit(error); }调用者会收到实际发生的底层错误。
error_handler
接着是” catch”(捕获)代码块:
#define error_handler() \ goto _error_done; \ _error_exit: __attribute__((unused));error_handler() 宏紧接在函数中” something went wrong”(出了问题)的部分之前。在函数主体之后,它会跳转到” done” 标签。这就是函数的” normal exit”(正常退出)情形,使得函数主体看起来如下所示:
_error_start: // main body goto _error_done; _error_exit: // something went wrong_error_exit 标签(label)被声明为未使用,以便如果函数(function)没有 on_*() 语句(statements)时编译器(compiler)不会抱怨,这种情况可能发生在你为了将来添加错误处理(error handling)而未来防护一个例程但还没有实现时。这是” catch”(捕获)代码放置的地方,处理任何错误。
error_finally 并且最终,“finally”:
#define error_finally() \ goto _error_done; \ _error_finally:; \ CLEANUP_ASSIGN {精彩的部分就在这里。finally 部分在 catch 之后,会跳转到块末尾的 done 标签。请注意,这意味着在正常的控制流中,finally 代码根本不会被执行!实际上,在 GCC 版本中,_error_finally 下的代码行永远不会被执行。
Clang 中的 CLEANUP_ASSIGN 会取到在 CLEANUP_DECL 中声明的 _error_cleanup 块,并将 finally 代码实际赋值给它。因此,任何需要被 finally 处理器修改其值的变量都必须声明为 __block。这就是为什么,正如我们之前看到的,start 标签在真正执行函数主体之前就跳转到这里;如果块在那时没有被赋值,那么任何清理操作都不会被执行。
GCC 的版本则简单地启动先前声明的嵌套函数。无论哪种情况,finally 代码最终都成为某个函数的一部分,该函数不属于正常控制流,并将通过 _error_cleanup 变量上的 cleanup attribute 来执行。请注意,GCC 版本从未费心为该变量赋值,因为嵌套函数将通过名称直接调用,而不是像块那样通过值跳转过去。(译注:GCC 通过嵌套函数实现 @finally 的方式较为古老,现代主流编译器如 Clang 主要使用基于 block 的机制。)
error_exit 现在是时候清理我们留下的东西了:
#define error_exit(error_param) \ if (_last_error && error_param) \ *error_param = _last_error; \ }; \ CLEANUP_DONE_ASSIGN; \ _error_done:; \ } while (0); \ } while (0)作为 “finally” 块的一部分,传递给此宏的 error 参数(若非 NULL)会被赋予 _last_error 的值(若有)。“finally” 块至此结束,在 Clang 编译器下,控制流跳转回 “start” 处,继续执行函数的主体部分。(同样,在 GCC 编译器下不需要这额外的控制流技巧。)
标记着整个流程最终结束的 “done” 标签在此处出现,错误处理器的作用域随之关闭。
因此,执行顺序如下:
- 声明一个错误变量和一个清理块。
- (仅限 Clang)跳转到 “finally” 处理器,将错误代码赋值给清理块。
- (仅限 Clang)跳转回函数的主体部分。
- (仅当发生错误时)赋值错误变量,并运行错误处理器。
- 执行 “finally” 处理器。
- 更新任何错误参数。
error_*() 函数
还有一组代码我们尚未涉及:那些实际管理 error_t 类型的函数。它们相当直接;这部分没有涉及任何 hack。(译注:此处的 “hack” 指技巧性或非正统的实现方式)嗯,也许只有少许是为了处理调试与非调试情况之间的差异并获取可执行文件的名称:
typedef struct { int error_code; char *message; #if DEBUG int line; char *file; char *function; #endif } error_t;
#if DEBUG #define DEBUG_PARMS , int line, const char *file, const char *function #define DEBUG_PASS(f) , line, f(file), f(function) #define DEBUG_FNUM 5 #define DEBUG_VNUM 6 #else #define DEBUG_PARMS #define DEBUG_PASS(f) #define DEBUG_FNUM 2 #define DEBUG_VNUM 3 #endif
static inline error_t __attribute__((malloc)) *error_new(int error_code DEBUG_PARMS, char *message) { error_t *r = calloc(1, sizeof(error_t));
*r = (error_t){ error_code, strdup(message) DEBUG_PASS(strdup) }; return r; }
static inline error_t __attribute__((format(printf,DEBUG_FNUM,0),malloc)) *error_newvf(int error_code DEBUG_PARMS, char *format, va_list args) { error_t *r = calloc(1, sizeof(error_t));
*r = (error_t){ error_code, NULL DEBUG_PASS(strdup) }; if (vasprintf(&r->message, format, args) < 0) r->message = strdup(format); return r; }
static inline error_t __attribute__((format(printf,DEBUG_FNUM,DEBUG_VNUM),malloc)) *error_newf( int error_code DEBUG_PARMS, char *format, ...) { error_t *r = NULL; va_list args;
va_start(args, format); r = error_newvf(error_code DEBUG_PASS(), format, args); va_end(args); return r; }
static inline void error_free(error_t *error) { #if DEBUG free(error->function); free(error->file); #endif free(error->message); free(error); }
static inline void error_write(error_t *error, int fd, bool do_free) { #ifdef MACOSX extern char **_NSGetProgname(void); char *progname = *_NSGetProgname(); #elif defined(LINUX) char buf[MAXPATHLEN + 1] = {0}, *progname = buf; if (readlink("/proc/self/exe", buf, MAXPATHLEN) < 0) progname = "unknown"; #endif #if DEBUG dprintf(fd, "%s: %s (in %s at %s:%d)\n", basename(progname), error->message, error->function, error->file, error->line); #else dprintf(fd, "%s: %s\n", basename(progname), error->message); #endif if (do_free) error_free(error); }这段代码中唯一真正有趣的部分是 error_write() 里获取可执行文件名的代码段。由于不能直接假定 argv 可用,因此调用了平台相关的代码。在 OS X 上,使用了来自 crt_externs.h 的 _NSGetProgname() 函数;而在 Linux 上,则读取 /proc/self/exe 符号链接的内容。dprintf() 只是接受文件描述符而非 FILE * 参数的 fprintf()。
对于好奇者,__attribute__((malloc)) 标记该函数返回的指针不会被任何其他指针别名化,这对优化很有用。__attribute__((format(printf,n,m))) 则是简单地告诉编译器对格式字符串和参数执行与 printf() 系列函数相同的检查。这些属性以及其他属性的文档可在 GCC 手册中找到。
结论
这个错误处理技巧采用了一种混合异常 / 错误对象的方法来解决问题,但附带一长串缺点:
-
严重依赖大量编译器特定的语言扩展,这些扩展无疑不具备普遍可移植性
-
要求在 “finally” 处理器中所有被修改的变量必须使用
__block修饰符,而这个修饰符在 Linux 环境下查看代码时会让任何人感到困惑。 -
严重滥用预处理器(preprocessor),导致代码的实际功能对新来者而言晦涩难懂。
-
严重滥用
goto,导致控制流非线性且不直观。 -
任何需要在函数的多个代码块中使用的变量,都必须在错误处理作用域之外声明。
-
空的 “catch” 或 “finally” 处理器不能被省略。
-
错误处理作用域无法嵌套。
-
在这个实现中,宏的命名有些糟糕。
-
传递
error_t对象相当繁琐,除非你习惯了 NSError(NSError 是 Objective-C 的错误对象类)那种错误处理风格。 -
可能不是线程安全的(我还没有测试过这一点)。
-
它既不能完美模拟异常,也不能完美模拟错误对象,这意味着来自两者的经验都无法完全应用。
-
除非你排斥或无法使用 C++,否则它绝不优于直接使用 C++ 的异常(exception)。
这段代码设计仓促、自然演进,存在明显的低效之处,绝非生产环境中的可用形态。它更多只是一种与语言嬉戏的炫酷方式。尽管如此,它确实展示了一个坚定(或许略带偏执)的头脑如何利用相对笨拙的工具实现惊人操作。
若想探索更极致的范例 —— 那些在 C 与 Objective-C 中完成古怪不可思议之事的代码,我推荐查阅 Justin Spahr-Summers 的 libextc 和 libextobjc 库。这两个库部分启发了本文呈现的代码,其中蕴含着我所见过最神奇深奥的宏、switch、if、for 及 Duff’s Device(达夫设备)用法,其精妙程度堪比 IOCCC(国际 C 语言混淆代码大赛)之外的极限之作。更甚的是,libextc 在完全保持平台与编译器无关性的前提下完成了所有工作,这堪称真正壮举。
本周内容到此结束。敬请期待迈克下周今天发布的下一篇文章。感谢阅读!
Original (English)
Source: https://www.mikeash.com/pyblog/friday-qa-2012-08-24-things-you-never-wanted-to-know-about-c.html
It’s been a bit since I did an article, but I’m back again, with a somewhat off-the-cuff treatment of a very twisted set of code I use to pretend that C has exceptions. I delve into little-known extensions of C, Linux compatibility, and worst of all, goto, so be warned!
Error handling in CC, being something of a bare-metal language by today’s standards, lacks any advanced facilities for managing control flow when errors happen. Most platforms have found their own ways of coping with the situation, some with language features, some with framework conventions, and others by not coping at all.
ExceptionsThe best known, and also perhaps most debated, means of handling control flow for error conditions is exceptions. C++, Java, Objective-C, PHP, and many others all have some form of exception handling construct. In Objective-C, exceptions look like this:
@try { if (![someObject doSomethingInteresting]) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Failed to frobulate the glockenspiel!" userInfo:nil]; } return YES; } @catch (NSException *exception) { NSLog(@"Either die in the vacuum of space, or tell me what you thought of my poem."); return NO; } @finally { [prognosticator cleanupAfterSlithyToves]; }Fortunately, Apple realized how ridiculous this tends to look in Cocoa and deprecated exceptions for error handling, instead opting for the NSError model. The use of exception handling in Objective-C is now reserved (at least in theory) for truly “exceptional” conditions, particularly programmer error.
C does have a primitive sort of exception handling in the setjmp/longjmp functions. However, they’re extremely weird, not very full featured, and highly error-prone, so they make a poor choice.
Historically, exceptions in Objective-C were implemented with these functions, but Apple quite sensibly replaced the unwieldy macros with a compiler feature which calls into the C++ exception implementation. In essence, @try in Objective-C and try in C++ are now the same thing, though they’re still not interchangeable due to the separation of class heirarchies.
Grafting exceptions onto CThe first question asked is why anyone would want to put exceptions into pure C. My answer is that this code is just too ugly to let live:
struct whatever *make_a_whatever(int alpha, const char *beta, const char *gamma, size_t delta) { struct whatever *brillig = malloc(sizeof(struct whatever)); int fd = -1, r = 0;
if (!brillig) return NULL; brillig->alpha = alpha; brillig->beta = strdup(beta); if (!brillig->beta) { free(brillig); return NULL; } if ((fd = open(gamma, O_RDONLY)) == -1) { free(brillig->beta); free(brillig); return NULL; } brillig->gamma = malloc(delta); if (!brillig->gamma) { close(fd); free(brillig->beta); free(brillig); return NULL; } if ((r = read(fd, brillig, delta)) != delta) { close(fd); free(brillig->gamma); free(brillig->beta); free(brillig); return NULL; } close(fd); return brillig; }Admittedly, the example is contrived and could be reorganized to require less repetition, but in the real world I’ve ended up with code that looks a lot like this. One possible solution is a dumb goto:
struct whatever *make_a_whatever(int alpha, const char *beta, const char *gamma, size_t delta) { struct whatever *brillig = malloc(sizeof(struct whatever)); int fd = -1, r = 0;
if (!brillig) goto error; brillig->alpha = alpha; if (!(brillig->beta = strdup(beta))) goto error; if ((fd = open(gamma, O_RDONLY)) == -1) goto error; if (!(brillig->gamma = malloc(delta))) goto error; if ((r = read(fd, brillig, delta)) != delta) goto error; close(fd); return brillig; error: if (fd != -1) close(fd); if (brillig) { free(brillig->gamma); free(brillig->beta); free(brillig); } return NULL; }This is certainly better, but still litters the code with a nest of tightly clustered if branches. The next step to readability is to macroize the branches:
#define error_if(c) do { if (!(c)) goto error; } #define error_ifnull(p) do { if ((p) == NULL) goto error; } #define error_ifneg1(e) do { if ((e) == -1) goto error; }
struct whatever *make_a_whatever(int alpha, const char *beta, const char *gamma, size_t delta) { struct whatever *brillig = malloc(sizeof(struct whatever)); int fd = -1;
error_ifnull(brillig); brillig->alpha = alpha; error_ifnull(brillig->beta = strdup(beta)); error_ifneg1(fd = open(gamma, O_RDONLY)); error_ifnull(brillig->gamma = malloc(delta)); error_if(read(fd, brillig, delta) == delta); close(fd); return brillig; error: if (fd != -1) close(fd); if (brillig) { free(brillig->gamma); free(brillig->beta); free(brillig); } return NULL; }Now, what if you want to know what went wrong? You have no exceptions to pass information up the call stack, nor any place to stuff an error message besides printing to stderr, which may not be appropriate for an internal function. What if you have significant cleanup that has to be done regardless of whether or not errors occurred? What if you want to be able to return early from the function but have your cleanup code still run?
The Solution: Preprocessor Abuse 101For the sake of brevity, I’ll skip a few steps in the process that leads to the solution I came up with for these problems and just show it. I needed error handling that:
-
Can be applied easily to any function
-
Can pass information about specific errors up the call chain
-
Can run cleanup code regardless of whether an error ocurrred
-
Can run cleanup code regardless of whether the function returns early
-
Can be used with both Clang and GCC
That last is a doozy. One might imagine blocks as a fairly simple solution to several of these issues at once, but blocks don’t exist in GCC outside of Apple’s obsolete customized version.
First, here is what our make_a_whatever function looks like with the error handling I wrote:
struct whatever *make_a_whatever(int alpha, const char *beta, const char *gamma, size_t delta, error_t **error) { struct whatever *brillig = NULL: int fd = -1;
error_enter() { on_nullerror(brillig = malloc(sizeof(struct whatever)), ENOMEM, "can't allocate our brillig!"); brillig->alpha = alpha; on_nullerror(brillig->beta = strdup(beta), ENOMEM, "can't allocate beta for brilling with alpha %d!", alpha); on_errno(fd = open(gamma, O_RDONLY), "can't open gamma %s!", gamma); on_nullerror(brillig->gamma = malloc(delta), ENOMEM, "can't allocate gamma either!"); on_readerr(fd, brillig, delta, "delta's not in gamma!"); return brillig; } error_handler() { if (brillig) { free(brillig->gamma); free(brillig->beta); free(brillig); } return NULL; } error_finally() { if (fd != -1) close(fd); } error_exit(error); }Whether or not you find this more readable is a matter of opinion, of course, but it’s certainly more functional. For any given error, a formatted message is available, and the error is returned by reference if and only if one occurred. Here’s an example call:
error_t *error = NULL; struct whatever *whatever = make_a_whatever(1, "gyre", "gimble", SIZE_WABE, &error);
if (!whatever) error_write(error, STDERR_FILENO, true);For convenience, the error_write function can optionally free the error_t object (this is the third parameter).
There’s a good bit of magic behind these macros and functions. Here they are in order of appearance:
error_enterPrepare yourself:
#define error_enter() do { \ __label__ _error_start, _error_exit, _error_finally, _error_done; \ __block error_t *_last_error = NULL; \ do { \ CLEANUP_DECL \ _error_start:;Yuck, huh?
First, the entry macro declares several “local” labels. A local label is simply a label whose name is local to its enclosing scope. This is a GCC extension supported by Clang. These four labels are used by the rest of the macros to manage the function’s control flow. Notice that because the labels are used across multiple macros, error handling scopes can not be nested.
Next, an error_t is declared to hold any error that occurs within this error handling scope. It is marked __block, as the Clang implementation of the “finally” clause uses blocks and must update this variable from inside one. For the GCC build, __block is #defined to nothing.
Next, the “cleanup declaration” takes place, then the “start” label is marked. The main function body goes underneath this.
CLEANUP_DECL and friendsThe cleanup delcaration and several other pieces of the cleanup implementation are macros, as the implementation is significantly different between GCC and Clang:
#ifndef __has_feature #define __has_feature(f) 0 #endif
#if __has_feature(blocks) typedef void (^_error_cleanup_block_t)(void); static inline void _error_cleanup_block(_error_cleanup_block_t *b) { (*b)(); } #define CLEANUP_DECL _error_cleanup_block_t _error_cleanup __attribute__((cleanup(_error_cleanup_block),unused)) = NULL; \ goto _error_finally; #define CLEANUP_ASSIGN _error_cleanup = ^ #define CLEANUP_DONE_ASSIGN goto _error_start #elif __GNUC__ #define __block typedef void (*_error_cleanup_func_t)(void); #define CLEANUP_DECL auto void _error_cleanup_f(_error_cleanup_func_t *f); \ void (*_error_cleanup)(void) __attribute__((cleanup(_error_cleanup_f),unused)) = NULL; #define CLEANUP_ASSIGN void _error_cleanup_f(_error_cleanup_func_t *f) #define CLEANUP_DONE_ASSIGN #endifFor Clang (and any potential future GCC which adopts blocks), a typedef is declared for a cleanup block, just your basic takes-and-returns-void. A static inline C function is defined (in the header, remember, not as part of the macro expansion) for running the block.
The cleanup delcaration for Clang declares an _error_cleanup_block_t with the cleanup attribute. This attribute (another extension originally from GCC) says “run this C function when this variable goes out of scope”. It is also marked unused to avoid warnings when -Wunused-variable is turned on. Next, it jumps to the “finally” label, before the function’s main body runs. As we’ll see later, this is necessary to make sure the cleanup block is properly set up before any potential return from the function.
For GCC, which has no blocks, a similar process takes place, but __block is defined to nothing, and instead of declaring a cleanup block, the code declares a “nested function” to serve for cleanup.
Nested functions are a GCC-only extension which Clang refused to adopt as being not worth the trouble when blocks provide a much better solution for scope-specific functions. The nested function exists only within its parent function and has the same ability to capture from the lexical scope of the parent that a block does. As a nested function can not be used outside its parent function, unlike blocks, it has no need for a special qualifier to specify the memory management around captured variables.
Lastly, the GCC version does not jump to the “finally” label, as the nested function does not need to be assigned anywhere to work; the prototype is enough to use it.
The auto keyword, otherwise pretty much useless in C, marks the delcaration as the prototype for a nested function, rather than as a standard function prototype. This is a nonstandard use of a standard keyword and yet another GCC extension.
on_everythingFor the sake of brevity, again, I will not paste every single one of the on_*() macros here, as they’re all largely the same.
// Common macros, do not call directly #if DEBUG #define _error_fail(e, msg, ...) do { \ _last_error = error_newf(e, __LINE__, __FILE__, __PRETTY_FUNCTION__, msg, ## __VA_ARGS__); \ goto _error_exit; \ } while (0) #else #define _error_fail(e, msg, ...) do { \ _last_error = error_newf(e, msg, ## __VA_ARGS__); \ goto _error_exit; \ } while (0) #endif #define _error_throw() do { goto _error_exit; } while (0)
// Use e as the error code when (e) < 0. // Intended for POSIX routines that return an error code, such as pthread_*() #define on_error(e, msg, ...) do { \ int _error_code __attribute__((unused)) = (e); \ if (_error_code < 0) \ _error_fail(_error_code, msg, ## __VA_ARGS__); \ } while (0)
// Re-throw an error when (e) == false. // Intended for chaining error-returning methods. #define on_falsethrow(e) do { if (!(e)) _error_throw(); } while (0)Each on_*() macro checks its own particular condition for an error, saves off any relevant error code for use by the formatting function, and calls the _error_fail() macro.
In turn, the _error_fail() macro sets the _last_error variable to a new error_t based on the passed error code, message, and in debug mode, the file, line, and function in which the error occurred. It then jumps to _error_exit, which takes it to the “catch” handler (see below).
The “throw” versions of these macros are intended for passing errors up the call chain; the existing error_t object is simply left as is and the “catch” handler is invoked directly. For example:
bool make_many_whatevers(size_t n, struct whatever **list, error_t **error) { struct whatever *mimsy = NULL; size_t i = 0;
error_enter() { for (i = 0; i < n; ++i) { on_nullthrow(mimsy = make_a_whatever(/* etc */, error)); } return true; } error_handler() { for (size_t j = 0; j < i; ++j) free_a_whatever(list[i]); return false; } error_finally() { } error_exit(error); }The caller will receive whatever the underlying error was.
error_handlerAnd now the “catch” block:
#define error_handler() \ goto _error_done; \ _error_exit: __attribute__((unused));The error_handler() macro comes immediately before the “something went wrong” part of the function. Immediately after the main body of the function, it jumps to the “done” label. This is the “normal exit” from function case, making the main body look like this:
_error_start: // main body goto _error_done; _error_exit: // something went wrongThe error_exit label is declared unused so that the compiler doesn’t complain if the function has no on*() statements, which could happen if you’re future-proofing a routine for adding error handling later but haven’t gotten there yet. This is where the “catch” code goes, handling any errors.
error_finallyAnd finally, the “finally”:
#define error_finally() \ goto _error_done; \ _error_finally:; \ CLEANUP_ASSIGN {This is where the fun happens. The “finally” part, coming after the “catch”, jumps to “done” at the end of it. Notice that this means the “finally” code is never directly executed in normal control flow! In fact, in the GCC version, the lines under _error_finally are never executed at all.
CLEANUP_ASSIGN in Clang takes the _error_cleanup block that was declared in CLEANUP_DECL and actually assigns the “finally” code to it. Hence, any variables whose values need to be modified by the “finally” handler must be declared __block. This is why, as we saw above, the “start” jumps to here before actually executing the function’s main body; if the block wasn’t assigned at that time, no cleanup would ever be executed.
The GCC version simply starts the nested function which was declared earlier. In either case, the “finally” code ends up as part of a function which is not part of the normal control flow, and will be executed by the cleanup attribute on the _error_cleanup variable. Notice that the GCC version never bothers assigning any value to that variable, since the nested function will simply be called by name instead of jumped to by value as a block would be.
error_exitTime to clean up after ourselves:
#define error_exit(error_param) \ if (_last_error && error_param) \ *error_param = _last_error; \ }; \ CLEANUP_DONE_ASSIGN; \ _error_done:; \ } while (0); \ } while (0)As part of the “finally” block, the error parameter which was passed to this macro, if non-NULL, is given the value of _last_error, if any. The “finally” block is ended, and under Clang, control jumps back to the “start”, continuing the main body of the function. (Again, under GCC the extra bit of control-flow trickery doesn’t need to happen.)
The “done” label, which marks the final end of all this mess, shows up here, and the error handler scope is closed out.
So, here are the steps in execution order:
-
Declare an error variable and a cleanup block.
-
In Clang only, jump to the “finally” handler to assign the code to the cleanup block.
-
In Clang only, jump back to the function’s main body.
-
On error only, assign the error variable, and run the error handler.
-
Execute the “finally” handler.
-
Update any error parameter.
The error_*() functionsThere’s one group of code we haven’t looked at yet: The functions which actually manage the error_t type. They’re pretty straightforward; there’s no hackery involved in this part. Well, maybe just a little to cover the differences between the debug and non-debug cases and grab the executable’s name:
typedef struct { int error_code; char *message; #if DEBUG int line; char *file; char *function; #endif } error_t;
#if DEBUG #define DEBUG_PARMS , int line, const char *file, const char *function #define DEBUG_PASS(f) , line, f(file), f(function) #define DEBUG_FNUM 5 #define DEBUG_VNUM 6 #else #define DEBUG_PARMS #define DEBUG_PASS(f) #define DEBUG_FNUM 2 #define DEBUG_VNUM 3 #endif
static inline error_t __attribute__((malloc)) *error_new(int error_code DEBUG_PARMS, char *message) { error_t *r = calloc(1, sizeof(error_t));
*r = (error_t){ error_code, strdup(message) DEBUG_PASS(strdup) }; return r; }
static inline error_t __attribute__((format(printf,DEBUG_FNUM,0),malloc)) *error_newvf(int error_code DEBUG_PARMS, char *format, va_list args) { error_t *r = calloc(1, sizeof(error_t));
*r = (error_t){ error_code, NULL DEBUG_PASS(strdup) }; if (vasprintf(&r->message, format, args) < 0) r->message = strdup(format); return r; }
static inline error_t __attribute__((format(printf,DEBUG_FNUM,DEBUG_VNUM),malloc)) *error_newf( int error_code DEBUG_PARMS, char *format, ...) { error_t *r = NULL; va_list args;
va_start(args, format); r = error_newvf(error_code DEBUG_PASS(), format, args); va_end(args); return r; }
static inline void error_free(error_t *error) { #if DEBUG free(error->function); free(error->file); #endif free(error->message); free(error); }
static inline void error_write(error_t *error, int fd, bool do_free) { #ifdef MACOSX extern char **_NSGetProgname(void); char *progname = *_NSGetProgname(); #elif defined(LINUX) char buf[MAXPATHLEN + 1] = {0}, *progname = buf; if (readlink("/proc/self/exe", buf, MAXPATHLEN) < 0) progname = "unknown"; #endif #if DEBUG dprintf(fd, "%s: %s (in %s at %s:%d)\n", basename(progname), error->message, error->function, error->file, error->line); #else dprintf(fd, "%s: %s\n", basename(progname), error->message); #endif if (do_free) error_free(error); }The only really interesting part of this code is the part in error_write() which grabs the executable’s name. Since it’s not assumed that argv is available directly, some platform-specific code is invoked. On OS X, the _NSGetProgname() function from crt_externs.h is used, whereas on Linux, the contents of /proc/self/exe symlink are read. dprintf() is just fprintf() that takes a descriptor instead of a FILE *.
For the curious, attribute((malloc)) marks the function as returning a pointer that is guaranteed not to be aliased by any other pointers, which is useful for optimization purposes. attribute((format(printf,n,m))) simply tells the compiler to do the same checking of the format string and arguments that it would do for the printf() family of functions. These and the other attributes are documented in the GCC Manual.
ConclusionThis error handling trick takes a hybrid exception/error-object approach to the problem, and comes with a laundry list of drawbacks:
-
Relies heavily on lots of compiler-specific language extensions which are definitely not universally portable
-
Requires all modified variables in “finally” handlers to use __block, a qualifier which makes no sense to anyone looking at the code on Linux
-
Severely abuses the preprocessor, making the actual functionality of the code obscure to a newcomer
-
Severely abuses goto, resulting in a non-linear and unobvious control flow.
-
Any variable that needs to be available in more than one of the blocks of the function must be declared outside the error handler scope
-
Empty “catch” or “finally” handlers can’t be ommitted
-
Error handling scopes can’t be nested
-
The macros are somewhat poorly named in this implementation
-
Passing the error_t object around is cumbersome unless you’re used to the NSError style of error handling
-
Probably isn’t thread-safe (I haven’t tested this)
-
Doesn’t emulate either exceptions or error objects perfectly, meaning experience from neither can be fully applied
-
Is in no way superior to simply using C++‘s exceptions unless you have an aversion to, or are unable to use, C++
This code is hastily designed, organically grown, has obvious inefficiencies, and is definitely not useful in a production environment in this form. Mostly, it’s just a cool way of playing with the language. Still, it shows just how much a determined (and perhaps slightly unbalanced) mind can do with relatively crummy tools.
For some even more extreme examples of doing strange and unbelievable things in both C and Objective-C, I recommend looking into Justin Spahr-Summers’ libextc and libextobjc libraries, which served as partial inspiration for the code I presented here. Both contain some of the most amazing and arcane uses of macros, switch, if, for, and Duff’s Device that I’ve ever encountered outside of the IOCCC. To top it off, libextc manages to do all of its work while being completely platform- and compiler-agnostic, which is a true accomplishment.
That’s about all for this week. Stay tuned for Mike’s next article, coming to you a week from today. Thanks for reading!