setjmp longjmp signal实现简单的try-catch

最近升级了Xcode 7 GM,发现GitDiff 插件不工作了。于是自力更生翻源码找bug。虽然最后发现原因是之前用Xcode-beta 时xcode-select 到了beta 版,和GitDiff 并没有什么关系,但看源码时发现一对看起来很牛逼的函数:setjmp/longjmp。好奇心大起,查阅了一下他们究竟为何物。

简单来说,setjmp/longjmp 类似goto,但牛逼的多,因为setjmp/longjmp 可以在函数间“跳来跳去”。

不得不吐槽这对函数名,实在太不直观了。。。

仅仅这么个作用的话,貌似没什么使用的必要,那GitDiff为何这么兴师动众的调用这么底层的函数呢?翻看了一下GitDiff 的commit 记录,才知道原来还有个主要人物--signal

概括GitDiff 的做法,就是通过setjmp保存好上下文,用signal 函数来截获可能出现的SIGPIPE,然后在SIGPIPE出现时通过longjmp返回出错之前的代码,并通过返回值来避开出错代码,继续往下走。
换句话说,就是实现了一个简单的try-catch机制。这也是setjmp/longjmp/signal互相配合的一个“常见”用法。

关于这个SIGPIPE,简单Google 了一下,貌似用popen时还是挺常见的,一般都需要一套同步机制来保证popenpclose

分开来说

  1. setjmp保存当前上下文环境到参数(jmp_buf类型)里,jmp_buf的细节和系统具体实现相关,我也没再追下去。其返回值的规则是:第一次调用时返回0,表示是从setjmp返回的;之后,如果longjmp被调用,那么程序会根据保存的上下文环境(之前保存的jmp_buf类型的变量,也是longjmp的第一个参数),再次来到setjmp这里,并返回一个由longjmp带过来的值(longjmp的第二个参数),程序可以通过setjmp的返回值来改变流程。
  2. 调用signal函数安装SIGPIPE信号的处理函数。通过man知道signal的返回值是“当前的对该信号(这里也即SIGPIPE)的处理函数”,也就是默认的处理函数,保存这个值很有必要,因为需要在最后进行恢复。
  3. 如果SIGPIPE被触发,那么在之前安装的信号处理函数里调用longjmp。其第二个参数就是返回到setjmp时的值,这里只要给一个不是0的值,就可以标记为“出错”。

虽然最后没有找出GitDiff 的bug,但是收获颇丰:)