原文:Exercise 20: Zed's Awesome Debug Macroshtml
譯者:飛龍git
在C中有一個永恆的問題,它伴隨了你很長時間,然而在這個練習我打算使用一系列我開發的宏來解決它。到如今爲止你都不知道它們的強大之處,因此你必須使用它們,總有一天你會來找我說,「Zed,這些調試宏真是太偉大了,我應該把個人第一個孩子的出生歸功於你,由於你治好了我十年的心臟病,而且打消了我數次想要自殺的念頭。真是要謝謝你這樣一個好人,這裏有一百萬美圓,和Leo Fender設計的Snakehead Telecaster電吉他的原型。」程序員
是的,它們的確很強大。github
幾乎每一個編程語言中,錯誤處理都很是難。有些語言儘量試圖避免錯誤這個概念,而另外一些語言發明了複雜了控制結構,好比異常來傳遞錯誤狀態。固然的錯誤大可能是由於程序員嘉定錯誤不會發生,而且這一樂觀的思想影響了他們所用和所創造的語言。數據庫
C經過返回錯誤碼或設置全局的errno
值來解決這些問題,而且你須要檢查這些值。這種機制能夠檢查現存的複雜代碼中,你執行的東西是否發生錯誤。當你編寫更多的C代碼時,你應該按照下列模式:編程
調用函數。安全
若是返回值出現錯誤(每次都必須檢查)。服務器
清理建立的全部資源。app
打印出全部可能有幫助的錯誤信息。編程語言
這意味着對於每個函數調用(是的,每一個函數)你均可能須要多編寫3~4行代碼來確保它正常功能。這些還不包括清理你到目前建立的全部垃圾。若是你有10個不一樣的結構體,3個方式。和一個數據庫連接,當你發現錯誤時你應該寫額外的14行。
以前這並非個問題,由於發生錯誤時,C程序會像你之前作的那樣直接退出。你不須要清理任何東西,由於OS會爲你自動去作。然而如今不少C程序須要持續運行數週、數月或者數年,而且須要優雅地處理來自於多種資源的錯誤。你並不能僅僅讓你的服務器在首次運行就退出,你也不能讓你寫的庫使使用它的程序退出。這很是糟糕。
其它語言經過異常來解決這個問題,可是這些問題也會在C中出現(其它語言也同樣)。在C中你只可以返回一個值,可是異常是基於棧的返回系統,能夠返回任意值。C語言中,嘗試在棧上模擬異常很是困難,而且其它庫也不會兼容。
我使用的解決方案是,使用一系列「調試宏」,它們在C中實現了基本的調試和錯誤處理系統。這個系統很是易於理解,兼容於每一個庫,而且使C代碼更加健壯和簡潔。
它經過實現一系列轉換來處理錯誤,任什麼時候候發生了錯誤,你的函數都會跳到執行清理和返回錯誤代碼的「error:」區域。你可使用check
宏來檢查錯誤代碼,打印錯誤信息,而後跳到清理區域。你也可使用一系列日誌函數來打印出有用的調試信息。
我如今會向你展現你目前所見過的,最強大且卓越的代碼的所有內容。
#ifndef __dbg_h__ #define __dbg_h__ #include <stdio.h> #include <errno.h> #include <string.h> #ifdef NDEBUG #define debug(M, ...) #else #define debug(M, ...) fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__) #endif #define clean_errno() (errno == 0 ? "None" : strerror(errno)) #define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define log_warn(M, ...) fprintf(stderr, "[WARN] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define log_info(M, ...) fprintf(stderr, "[INFO] (%s:%d) " M "\n", __FILE__, __LINE__, ##__VA_ARGS__) #define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; } #define sentinel(M, ...) { log_err(M, ##__VA_ARGS__); errno=0; goto error; } #define check_mem(A) check((A), "Out of memory.") #define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; } #endif
是的,這就是所有代碼了,下面是它每一行所作的事情。
dbg.h:1-2
防止意外包含屢次的保護措施,你已經在上一個練習中見過了。
dbg.h:4-6
包含這些宏所需的函數。
dbg.h:8
#ifdef
的起始,它可讓你從新編譯程序來移除全部調試日誌信息。
dbg.h:9
若是你定義了NDEBUG
以後編譯,沒有任何調試信息會輸出。你能夠看到#define debug()
被替換爲空(右邊沒有任何東西)。
dbg.h:10
上面的#ifdef
所匹配的#else
。
dbg.h:11
用於替代的#define debug
,它將任何使用debug("format", arg1, arg2)
的地方替換成fprintf
對stderr
的調用。許多程序員並不知道,可是你的確能夠建立列斯與printf
的可變參數宏。許多C編譯器(其實是C預處理器)並不支持它,可是gcc能夠作到。這裏的魔法是使用##__VA_ARGS__
,意思是將剩餘的全部額外參數放到這裏。同時也要注意,使用了__FILE__
和__LINE__
來獲取當前fine:line
用於調試信息。這會很是有幫助。
dbg.h:12
#ifdef
的結尾。
dbg.h:14
clean_errno
宏用於獲取errno
的安全可讀的版本。中間奇怪的語法是「三元運算符」,你會在後面學到它。
dbg.h:16-20
log_err
,log_warn
和log_info
宏用於爲最終用戶記錄信息。它們相似於debug
但不能被編譯。
dbg.h:22
到目前爲止最棒的宏。check
會保證條件A
爲真,不然會記錄錯誤M
(帶着log_err
的可變參數),以後跳到函數的error:
區域來執行清理。
dbg.h:24
第二個最棒的宏,sentinel
能夠放在函數的任何不該該執行的地方,它會打印錯誤信息而且跳到error:
標籤。你能夠將它放到if-statements
或者switch-statements
的不應被執行的分支中,好比default
。
dbg.h:26
簡寫的check_mem
宏,用於確保指針有效,不然會報告「內存耗盡」的錯誤。
dbg.h:28
用於替代的check_debug
宏,它仍然會檢查並處理錯誤,尤爲是你並不想報告的廣泛錯誤。它裏面使用了debug
代替log_err
來報告錯誤,因此當你定義了NDEBUG
,它仍然會檢查而且發生錯誤時跳出,可是不會打印消息了。
下面是一個例子,在一個小的程序中使用了dbg.h
的全部函數。這實際上並無作什麼事情,知識想你演示瞭如何使用每一個宏。咱們將在接下來的全部程序中使用這些宏,全部要確保理解了如何使用它們。
#include "dbg.h" #include <stdlib.h> #include <stdio.h> void test_debug() { // notice you don't need the \n debug("I have Brown Hair."); // passing in arguments like printf debug("I am %d years old.", 37); } void test_log_err() { log_err("I believe everything is broken."); log_err("There are %d problems in %s.", 0, "space"); } void test_log_warn() { log_warn("You can safely ignore this."); log_warn("Maybe consider looking at: %s.", "/etc/passwd"); } void test_log_info() { log_info("Well I did something mundane."); log_info("It happened %f times today.", 1.3f); } int test_check(char *file_name) { FILE *input = NULL; char *block = NULL; block = malloc(100); check_mem(block); // should work input = fopen(file_name,"r"); check(input, "Failed to open %s.", file_name); free(block); fclose(input); return 0; error: if(block) free(block); if(input) fclose(input); return -1; } int test_sentinel(int code) { char *temp = malloc(100); check_mem(temp); switch(code) { case 1: log_info("It worked."); break; default: sentinel("I shouldn't run."); } free(temp); return 0; error: if(temp) free(temp); return -1; } int test_check_mem() { char *test = NULL; check_mem(test); free(test); return 1; error: return -1; } int test_check_debug() { int i = 0; check_debug(i != 0, "Oops, I was 0."); return 0; error: return -1; } int main(int argc, char *argv[]) { check(argc == 2, "Need an argument."); test_debug(); test_log_err(); test_log_warn(); test_log_info(); check(test_check("ex20.c") == 0, "failed with ex20.c"); check(test_check(argv[1]) == -1, "failed with argv"); check(test_sentinel(1) == 0, "test_sentinel failed."); check(test_sentinel(100) == -1, "test_sentinel failed."); check(test_check_mem() == -1, "test_check_mem failed."); check(test_check_debug() == -1, "test_check_debug failed."); return 0; error: return 1; }
要注意check
是如何使用的,而且當它爲false
時會跳到error:
標籤來執行清理。這一行讀做「檢查A是否爲真,不爲真就打印M並跳出」。
當你執行這段代碼而且向第一個參數提供一些東西,你會看到:
$ make ex20 cc -Wall -g -DNDEBUG ex20.c -o ex20 $ ./ex20 test [ERROR] (ex20.c:16: errno: None) I believe everything is broken. [ERROR] (ex20.c:17: errno: None) There are 0 problems in space. [WARN] (ex20.c:22: errno: None) You can safely ignore this. [WARN] (ex20.c:23: errno: None) Maybe consider looking at: /etc/passwd. [INFO] (ex20.c:28) Well I did something mundane. [INFO] (ex20.c:29) It happened 1.300000 times today. [ERROR] (ex20.c:38: errno: No such file or directory) Failed to open test. [INFO] (ex20.c:57) It worked. [ERROR] (ex20.c:60: errno: None) I shouldn't run. [ERROR] (ex20.c:74: errno: None) Out of memory.
看到check
失敗以後,它是如何打印具體的行號了嗎?這會爲接下來的調試工做節省時間。同時也觀察errno
被設置時它如何打印錯誤信息。一樣,這也能夠節省你調試的時間。
如今我會想你簡單介紹一些預處理器的工做原理,讓你知道這些宏是如何工做的。我會拆分dbg.h
中阿最複雜的宏而且讓你運行cpp
來讓你觀察它其實是如何工做的。
假設我有一個函數叫作dosomething()
,執行成功是返回0,發生錯誤時返回-1。每次我調用dosomething
的時候,我都要檢查錯誤碼,因此我將代碼寫成這樣:
int rc = dosomething(); if(rc != 0) { fprintf(stderr, "There was an error: %s\n", strerror()); goto error; }
我想使用預處理器作的是,將這個if
語句封裝爲更可讀而且便於記憶的一行代碼。因而可使用這個check
來執行dbg.h
中的宏所作的事情:
int rc = dosomething(); check(rc == 0, "There was an error.");
這樣更加簡潔,而且剛好解釋了所作的事情:檢查函數是否正常工做,若是沒有就報告錯誤。咱們須要一些特別的預處理器「技巧」來完成它,這些技巧使預處理器做爲代碼生成工具更加易用。再次看看check
和log_err
宏:
#define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }
第一個宏,log_err
更簡單一些,只是將它本身替換爲fprintf
對stderr
的調用。這個宏惟一的技巧性部分就是在log_err(M, ...)
的定義中使用...
。它所作的是讓你向宏傳入可變參數,從而傳入fprintf
須要接收的參數。它們是如何注入fprintf
的呢?觀察末尾的##__VA_ARGS__
,它告訴預處理器將...
所在位置的參數注入到fprintf
調用的相應位置。因而你能夠像這樣調用了:
log_err("Age: %d, name: %s", age, name);
age, name
參數就是...
所定義的部分,這些參數會被注入到fprintf
中,輸出會變成:
fprintf(stderr, "[ERROR] (%s:%d: errno: %s) Age %d: name %d\n", __FILE__, __LINE__, clean_errno(), age, name);
看到末尾的age, name
了嗎?這就是...
和##__VA_ARGS__
的工做機制,在調用其它變參宏(或者函數)的時候它會起做用。觀察check
宏調用log_err
的方式,它也是用了...
和##__VA_ARGS__
。這就是傳遞整個printf
風格的格式字符串給check
的途徑,它以後會傳給log_err
,兩者的機制都像printf
同樣。
下一步是學習check
如何爲錯誤檢查構造if
語句,若是咱們剖析log_err
的用法,咱們會獲得:
if(!(A)) { errno=0; goto error; }
它的意思是,若是A
爲假,則重置errno
而且調用error
標籤。check
宏會被上述if
語句·替換,因此若是咱們手動擴展check(rc == 0, "There was an error.")
,咱們會獲得:
if(!(rc == 0)) { log_err("There was an error."); errno=0; goto error; }
在這兩個宏的展開過程當中,你應該瞭解了預處理器會將宏替換爲它的定義的擴展版本,而且遞歸地來執行這個步驟,擴展宏定義中的宏。預處理器是個遞歸的模板系統,就像我以前提到的那樣。它的強大來源於使用參數化的代碼來生成整個代碼塊,這使它成爲便利的代碼生成工具。
下面只剩一個問題了:爲何不像die
同樣使用函數呢?緣由是須要在錯誤處理時使用file:line
的數值和goto
操做。若是你在函數在內部執行這些,你不會獲得錯誤真正出現位置的行號,而且goto
的實現也至關麻煩。
另外一個緣由是,若是你編寫原始的if
語句,它看起來就像是你代碼中的其它的if
語句,因此它看起來並不像一個錯誤檢查。經過將if
語句包裝成check
宏,就會使這一錯誤檢查的邏輯更清晰,而不是主控制流的一部分。
最後,C預處理器提供了條件編譯部分代碼的功能,因此你能夠編寫只在構建程序的開發或調試版本時須要的代碼。你能夠看到這在dbg.h
中已經用到了,debug
宏的主體部分只被編譯器用到。若是沒有這個功能,你須要多出一個if
語句來檢查是否爲「調試模式」,也浪費了CPU資源來進行沒有必要的檢查。
將#define NDEBUG
放在文件頂端來消除全部調試信息。
撤銷上面添加的一行,並在MakeFile
頂端將-D NDEBUG
添加到CFLAGS
,以後從新編譯來達到一樣效果。
修改日誌宏,使之包含函數名稱和file:line
。