一個Go和C++多用途工程專案的模型研究
持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第4天,點選檢視活動詳情
本文探討一個使用Go語言和C++語言實現的多用途工程專案的模型,該工程可適用於一些實際工作環境,且能提高開發效率,降低維護成本。
問題提出
筆者負責的一個工程的測試程式,需要有互動環境,類似於 Linux 命令列那樣,比如修改引數A執行一次測試,修改引數B執行一次測試,這類針對測試的使用,用命令列的方式是最快捷的,因為不需要重新初始化。除此外,也會做一些核算驗證工作,但不需要互動,單獨執行即可,比如執行一次全量資料的核算,需要耗時1小時(即使是32核心/64GB記憶體伺服器亦要如此久),則需在後臺執行,而不能用命令列方式,如果網路斷開則會前功盡棄。另外還需有動態庫以提供其它程式使用,當然,“其它程式”亦由筆者負責。
綜上,這個工程最終要產生三種檔案:兩個可執行程式檔案(包括命令列版和單獨版)以及一個動態庫檔案。為能夠複用程式碼,減少維護成本——主要是指筆者的維護成本,做到模組化但又相對獨立,需從較高層面考慮整體的架構。經較長一段時間的探索,以目前的技術水平,以目前的實踐經驗,提出本文所述之模型。
層架圖
層架圖如下圖所示。
縱向看,可執行檔案foobar和foobar_allone分別有不同的執行流程,而foobar_allone可以認為是動態庫libfoobar.so的整合呼叫。
橫向看,雖然都有初始化、執行、退出等主要流程,但或多或少有差異。最終執行到的函式,則在全域性命令結構體中,在該結構體中,定義了不同的命令名稱及對應的執行函式。
從結果上看,不管哪一種可執行檔案形式,真正執行操作的是命令結構體中的函式,只是過程稍有不同而已,最終殊途同歸。
開發相關
上述層架圖涉及的內容,均在同一個工程中,使用不同的適當數量的目錄,不同字首的檔名稱,這樣做方便專案管理。編碼風格上,使用不同風格的函式以示區別。對外提供的函式,使用大小寫形式,如FoobarInit
,而內部函式,一般使用小寫及下劃線形式,比如實際命令的執行函式一般為do_xx
。
工程概述
- 命令列版本適用於互動場合;單獨版本適用於單獨執行場合。
- 不同形式程式,最終本質還是呼叫不同命令對應的函式。
- 網頁版本由go語言載入so庫,再呼叫FoobarXX函式。
整個工程使用 Makefile 編譯,對外輸出檔案為foobar、foobar_allone、libfoobar.so,其中前兩者內容完全一致,通過檔名稱實現不同的程式功能。該功能實現不復雜,即在 main 函式中根據執行程式的名稱,從而呼叫不同的模組入口函式,以往文章有涉及,有興趣可自行查閱。
命令互動的實現,沿用筆者之前實現的模組程式碼,也有相應的文章。命令結構體定義示例如下:
cmd_tbl_t my_cmd_table[] =
{
{"help", CONFIG_SYS_MAXARGS, do_help_default, "print help info."},
{"?", CONFIG_SYS_MAXARGS, do_help_default, "print help info."},
{"exit", 1, do_exit, "quit program"},
{"quit", 1, do_exit, "quit program"},
{"show", 1, do_show, "show param"},
};
其中,命令名稱與實現函式一一對應,如 exit 命令,對應實現函式為do_exit
。
設計說明
由於功能的實現體現在命令中,因為能夠在一定程度上解耦,要新加功能,直接新增命令即可。
設定全域性引數,gConfig結構體,引數可以在命令列中修改。
設定日誌列印標誌 showlog,根據等級不同列印不同日誌。
單獨版本支援多命令輸入,與命令列版本相似。以下2種示例,效果完全一樣。
``` 命令版本: ./foobar
set showlog=1 test 單獨版本: ./foobar_allone "set showlog=1; test" ```
動態庫:
```
ifdef __cplusplus
extern "C" {
endif
typedef struct InParam_s{ char logstr; } InParam; typedef struct OutParam_s{ char logstr; } OutParam; int CalFeeRun(InParam inparam, OutParam outparam);
ifdef __cplusplus
}
endif
```
Go 和 C++ 互動
互動方式
筆者除了在終端執行外,還需要用網頁做視覺化,這樣方便給領導展示,也可給其它同事使用。Go 實現 web 服務比較方便,有很多現成的庫,筆者實際使用 gin 框架。但底層用到的庫還是C++,這樣就涉及兩種語言的互動了。
前面已經提供了動態庫so檔案,動態庫是C++語言編寫,但 Go 只支援 C 語言,因此要解決在 Go 中如何使用 C 封裝動態庫的呼叫。
在資料傳輸上,設計成只有字串形式而不是結構體。實現簡單,能自由定製內容。
具體看,使用C.CString(instr)
將 Go 的字串轉換成 C 語言字串,再呼叫。呼叫結束後,將返回字串用outstr = C.GoString(outParam.logstr)
轉換成 Go 字串。
// 結構體指標,傳入傳出
int CalFeeRun(InParam* inparam, OutParam* outparam)
{
typedef int (*ptr)(InParam*, OutParam*);
ptr fptr = (ptr)dlsym(g_sohandle, "CalFeeRun");
return (*fptr)(inparam, outparam);
}
呼叫示例:
var inParam C.InParam
var outParam C.OutParam
inParam.logstr = C.CString(instr)
ret := C.CalFeeRun(&inParam, &outParam)
if ret < 0 {
klog.Printf("run cal cmd failed\n")
return outstr
}
outstr = C.GoString(outParam.logstr)
// 傳出引數為靜態緩衝區,不在這裡釋放
defer C.free(unsafe.Pointer(inParam.logstr))
資料傳遞
有時資料量較大,可能會溢位或者不完整,前者,可限制固定大小的緩衝區,後者,則可使用crc32或其它方式校驗資料。由於較簡單,因此略去不談。
小結
本文根據筆者需求,提出一種方案,不涉及具體的程式碼實現。從實際實施效果看,還是不錯的,當然,因為所有這些工程均是筆者一人維護,是否經得起考驗,那是後來的事了。