如何用C語言實現異常/狀況處理機制?
異常機制即Java或者類似風格的exception handling,使用try/catch(或類似的關鍵字)進行捕捉,然後進行處理或者繼續向上拋出異常;狀況機制主要指的是Common Lisp風格的做法,即除了拋出異常、捕捉異常以外,還有重啟(restart)機制,可以向下移動到某一個指定的代碼處理點,例如從錯誤處繼續運行等做法。
C本身不支持這樣的功能,只有和Java風格接近的setjmp/longjmp而已。如果要用C語言實現這兩種功能,那麼大致的思路是怎樣的呢?或者說有什麼適合的參考文獻嗎?
題主你厲害,你提了一個無比經典而又深藏功名的問題 -- "C語言編譯器如何實現try/catch"。
我記得VC從版本4.0開始就提供了
__try __catch __finally
這三個VC (注意不是C++)擴展關鍵詞。那時候我就被這個問題深深地困擾住了,
直到1997年, -- 那時候MS有一本電子雜誌叫 MSJ (MS Journal ),專門刊登一些奇奇怪怪的MS系技術問題 --MSJ上出了一篇 奇文A Crash Course on theDepths of Win32 Structured Exception Handling, MSJ January 1997,徹底地解剖了SEH的實現機制。(可惜,MSJ早已停刊了, 難道他們冥冥聽到有一個聲音說「不要發表一些奇奇怪怪的文章」 ?)後來有人基於那篇文章又搞了一個簡化版,並且進一步闡述了C++ try/catch的實現機制。
在這裡 How a C++ compiler implements exception handling
從中我們依然可以得知那篇MSJ奇文的精髓。雖然是2002年的文章,決不過時,請有興趣的C/C++猿一定不要錯過。最後,讓我們看看code project文章的最後,
Notes and References- As of this discussion, Visual Studio 7.0 is released. I compiled and tested the exception handling library primarily with VC++ 6.0 on Windows 2000 running on pentium processors. I also tested it with VC++ 5.0 and VC++ 7.0 beta release. There is small difference between 6.0 and 7.0. 6.0 first copies the exception (or its reference) on catch block"s frame and then performs stack unwinding before calling the catch block. 7.0 library first performs stack unwinding. The behavior of my library is similar to 6.0 library in this respect.
- See excellent article on MSDN by Matt Pietrek on structured exception handling.
- The compiler may not generate any exception related data for a function that does not have a try block and does not define any object that has a non-trivial destructor.
粗體部分就是我說的97年MSJ奇文(但是link已經點不開了),向傳說中的MSJ致敬!
Setjmp/longjmp 最大的局限,是 longjmp 跳轉時只是簡單的降低棧頂,而不對拋棄的棧內容做處理。所以,它不能對棧上的數據進行複雜的 clean-up 工作(類似 C++ 的 destructor)。不過,話說回來,C++ 的機制也很爛。
所以,歸根結底,還是 C 沒有 GC。而且,和 exception 配合的 GC,如果是 ref-counting,則需要在 stack unwinding 時進行分析減少相應的 ref-count。而 root-tracing 在 unwinding 時無需特別的 overhead。
這方面,Lua 做了一個比較好的例子。它從 pcall(調用 setjmp) 到 error(調用 longjmp) 中發生的所有資源分配,都對應到自己維護的一個 stack 上,所以可以在 longjmp 時回收資源。但是歸根結底,這個機制還是需要 Lua 有 GC 才得以實現,不過算是 C 中最 minimal 風格的利用 setjmp/longjmp 模擬異常的方案。
另外,C 本身的錯誤處理風格是 error code。這個風格和 exception 的爭論由來已久。我個人傾向於不使用 exception。參見: http://techsingular.net/?p=2153請參考C語言介面與實現 第四章 異常與斷言 一章 有利用setjmp/longjmp實現的簡單的try catch finally raise 的介面封裝
還可以參考:http://www.hpl.hp.com/techreports/Compaq-DEC/SRC-RR-40.pdf
如果你關注的是異常發生時資源的釋放,其實setjump/longjmp加上一個Conservative GC就基本可以達到Java的效果。目前最常見的Conservative GC實現是boehm GC。http://www.hpl.hp.com/personal/Hans_Boehm/gc/restart這事還真沒什麼現成方案。
用強類型的union (俗稱Algebraic Data Type)加Functor和Monad,完全代替辣雞Exception
如何設計一門語言(六)——exception和error code ←看我這篇博客你就什麼都明白了
在沒有垃圾回收的情況下,使用異常機制絕對會降低程序的質量。Symbian C++中,使用了一種Leave機制,是對C++中異常機制的包裝,而且因為Symbian系統運行在資源有限的嵌入式設備上的原因,棧空間很小,幾乎所有的數據都要放在堆上,所以Symbian C++中引入了清除棧機制來避免內存泄漏的問題。
Symbian C++中要求,剛創建的對象,在沒有保存到別的對象之前,應該立刻放到清除棧中保存,以便一旦產生Leave(異常),系統可以將清除棧中的對象銷毀,防止內存泄漏。於是這樣子代碼就好麻煩了,每一次都要寫類似的代碼
HSomeClass* anInstance = HSomeClass.NewL();CleanupStack::PushL(anInstance);
HSomeOtherClass* aNewInstance = HSomeOtherClass.NewL();CleanupStack::PushL(aNewInstance);.......CleanupStack::Pop();return;所以,如果你想在C中使用異常機制的話,那麼你需要:包裝把setjump包裝成TRAP宏,然後在實現一個清除棧。這個清除棧要分類型,一般的數據是一種,句柄又是一種,C中還不好實現類似IDispose之類的介面。另外,Symbian沒落的一個原因是,這樣的機制過於複雜,好多人都搞不懂,還是負責垃圾回收的Java比較方便,詳見另外一則答案 http://www.zhihu.com/question/20304750/answer/14686650 。正如馮冬君所言,setjmp/longjmp 的局限性很大,無法實現C++中棧迴繞並清理的操作。這樣的異常機制即使勉強實現,也會導致「既不安全,也不優雅」的設計。
如果你只是關心在C語言中是否有較可靠的異常機制可用,而不關心其具體實現,那麼有 Windows SEH 可供參考。需要注意的是, SEH 是微軟的擴展實現,僅在Windows 平台可用。
Windows SEH (Structured Exception Handling) 是一個操作系統實現的系統級異常處理機制,在 C 和(或) C++ 環境下均可用。
具體資料請參考:
(From MSDN)Structured Exception Handling (C/C++)Structured Exception Handling (Windows)(From wikipedia)Microsoft-specific exception handling mechanismsDon"t fight the language.
需要重啟機制的話,用多進程+IPC方案來做比模擬異常機制直接了當推薦閱讀:
※Lua 語言有哪些不足?
※為什麼Lua不支持大多數編程語言都有的continue,卻非得支持一般情況下用得很少的 repeat until ?
※為什麼很多編程語言用 end 作為區塊結束符,而放棄花括弧?
※unity中lua的開發工具?
※學習哪些 Functional programming language 能夠拓寬眼界,學到和其他編程範式明顯不一樣的東西?
TAG:C編程語言 | Lua | CommonLisp | 異常處理 |