這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go語言中引入了一個新的關鍵字defer,個人認為這個文法關鍵字讓異常處理也變得得心應手許多,對改善代碼的可讀性和可維護性大有裨益,是典型的文法棒棒糖^_^。
像下面這種代碼(虛擬碼):
void foo() {
apply resource1;
retv = action1;
if not success
release resource1
apply resource2;
retv = action2;
if not success
release resource1
release resource2
}
有了defer後,代碼就變得優美多了。
void foo_with_defer() {
apply resource1;
defer (release_resource1)
retv = action1;
if not success
return
apply resource2;
defer (release_resource2)
retv = action2;
if not success
return
}
如果能在C語言中實現defer這樣的文法糖,那該多棒!是否可行呢?經過一段時間鑽研,找到一個不那麼美的實現方法,約束也很多,也不甚嚴謹, 談不上什麼可移植性,切不可用到產品環境,權當一種探討罷了。
Go中defer的語義大致是這樣的:
* 在使用defer的函數退出前,defer後面的函數將會被執行;
* 如果一個函數內有多個defer,那麼defer按後進先出(LIFO)的順行執行;
* 即使發生Panic,defer依然可以得到執行
最後一個比較難於類比,這裡僅先嘗試前兩個語義。下面從設計思路說起。
* “借東風”
要想類比defer,首先要考慮的一點那就是defer後的語句是在函數return之前執行的。在標準C中,我們無任何舉措可以實現這些。要在 C中實現defer,勢必要借用一些編譯器擴充特性,比如Gcc的擴充。這裡實驗所使用的編譯器是Gcc(4.6.3 (Ubuntu 12.04))。Gcc擴充支援-finstrument-functions編譯選項,該選項可以在函數執行前後插入一段運行代碼。在之前寫過的一篇名 為“為函數添加enter和exit級trace”的文章中對此有較為詳細的說明,這裡我們還要用到這個擴充特性。
* 偷天換日
如果完全模仿Go的文法,在C中使用defer,大致是這樣一種形式:
void foo(void) {
FILE * fp = NULL;
fp = fopen("foo.txt", "r");
if (!fp) return;
defer(fclose(fp));
/* use fp */
… …
return;
}
但C畢竟是C,一門靜態編譯型語言,我們如何將fclose(fp)這個資訊傳遞給編譯器自動插入的代碼中呢?在C語言中,幾乎沒有手段獲得函 數的元資訊以及運行時參數資訊,並再通過這些資訊重新調用和執行該函數。我們得“想招”將這些資訊儲存起來。
大家知道C語言中的函數,比如這裡的fclose,其實是一個函數起始地址;如果我們知道函數地址或又叫函數指標,再加上函數的參數,我們就可以 拼湊在一起執行該函數了。但理論上來說,函數指標也是有類型的,比如:
typedef int (*FUNC_POINTER)(int, int);
這個函數指標類型可以用來執行諸如:int foo(int a, int b)這樣的函數,比如:
FUNC_POINTER fp = foo;
fp(1, 2);
但defer後面執行的函數千差萬別,我們如何能夠得知函數對應的函數指標類型呢?用void*儲存?比如:
void *p = foo;
p(1, 2);
編譯器會給你一個嚴重錯誤!p不是函數指標,不能這麼用。那我們如何能讓編譯器知道這個指標是一個可調用的函數指標呢?我們試試來定義一個“通用 的函數指標”:
typedef void (*defer_func)();
沒有傳回值,沒有參數,這樣的函數指標能否執行foo這樣的函數呢?答案是可以的,但不是那麼完美。至少你不會得到傳回值。這麼做有兩點考慮:
a) 至少可以讓編譯器知道這是一個函數指標,可以被用來執行函數。
b) 通常我們並不關心defer後面函數的傳回值。
c) 參數列表的不同至少目前可以逃過編譯器的錯誤檢查,至多給個Warning。
函數指標的問題暫時算是有著落了,那參數怎麼辦?也就是說defer(fclose(fp))中的fp如何儲存下來呢?如果在C中真的使用 defer(fclose(p))這種形式的文法,那麼我是砸破腦袋也想不出啥招了!因此我們應該重新設計一下C中的defer應該如何使用?我 們用下面的文法來替代:
defer(fclose, 1, p);
fclose是函數起始地址,1是參數個數,p則是傳給fclose的參數。這樣fclose和p都可以單獨分離出來儲存了。但是還是那句 話:defer後面可以執行的函數千萬種,哪能窮盡?怎麼才能表示成一種通用的方式儲存參數呢?回想一下自己在編碼過程中用於釋放資源的那幾類函 數,無非就是關閉檔案、關閉檔案描述符(包括socket)、釋放記憶體等,這些函數傳遞的參數不是指標就是整型數,少有傳浮點類型或將一個自訂 結構體以傳值的方式傳入的。我們不妨再次嘗試一次“偷天換日” – 用void*儲存整型參數或任意指標型別參數。當然其約束就像剛才所說的那些。不過對付大多數資源釋放函數而言,應該是足夠的了。至於將參數個數也作為一 個固定參數放入defer中,也是鑒於目前無法通過操作可變個數參數列表相關宏來獲得參數數量。
最後一個問題。由於被defer的函數的參數個數不定。defer無法將可變個數參數重組後傳給被defer的函數。因此目前暫只能通過一種“醜陋”的方式來實現。範例中最多隻支援兩個參數的被defer函數。
* 範例
首先看看我們的examples的主函數檔案main.c。
#include
#include
#include "defer.h"
int bar(int a, char *s) {
printf("a = [%d], s = [%s]\n", a, s);
}
int main() {
FILE *fp = NULL;
fp = fopen("main.c", "r");
if (!fp) return;
defer(fclose, 1, fp);
int *p = malloc(sizeof(*p));
if (!p) return;
defer(free, 1, p);
defer(bar, 2, 13, "hello");
return 0;
}
從這裡我們可以看到defer的用法,但這不是重點,重點是實現。
有了上面的一些設計思路的闡述,下面的代碼也就不難理解了。核心是defer.c。
/* defer.h */
typedef void (*defer_func)();
struct zero_params_func_ctx {
defer_func df;
};
struct one_params_func_ctx {
defer_func df;
void *p1;
};
struct two_params_func_ctx {
defer_func df;
void *p1;
void *p2;
};
struct defer_func_ctx {
int params_count;
union {
struct zero_params_func_ctx zp;
struct one_params_func_ctx op;
struct two_params_func_ctx tp;
} ctx;
};
void stack_push(struct defer_func_ctx *ctx);
struct defer_func_ctx* stack_pop();
int stack_top();
/* defer.c */
struct defer_func_ctx ctx_stack[10];
int top_of_stack = 0; /* stack top from 1 to 10 */
void stack_push(struct defer_func_ctx *ctx) {
if (top_of_stack >= 10) {
return;
}
ctx_stack[top_of_stack] = *ctx;
top_of_stack++;
}
struct defer_func_ctx* stack_pop() {
if (top_of_stack == 0) {
return NULL;
}
top_of_stack–;
return &ctx_stack[top_of_stack];
}
int stack_top() {
return top_of_stack;
}
void defer(defer_func fp, int arg_count, …) {
va_list ap;
va_start(ap, arg_count);
struct defer_func_ctx ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.params_count = arg_count;
if (arg_count == 0) {
ctx.ctx.zp.df = fp;
} else if (arg_count == 1) {
ctx.ctx.op.df = fp;
ctx.ctx.op.p1 = va_arg(ap, void*);
} else if (arg_count == 2) {
ctx.ctx.tp.df = fp;
ctx.ctx.tp.p1 = va_arg(ap, void*);
ctx.ctx.tp.p2 = va_arg(ap, void*);
ctx.ctx.tp.df(ctx.ctx.tp.p1, ctx.ctx.tp.p2);
}
va_end(ap);
stack_push(&ctx);
}
多個defer的FIFO調用順序用一個固定大小的stack來實現。這裡只是為了示範,所以stack實現的簡單和固定些。
組裝後的函數在funcexit.c中執行:
extern struct defer_func_ctx ctx_stack[10];
__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void *this_fn, void *call_site) {
struct defer_func_ctx *ctx = NULL;
while ((ctx = stack_pop()) != NULL) {
if (ctx->params_count == 0) {
ctx->ctx.zp.df();
} else if (ctx->params_count == 1) {
ctx->ctx.op.df(ctx->ctx.op.p1);
} else if (ctx->params_count == 2) {
ctx->ctx.tp.df(ctx->ctx.tp.p1, ctx->ctx.tp.p2);
}
}
}
最後我們將defer.c、funcexit.c編譯成一個.so檔案:
gcc -g -fPIC -shared -o libcdefer.so funcexit.c defer.c
而編譯main.c的方法如下:
gcc -g main.c -o main -finstrument-functions -I ../lib -L ../lib -lcdefer
一切OK後,先將libcdefer.so放在main同級目錄下,執行main即可。
$> ./main
a = [13], s = [hello]
具體代碼已經傳至這裡(trunk/cdefer),需要的童鞋可自行下載。
2013, bigwhite. 著作權.