軟體架構,流程類軟體指南
緣起
在軟體裡,有些地方是流程(flow),目的是把別人叫起來。至於對方做什麼,其實我們不在意。這種軟體寫起來有些注意事項
開機流程範例
一般會有個進入點,對每個模組初始化,長得像下面這樣
// init.c
extern void mod_a_init(u32 evt);
extern void mod_b_init(u32 evt);
typedef void (*func_fp)(u32);
void init_flow()
{
func_fp tbl[] = {
mod_a_init,
mod_b_init,
};
func_exec(tbl, sizeof(tbl)/sizeof(func_fp), EVT_BOOT);
}
流程的比喻:貨輪
海運的貨櫃是標準化的服務,以貨櫃作為運送的單位。上述流程要求大家參數一致,讓流程呼叫
void (*fp)(u32);
Function prototype 的用途
呼叫函數的約定(Calling Convention)定義在ABI(Application Binary Interface)裡。因為每次都會忘記,寫下這些詞彙當筆記
以範例的函數,第一個參數放r0,再用rcall指令跳進去。編譯器知道函數原型,比如幾個參數,各自的型別,才能正確呼叫
為什麼直接寫prototype,而不是吃對方的header file
不要吃模組的header,例如mod_a_intf.h,原因如下
- 軟體分層的邏輯,一般是上層應用吃底層的介面,沒有反著走的。init.c顯然是底層服務(例如長榮貨輪),他只要知道呼叫哪個函數,以及對方的參數正確(合乎貨櫃規格),不用全套知識(mod_a_intf.h)
- 防禦性設計,我們希望init.c固若金湯。如下面範例,mod_a_intf.h故意把SOME_OPTION轉ANOTHER_OPTION;假如init.c得看到SOME_OPTION,吃下去會出事
#if defined(SOME_OPTION)
#undef SOME_OPTION
#define ANOTHER_OPTION
#endif
所以為了邏輯結構正確,以及流程可靠,我們直接寫下呼叫什麼函數,而不是吃下整包介面
標準化的介面
上述的範例,先宣告函數指標陣列,再用func_exec()逐一執行,為什麼不直接呼叫?
mod_a_init(EVT_BOOT);
mod_b_init(EVT_BOOT);
一但允許直接呼叫,可能會有人這樣寫
mod_a_init(EVT_BOOT);
mod_b_init(EVT_BOOT);
mod_c_init();
基於維護的觀點,用func_exec()保證介面只有一種,直接用設計解決問題
進階設計:不給別人改C file
範例的init.c需要用戶來改,有潛在維護問題(如果改壞了...)。可以再切割註冊檔init_hook.h,讓用戶不用改任何init.c的邏輯
// init_hook.h
INIT_MOD(mod_a_init)
INIT_MOD(mod_b_init)
// init.c
#define INIT_MOD(f) extern void f(u32 evt);
#include "init_hook.h"
#undef INIT_MOD
void init_flow()
{
func_fp fp[] = {
#define INIT_MOD(f) f,
#include "init_hook.h"
#undef INIT_MOD
};
func_exec(fp, sizeof(fp)/ sizeof(func_fp), EVT_BOOT);
}
結語
這類的流程的軟體,編寫時有以上特殊的考量。以後應該還要解釋很多次,寫下來備用
留言