这是我参与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 ", numThread, dbNetPath, anglePath, crnnPath, keyPath); if (!isFileExists(dbNetPath) || !isFileExists(anglePath) || !isFileExists(crnnPath) || !isFileExists(keyPath)) { g_ocrLite->Logger("invalid file path. "); 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) ", 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 "); 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编译工具生成动态库和静态库之一----介绍