异常是如何实现的?
几乎每个人都在用它们,但很多人,包括我自己,往往理所当然地认为它们就是这样工作的。
我在寻找高质量的资料。我使用的编程语言有:Java、C、C#、Python、C++,所以这些语言的内容对我来说最有兴趣。
现在,C++可能是个不错的起点,因为在这个语言里你可以随意尝试各种东西。
另外,C语言和汇编语言很接近。那怎么用纯C语言的构造来模拟异常处理,而不使用汇编呢?
最后,我听说谷歌的员工在某些项目中因为速度的原因不使用异常处理。这是真的吗?没有异常处理,怎么能完成一些重要的事情呢?
谢谢。
10 个回答
在他的书《C接口与实现:创建可重用软件的技术》中,D. R. Hanson 提供了一种在纯C语言中实现异常处理的好方法,使用了一些宏和 setjmp/longjmp
。他提供了 TRY/RAISE/EXCEPT/FINALLY 这些宏,几乎可以模拟C++异常处理的所有功能,甚至更多。
你可以在 这里 查看代码(看看 except.h/except.c 文件)。
顺便说一下,关于你提到的谷歌的问题,他们的员工实际上是可以在新代码中使用异常处理的,而对于旧代码禁止使用的官方理由是因为旧代码已经是那样写的,混用风格没有意义。
我个人也认为,没有异常处理的C++并不是一个好主意。
这里有一种常见的C++异常处理的实现方式:
http://www.codesourcery.com/public/cxx-abi/abi-eh.html
这个实现是针对Itanium架构的,但文中描述的实现方式也适用于其他架构。需要注意的是,这是一份很长的文档,因为C++的异常处理比较复杂。
这里有一个关于LLVM如何实现异常处理的好描述:
http://llvm.org/docs/ExceptionHandling.html
由于LLVM旨在成为许多运行时的通用中间表示,因此文中描述的机制可以应用于多种编程语言。
异常其实是更广泛的非本地控制流结构中的一种特例。还有其他一些例子,比如:
- 通知(异常的一种推广,最早来自某些老旧的Lisp对象系统,现在在CommonLisp和Ioke等语言中实现),
- 继续(一种更结构化的
GOTO
,在高级语言中比较流行), - 协程(子程序的一种推广,特别在Lua中很受欢迎),
- 生成器(类似Python中的生成器,实际上是协程的一种限制形式),
- 纤程(一种轻量级的协作线程),当然还有之前提到的
GOTO
。
(我相信还有很多其他的例子我没有提到。)
这些结构有一个有趣的特性,那就是它们在表达能力上大致是等价的:如果你有一种,你可以很容易地构建出其他的。
所以,如何最好地实现异常,取决于你手头有哪些其他结构:
- 每个CPU都有
GOTO
,因此如果必须的话,你总是可以退而求其次使用它。 - C语言有
setjmp
/longjmp
,这基本上是用“万用胶带和牙签”做出来的MacGyver式的继续(虽然不是真正的东西,但至少能帮你解决眼前的问题,如果没有更好的选择的话)。 - JVM和CLI有自己的异常处理机制,这意味着如果你的语言的异常语义和Java或C#的匹配,那你就没问题了(但如果不匹配,那就麻烦了)。
- Parrot虚拟机同时支持异常和继续。
- Windows有自己的异常处理框架,语言实现者可以在此基础上构建自己的异常处理机制。
一个非常有趣的用例,既涉及到异常的使用,也涉及到异常的实现,就是微软Live Lab的Volta项目。(现在已经停止运营。)Volta的目标是通过一键实现Web应用的架构重构。也就是说,你可以通过在你的.NET代码上加一些[Browser]
或[DB]
属性,就能把一个单层的Web应用变成双层或三层的应用。为了实现这一点,.NET代码必须被转换成JavaScript源代码,显然是这样。
现在,你可以直接用JavaScript写一个完整的虚拟机,运行未修改的字节码。(基本上就是把CLR从C++移植到JavaScript。)实际上确实有这样的项目(比如HotRuby虚拟机),但这样做效率低下,而且与其他JavaScript代码的兼容性也不好。
因此,他们选择编写一个编译器,将CIL字节码编译成JavaScript源代码。然而,JavaScript缺少一些.NET所具备的特性(比如生成器、线程,还有两个异常模型并不完全兼容),更重要的是,它缺少一些编译器作者喜欢的特性(要么是GOTO
,要么是继续),这些特性可以用来实现上述缺失的功能。
不过,JavaScript确实有异常。因此,他们利用JavaScript异常来实现Volta继续,然后又用Volta继续来实现.NET异常、.NET生成器,甚至.NET托管线程(!!!)
所以,回到你最初的问题:
异常是如何在底层实现的?
用异常,讽刺的是!至少在这个特定的案例中是这样。
另一个很好的例子是Go邮件列表上的一些异常提案,它们使用Goroutines来实现异常(有点像并发协程和CSP进程的混合)。还有Haskell,它使用单子、惰性求值、尾调用优化和高阶函数来实现异常。一些现代CPU也支持异常的基本构建块(例如专为Azul Systems Java计算加速器设计的Vega-3 CPU)。