這是我參與8月更文挑戰的第5天,活動詳情檢視:8月更文挑戰
系列文章見:
- [第四天] GDB除錯指南:C++中如何除錯生產環境的程式?
- [第三天] IM敏感詞演算法原理和實現
- [第二天] 現代IM架構研究筆記(一):瓜子IM和OpenIM
- [第一天] Golang中如何正確的使用sarama包操作Kafka?
CGO是什麼
簡單點來講,如果要呼叫C++,C寫的庫(動態庫,靜態庫),那麼就需要使用Cgo。其他情況下一般用不到,只需要知道Go能呼叫C就行了,當然C也可以回撥到Go中。
使用Cgo有2種姿勢:
- 直接在go中寫c程式碼
- go呼叫so動態庫(c++要用extern “c”匯出)
為了熟悉CGO,我們先介紹第一種方法,直接在Go中寫C程式碼。
入門,直接在Go中寫C程式碼
引用:Command cgo
首先,通過import “C”匯入偽包(這個包並不真實存在,也不會被Go的compile元件見到,它會在編譯前被CGO工具捕捉到,並做一些程式碼的改寫和樁檔案的生成)
import "C"
然後,Go 就可以使用C的變數和函式了, C.size_t 之類的型別、諸如 C.stdout 之類的變數或諸如 C.putchar 之類的函式。
func main(){ cInt := C.int(1) // 使用C中的int型別 fmt.Println(goInt) ptr := C.malloc(20) // 呼叫C中的函式 fmt.Println(ptr) // 列印指標地址 C.free(ptr) // 釋放,需要 #include <stdlib.h> }
如果“C”的匯入緊跟在註釋之前,則該註釋稱為序言。例如:
// #include <stdio.h> /* #include <errno.h> */ import "C"
序言可以包含任何 C 程式碼,包括函式和變數宣告和定義。然後可以從 Go 程式碼中引用它們,就好像它們是在包“C”中定義的一樣。可以使用序言中宣告的所有名稱,即使它們以小寫字母開頭。例外:序言中的靜態變數不能從 Go 程式碼中引用;靜態函式是允許的。
所以,你可以直接在/**/裡面寫C程式碼(注意,C++不行!):
package main /* int add(int a,int b){ return a+b; } */ import "C" import "fmt" func main() { a, b := 1, 2 c := C.add(a, b) }
編譯下,會出現下面的問題( fmt.Println(C.add(1, 2)) 能編譯通過,思考下為什麼? ):
./main.go:20:12: cannot use a (type int) as type _Ctype_int in argument to _Cfunc_add ./main.go:20:12: cannot use b (type int) as type _Ctype_int in argument to _Cfunc_add
為什麼呢?因為C沒有辦法使用Go的型別,必須先轉換成CGO型別才可以,改成這樣就行了:
func main() { cgoIntA, cgoIntB := C.int(1), C.int(2) c := C.add(cgoIntA, cgoIntB) fmt.Println(c) }
執行後輸出:
3
CGO基礎型別
就像上面的程式碼一樣,Go沒有辦法直接使用C的東西,必須先轉換成CGO型別,下面是一個基礎型別對應表。
C型別 | CGO型別 | GO型別 |
---|---|---|
char | C.char | byte |
signed char | C.schar | int8 |
unsigned char | C.uchar | uint8 |
short | C.short | int16 |
unsigned short | C.ushort | uint16 |
int | C.int | int32 |
unsigned int | C.uint | uint32 |
long | C.long | int32 |
unsigned long | C.ulong | uint32 |
long long int | C.longlong | int64 |
unsigned long long int | C.ulonglong | uint64 |
float | C.float | float32 |
double | C.double | float64 |
size_t | C.size_t | uint |
如果直接在C中 #include <stdint.h>
,則型別關係就比較一致了,例如:
C型別 | CGO型別 | GO型別 |
---|---|---|
int8_t | C.int8_t | int8 |
int16_t | C.int16_t | int16 |
uint32_t | C.uint32_t | uint32 |
uint64_t | C.uint64_t | uint64 |
字串、陣列和函式呼叫
那麼,在Go要如何傳遞字串、位元組陣列以及指標? CGO的C虛擬包提供了以下一組函式,用於Go語言和C語言之間陣列和字串的雙向轉換:
// Go string to C string // The C string is allocated in the C heap using malloc. // It is the caller's responsibility to arrange for it to be // freed, such as by calling C.free (be sure to include stdlib.h // if C.free is needed). func C.CString(string) *C.char // Go []byte slice to C array // The C array is allocated in the C heap using malloc. // It is the caller's responsibility to arrange for it to be // freed, such as by calling C.free (be sure to include stdlib.h // if C.free is needed). func C.CBytes([]byte) unsafe.Pointer // C string to Go string func C.GoString(*C.char) string // C data with explicit length to Go string func C.GoStringN(*C.char, C.int) string // C data with explicit length to Go []byte func C.GoBytes(unsafe.Pointer, C.int) []byte
字串,可以通過C.CString()函式(別忘記通過free釋放):
// 通過C.CString,這裡會發生記憶體拷貝,cgo通過malloc重新開闢了一塊空間,使用完需要釋放,否則記憶體洩露 imagePath := C.CString("a.png") defer C.free(unsafe.Pointer(imagePath))
位元組陣列,直接使用go的陣列,然後強制轉換即可:
// 只能使用陣列,無法使用切片用作緩衝區給C使用 var buffer [20]byte // &buffer[0]: 陣列在記憶體中是連續儲存的,取首地址 // unsafe.Pointer():轉換為非安全指標,型別是*unsafe.Pointer // (*C.char)():再強轉一次 cBuffer := (*C.char)(unsafe.Pointer(&buffer[0]))
對應型別的指標,直接使用Cgo型別,然後&取地址即可:
bufferLen := C.int(20) cPoint := &bufferLen // cPoint在CGO中是*C.int型別,在C中是*int型別。
假如ocr識別函式如下:
int detect(const char* image_path, char * out_buffer, int *len);
有3個引數:
- image_path:指示了要識別的圖片路徑。
- out_buffer:識別到的文字輸出到這裡,是一個char位元組陣列。
- len:指示輸出位元組緩衝區大小,呼叫成功後,值變成字串長度,便於外界讀取。
在go中呼叫方式如下:
imagePath := C.CString("a.png") defer C.free(unsafe.Pointer(imagePath)) var buffer [20]byte bufferLen := C.int(20) cInt := C.detect(imagePath, (*C.char)(unsafe.Pointer(&buffer[0])), &bufferLen) if cInt == 0 { fmt.Println(string(buffer[0:bufferLen])) }
分離Go和C程式碼
為了簡化程式碼,我們可以把C的程式碼放到xxx.h和xxx.c中實現。
有以下結構:
├── hello.c ├── hello.h └── main.go
hello.h的內容:
#include <stdio.h> void sayHello(const char* text);
hello.c:
#include "hello.h" void sayHello(const char* text){ printf("%s", text); }
main.go中呼叫hello.h中的函式:
#include "hello.h" import "C" // 必須放在匯入c程式碼活標頭檔案的註釋後面,否則不生效 func main() { cStr := C.CString("hello from go") defer C.free(unsafe.Pointer(cStr)) C.sayHello(cStr) }
常用cgo編譯指令
如果我們把h和c檔案放到其他目錄,則編譯會報錯:
├── main.go └── mylib ├── hello.c └── hello.h
Undefined symbols for architecture x86_64:
"_sayHello", referenced from:
__cgo_7ab15a91ce47_Cfunc_sayHello in _x002.o
(maybe you meant: __cgo_7ab15a91ce47_Cfunc_sayHello)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
這裡應該可以使用#cgo預編譯解決(CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS) :
// #cgo CFLAGS: -DPNG_DEBUG=1 -I ./include // #cgo LDFLAGS: -L /usr/local/lib -lpng // #include <png.h> import "C"
- CFLAGS:-D部分定義了巨集PNG_DEBUG,值為1。-I定義了標頭檔案包含的檢索目錄
- LDFLAGS:-L指定了連結時庫檔案檢索目錄,-l指定了連結時需要連結png庫
通常實際的工作中遇到要使用cgo的場景,都是呼叫動態庫的方式,所以這裡沒有繼續往下深究上面的錯誤如何解決了。
呼叫C靜態庫和動態庫
目錄結構如下:
├── call_shared_lib │ └── main.go ├── call_static_lib │ └── main.go └── mylib ├── hello.c ├── hello.h ├── libhello.a └── libhello.so
1)靜態庫
把上面的hello.h 和 hello.c 生成為靜態庫(需要安裝gcc,省略):
# 生成o物件 $ gcc -c hello.c # 生成靜態庫 $ ar crv libhello.a hello.o # 檢視裡面包含的內容 # ar -t libhello.a # 使用靜態庫 #gcc main.c libhello.a -o main
Go中呼叫C靜態庫:
package main /* #cgo CFLAGS: -I ../mylib #cgo LDFLAGS: -L ../mylib -lhello #include <stdlib.h> #include "hello.h" */ import "C" import "unsafe" // 請先按照README.md 生成libhello.a 靜態庫檔案 func main() { cStr := C.CString("hello from go") defer C.free(unsafe.Pointer(cStr)) C.sayHello(cStr) }
2)動態庫
生成
# 生成o物件 $ gcc -fPIC -c hello.c # 生成動態庫 $ gcc -shared -fPIC -o libhello.so hello.o # 使用動態庫 #gcc main.c -L. -lhello -o main
呼叫程式碼和上面一樣的,LDFLAGS加上-lstdc++:
#cgo LDFLAGS: -L ../mylib -lhello -lstdc++
注意,生成的so檔案一定的是libhello.so,然後在Go中只需要寫-lhello即可,不是libhello,linux下會自動增加lib字首。
唯一不同的是,靜態庫需要指定so檔案的搜尋路徑或者把so動態庫拷貝到/usr/lib下,在環境變數中配置:
$ export LD_LIBRARY_PATH=../mylib $ go run main.go # 也可以在goland中在Run -> Edit Configurations -> Environment 配置 LD_LIBRARY_PATH=../mylib ,方便除錯
更多關於靜態庫和動態庫的區別:segmentfault.com/a/119000002…
呼叫C++動態庫
本質上和呼叫c動態庫在Go的寫法上是一樣的,只是需要匯出成C風格的即可:
#ifdef __cplusplus extern "C" { #else // 匯出C 命名風格函式,函式名字和定義的一樣,C++因為支援過載,所以匯出的函式名被編譯器改變了 #ifdef __cplusplus } #endif
CGO的缺陷
cgo is not Go中總結了cgo 的缺點:
- 編譯變慢,實際會使用 c 語言編譯工具,還要處理 c 語言的跨平臺問題
- 編譯變得複雜
- 不支援交叉編譯
- 其他很多 go 語言的工具不能使用
- C 與 Go 語言之間的的互相呼叫繁瑣,是會有效能開銷的
- C 語言是主導,這時候 go 變得不重要,其實和你用 python 呼叫 c 一樣
- 部署複雜,不再只是一個簡單的二進位制
這篇文章描述了CGO通過go去呼叫C效能開銷大的原因:blog.csdn.net/u010853261/…
- 必須切換go的協程棧到系統執行緒的主棧去執行C函式
- 涉及到系統呼叫以及協程的排程。
- 由於需要同時保留C/C++的執行時,CGO需要在兩個執行時和兩個ABI(抽象二進位制介面)之間做翻譯和協調。這就帶來了很大的開銷。
《GO原本》中進一步通過runtime原始碼解讀了原因
所以,使用的時候,自己靈活根據場景取捨吧
。
CGO最佳使用場景總結
CGO的一些缺點:
-
記憶體隔離
-
C函式執行切換到g0(系統執行緒)
-
收到GOMAXPROC執行緒限制
-
CGO空呼叫的效能損耗(50+ns)
-
編譯損耗(CGO其實是有個中間層)
CGO 適合的場景:
-
C 函式是個大計算任務(不在乎CGO呼叫效能損耗)
-
C 函式呼叫不頻繁
-
C 函式中不存在阻塞IO
-
C 函式中不存在新建執行緒(與go裡面協程排程由潛在可能互相影響)
-
不在乎編譯以及部署的複雜性
更多可以閱讀:
Ocr實戰
1.chineseocr_lite介紹
GitHub: github.com/DayBreak-u/…
Star: 7.1 K
介紹:超輕量級中文ocr,支援豎排文字識別, 支援ncnn、mnn、tnn推理 ( dbnet(1.8M) + crnn(2.5M) + anglenet(378KB)) 總模型僅4.7M。
這個開源專案提供了C++、JVM、Android、.Net等實現,沒有任何三方依賴,經作者實踐,識別效果中等,越小的圖片越快。
比如識別一個發票號碼,只需要50ms左右:
複雜的圖片識別大概500-900ms左右:
表格識別效果一般
所以,適合格式一致的識別場景。比如發票的某個位置,身份證,銀行卡等等
。
2.編譯chineseocr_lite
按照 chineseocr_lite/cpp_projects/OcrLiteOnnx 中的README.md文件編譯即可,推薦在Linux下,我再windows和Macos沒編譯通過。
然後需要改造成動態庫,我改動的內容有:
- 預設生成動態庫,給ocr_http_server使用
- 去掉jni的支援
- 增加ocr.h,匯出c風格函式
3.匯出c函式
ocr.h
/** @file ocr.h * @brief 封裝給GO呼叫 * @author teng.qing * @date 8/13/21 */ #ifndef ONLINE_BASE_OCRLITEONNX_OCR_H #define ONLINE_BASE_OCRLITEONNX_OCR_H #ifdef __cplusplus extern "C" { #else // c typedef enum{ false, true }bool; #endif const int kOcrError = 0; const int kOcrSuccess = 1; const int kDefaultPadding = 50; const int kDefaultMaxSideLen = 1024; const float kDefaultBoxScoreThresh = 0.6f; const float kDefaultBoxThresh = 0.3f; const float kDefaultUnClipRatio = 2.0f; const bool kDefaultDoAngle = true; const bool kDefaultMostAngle = true; /**@fn ocr_init *@brief 初始化OCR *@param numThread: 執行緒數量,不超過CPU數量 *@param dbNetPath: dbnet模型路徑 *@param anglePath: 角度識別模型路徑 *@param crnnPath: crnn推理模型路徑 *@param keyPath: keys.txt樣本路徑 *@return <0: error, >0: instance */ int ocr_init(int numThread, const char *dbNetPath, const char *anglePath, const char *crnnPath, const char *keyPath); /**@fn ocr_cleanup *@brief 清理,退出程式前執行 */ void ocr_cleanup(); /**@fn ocr_detect *@brief 識別圖片 *@param image_path: 圖片完整路徑,會在同路徑下生成圖片識別框選效果,便於除錯 *@param out_json_result: 識別結果輸出,json格式。 *@param buffer_len: 輸出緩衝區大小 *@param padding: 50 *@param maxSideLen: 1024 *@param boxScoreThresh: 0.6f *@param boxThresh: 0.3f *@param unClipRatio: 2.0f *@param doAngle: true *@param mostAngle: true *@return 成功與否 */ int ocr_detect(const char *image_path, char *out_buffer, int *buffer_len, int padding, int maxSideLen, float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle); /**@fn ocr_detect *@brief 使用預設引數,識別圖片 *@param image_path: 圖片完整路徑 *@param out_buffer: 識別結果,json格式。 *@param buffer_len: 輸出緩衝區大小 *@return 成功與否 */ int ocr_detect2(const char *image_path, char *out_buffer, int *buffer_len); #ifdef __cplusplus } #endif #endif //ONLINE_BASE_OCRLITEONNX_OCR_H
ocr.cpp
/** @file ocr.h * @brief * @author teng.qing * @date 8/13/21 */ #include "ocr.h" #include "OcrLite.h" #include "omp.h" #include "json.hpp" #include <iostream> #include <sys/stat.h> using json = nlohmann::json; static OcrLite *g_ocrLite = nullptr; inline bool isFileExists(const char *name) { struct stat buffer{}; return (stat(name, &buffer) == 0); } int ocr_init(int numThread, const char *dbNetPath, const char *anglePath, const char *crnnPath, const char *keyPath) { omp_set_num_threads(numThread); // 平行計算 if (g_ocrLite == nullptr) { g_ocrLite = new OcrLite(); } g_ocrLite->setNumThread(numThread); g_ocrLite->initLogger( true,//isOutputConsole false,//isOutputPartImg true);//isOutputResultImg g_ocrLite->Logger( "ocr_init numThread=%d, dbNetPath=%s,anglePath=%s,crnnPath=%s,keyPath=%s \n", numThread, dbNetPath, anglePath, crnnPath, keyPath); if (!isFileExists(dbNetPath) || !isFileExists(anglePath) || !isFileExists(crnnPath) || !isFileExists(keyPath)) { g_ocrLite->Logger("invalid file path.\n"); return kOcrError; } g_ocrLite->initModels(dbNetPath, anglePath, crnnPath, keyPath); return kOcrSuccess; } void ocr_cleanup() { if (g_ocrLite != nullptr) { delete g_ocrLite; g_ocrLite = nullptr; } } int ocr_detect(const char *image_path, char *out_buffer, int *buffer_len, int padding, int maxSideLen, float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle) { if (g_ocrLite == nullptr) { return kOcrError; } if (!isFileExists(image_path)) { return kOcrError; } g_ocrLite->Logger( "padding(%d),maxSideLen(%d),boxScoreThresh(%f),boxThresh(%f),unClipRatio(%f),doAngle(%d),mostAngle(%d)\n", padding, maxSideLen, boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle); OcrResult result = g_ocrLite->detect("", image_path, padding, maxSideLen, boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle); json root; root["dbNetTime"] = result.dbNetTime; root["detectTime"] = result.detectTime; for (const auto &item : result.textBlocks) { json textBlock; for (const auto &boxPoint : item.boxPoint) { json point; point["x"] = boxPoint.x; point["y"] = boxPoint.y; textBlock["boxPoint"].push_back(point); } for (const auto &score : item.charScores) { textBlock["charScores"].push_back(score); } textBlock["text"] = item.text; textBlock["boxScore"] = item.boxScore; textBlock["angleIndex"] = item.angleIndex; textBlock["angleScore"] = item.angleScore; textBlock["angleTime"] = item.angleTime; textBlock["crnnTime"] = item.crnnTime; textBlock["blockTime"] = item.blockTime; root["textBlocks"].push_back(textBlock); root["texts"].push_back(item.text); } std::string tempJsonStr = root.dump(); if (static_cast<int>(tempJsonStr.length()) > *buffer_len) { g_ocrLite->Logger("buff_len is too small \n"); return kOcrError; } *buffer_len = static_cast<int>(tempJsonStr.length()); ::memcpy(out_buffer, tempJsonStr.c_str(), tempJsonStr.length()); return kOcrSuccess; } int ocr_detect2(const char *image_path, char *out_buffer, int *buffer_len) { return ocr_detect(image_path, out_buffer, buffer_len, kDefaultPadding, kDefaultMaxSideLen, kDefaultBoxScoreThresh, kDefaultBoxThresh, kDefaultUnClipRatio, kDefaultDoAngle, kDefaultMostAngle); }
ocr_wrapper.go
package ocr // -I: 配置編譯選項 // -L: 依賴庫路徑 /* #cgo CFLAGS: -I ../../../OcrLiteOnnx/include #cgo LDFLAGS: -L ../../../OcrLiteOnnx/lib -lOcrLiteOnnx -lstdc++ #include <stdlib.h> #include <string.h> #include "ocr.h" */ import "C" import ( "runtime" "unsafe" ) //const kModelDbNet = "dbnet.onnx" //const kModelAngle = "angle_net.onnx" //const kModelCRNN = "crnn_lite_lstm.onnx" //const kModelKeys = "keys.txt" const kDefaultBufferLen = 10 * 1024 var ( buffer [kDefaultBufferLen]byte ) func Init(dbNet, angle, crnn, keys string) int { threadNum := runtime.NumCPU() cDbNet := C.CString(dbNet) // to c char* cAngle := C.CString(angle) // to c char* cCRNN := C.CString(crnn) // to c char* cKeys := C.CString(keys) // to c char* ret := C.ocr_init(C.int(threadNum), cDbNet, cAngle, cCRNN, cKeys) C.free(unsafe.Pointer(cDbNet)) C.free(unsafe.Pointer(cAngle)) C.free(unsafe.Pointer(cCRNN)) C.free(unsafe.Pointer(cKeys)) return int(ret) } func Detect(imagePath string) (bool, string) { resultLen := C.int(kDefaultBufferLen) // 構造C的緩衝區 cTempBuffer := (*C.char)(unsafe.Pointer(&buffer[0])) cImagePath := C.CString(imagePath) defer C.free(unsafe.Pointer(cImagePath)) isSuccess := C.ocr_detect2(cImagePath, cTempBuffer, &resultLen) return int(isSuccess) == 1, C.GoStringN(cTempBuffer, resultLen) } func CleanUp() { C.ocr_cleanup() }
3.環境變數設定
路徑包含庫所在目錄,或者直接把動態庫拷貝到/usr/lib中,推薦後者:
export LD_LIBRARY_PATH=../mylib
4.執行
效果如下
參考
- 【Free Style】CGO: Go與C互操作技術(一):Go調C基本原理
- 【Free Style】CGO: Go與C互操作技術(三):Go調C效能分析及優化
- Command cgo
- C? Go? Cgo!
- How to use C++ in Go?
- 深入學習CGO
- 如何把Go呼叫C的效能提升10倍?
- Go語言使用cgo時的記憶體管理筆記
- GO原本:cgo
- CGO 和 CGO 效能之謎
- cgo is not Go
- 深入CGO程式設計(Gopherchina2018)
- how-to-use-c-in-go
- 使用gcc生成靜態庫和動態庫
- 如何使用GCC生成動態庫和靜態庫
- gcc編譯工具生成動態庫和靜態庫之一----介紹