一個Go和C++多用途工程專案的模型研究

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第4天,點選檢視活動詳情

本文探討一個使用Go語言和C++語言實現的多用途工程專案的模型,該工程可適用於一些實際工作環境,且能提高開發效率,降低維護成本。

問題提出

筆者負責的一個工程的測試程式,需要有互動環境,類似於 Linux 命令列那樣,比如修改引數A執行一次測試,修改引數B執行一次測試,這類針對測試的使用,用命令列的方式是最快捷的,因為不需要重新初始化。除此外,也會做一些核算驗證工作,但不需要互動,單獨執行即可,比如執行一次全量資料的核算,需要耗時1小時(即使是32核心/64GB記憶體伺服器亦要如此久),則需在後臺執行,而不能用命令列方式,如果網路斷開則會前功盡棄。另外還需有動態庫以提供其它程式使用,當然,“其它程式”亦由筆者負責。

綜上,這個工程最終要產生三種檔案:兩個可執行程式檔案(包括命令列版和單獨版)以及一個動態庫檔案。為能夠複用程式碼,減少維護成本——主要是指筆者的維護成本,做到模組化但又相對獨立,需從較高層面考慮整體的架構。經較長一段時間的探索,以目前的技術水平,以目前的實踐經驗,提出本文所述之模型。

層架圖

層架圖如下圖所示。

image-20220304104419330.png

縱向看,可執行檔案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或其它方式校驗資料。由於較簡單,因此略去不談。

小結

本文根據筆者需求,提出一種方案,不涉及具體的程式碼實現。從實際實施效果看,還是不錯的,當然,因為所有這些工程均是筆者一人維護,是否經得起考驗,那是後來的事了。