Cgo 的诞生是为了继承C/C++积累了半个世纪的软件财富,这样的话我们可以方便的在Go项目中使用这些财富!具体信息可以看官方文档 ,本文会介绍如何使用Cgo,如何将C++项目集成到Go中,有兴趣可以直接看我自己用Cgo写的一个项目,成熟度还可以: https://github.com/anthony-dong/protobuf
Cgo 真的完美吗
Cgo 顾名思义,是C与GO的一个桥梁,但是C与GO的调度模型、内存模型不太一样,就会导致这个桥梁会有一些性能、内存损耗,例如Go的用户代码都跑在goroutine(有栈协程)中,但是C跑在原生的线程中,就会导致要进行一次线程的切换,由 goroutine -> Native-Thread -> goroutine ,所以应该尽量避免使用一些耗时比较长的c程序!
TODO:后续补充JNI的性能开销!!
下面我们可以对比一下简单的Go和Cgo差异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package test
import "C"
func sum(x, y int) int { return x + y }
func sum_c(x, y int) int { return int(C.sum_c(C.int(x), C.int(y))) }
|
来看下benchmark的结果,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| goos: linux goarch: amd64 pkg: github.com/anthony-dong/protobuf/internal/pb_gen cpu: Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHz BenchmarkSUM BenchmarkSUM-8 1000000000 0.3557 ns/op 0 B/op 0 allocs/op BenchmarkSUM-8 1000000000 0.3567 ns/op 0 B/op 0 allocs/op BenchmarkSUM-8 1000000000 0.3626 ns/op 0 B/op 0 allocs/op BenchmarkSUM-8 1000000000 0.3588 ns/op 0 B/op 0 allocs/op BenchmarkSUM-8 1000000000 0.3540 ns/op 0 B/op 0 allocs/op BenchmarkSUM_C BenchmarkSUM_C-8 14440388 79.73 ns/op 0 B/op 0 allocs/op BenchmarkSUM_C-8 15093638 85.74 ns/op 0 B/op 0 allocs/op BenchmarkSUM_C-8 14932076 85.33 ns/op 0 B/op 0 allocs/op BenchmarkSUM_C-8 14808447 79.42 ns/op 0 B/op 0 allocs/op BenchmarkSUM_C-8 13486689 78.92 ns/op 0 B/op 0 allocs/op PASS ok github.com/anthony-dong/protobuf/internal/pb_gen 8.531s
|
结论就是
- 大概调度上是3个数量级的损耗,差距近千倍!所以CGO不适合做那种简单的业务逻辑处理,如果代码可以很简单的通过Go程序实现,那么原则上不要用CGO去做,除非C性能要远高于GO或者GO去实现太过于麻烦!
- CGO使用原生的Native线程,如果你的C程序耗时比较严重,且并发较高,对于GO程序的影响也会很大!
- 注意GO里面可以通过
debug.SetMaxThreads(10)
来设置最大的线程数,但是假如CGO调度的线程不够了,那么会直接程序挂掉,所以不要使用 限制最大线程数的函数,可以通过channel等工具来限制最大并发数量 !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
import "C"
func sum_c(x, y int) int { return int(C.sum_c(C.int(x), C.int(y))) }
func TestCThread(t *testing.T) { t.Log("pid: ", os.Getpid()) currentLock := make(chan bool, 10)
wg := sync.WaitGroup{} wg.Add(100) for x := 0; x < 100; x++ { cloneX := x go func() { currentLock <- true defer func() { <-currentLock defer wg.Done() }() t.Log("sum-start: ", cloneX) s := sum_c(cloneX, cloneX+1) t.Log("sum-done: ", cloneX, s) }() } wg.Wait() }
|
熟悉Cgo基本写法
例子
代码分为两部分,一部分是C代码(注意: 必须是C,不能是C++),一部分是Go代码,其次一定要 import "C"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| package main
import "C" import ( "fmt" "unsafe" )
func main() { { gstr := "hello world\u00001111"
cstr := C.CString(gstr) defer C.free(unsafe.Pointer(cstr))
C.c_print_str(cstr) C.c_print_str_size(cstr, C.int(len(gstr)))
fmt.Println("int(C.c_str_len(cstr)): ", int(C.c_str_len(cstr))) fmt.Println("len(gstr): ", len(gstr)) }
{ cstr := C.c_new_str() defer C.free(unsafe.Pointer(cstr))
printStr("C.GoString(cstr)", C.GoString(cstr)) printStr("C.GoStringN(cstr, 5)", C.GoStringN(cstr, 5))
var data []byte = C.GoBytes(unsafe.Pointer(cstr), C.int(5)) for _, elem := range data { fmt.Printf("char: %U\n", elem) }
var data2 = data[:1] printStr("data2", string(data2)) }
{ var ss C.CStruct ss.name = C.CString("tom") ss.age = C.int(1) C.print_CStruct(&ss) } }
func printStr(name, value string) { fmt.Printf(`%s: "%s", len: %d`+"\n", name, value, len(value)) }
|
执行 CGO_ENABLED=1 go run -v main.go
,输出:
1 2 3 4 5 6 7 8 9 10 11 12 13
| c_print_str: hello world c_print_str_size: hello world1111 int(C.c_str_len(cstr)): 11 len(gstr): 16 C.GoString(cstr): "a", len: 1 C.GoStringN(cstr, 5): "abc", len: 5 char: U+0061 char: U+0000 char: U+0062 char: U+0000 char: U+0063 data2: "a", len: 1 name: tom, age: 1
|
总结
func C.CString(string) *C.char
这个函数转换成C的字符串的时候,没有考虑 \0
结尾符号的问题,所以这点一定要注意!func C.GoString(*C.char) string
的实现考虑了\0
结尾符号的问题,因此它实际上就是拷贝了 strlen
长度的C字符串到Go的字符串func C.GoStringN(*C.char, C.int) string
解决了\0
结尾符号的问题,需要显示指定 C语言中字符串的长度!func C.GoBytes(unsafe.Pointer, C.int) []byte
和 func C.CBytes([]byte) unsafe.Pointer
可以实现数组的转换- 其他基础类型都支持转换,具体看文档:官方文档
如何降低开销
使用原生的API,go->c 和 c->go 都需要涉及到数据的拷贝!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import "C" const cStrEnd = string('\u0000')
func unsafeCString(str string) *C.char { if index := strings.IndexByte(str, '\u0000'); index == -1 { str = str + cStrEnd } header := (*reflect.StringHeader)(unsafe.Pointer(&str)) return (*C.char)(unsafe.Pointer(header.Data)) }
func unsafeCBytes(str []byte) *C.char { return (*C.char)(unsafe.Pointer(&str[0])) }
func unsafeGoBytes(arr *C.char, arrSize C.int) []byte { header := reflect.SliceHeader{ Data: uintptr(unsafe.Pointer(arr)), Len: int(arrSize), Cap: int(arrSize), } return *(*[]byte)(unsafe.Pointer(&header)) }
|
调试工具
可以使用 go tool cgo main.go
查看cgo生成的文件, 其实我们用注释写C代码,编译器并不会识别,而是GO编译期间有个预处理的阶段 生成了 go tool cgo 的产物!
大概会生成一份 C -> GO 转换的代码,具体可以自己调试一下!
如何集成C++
C++ 与 C的关系
我们知道C++ 实际上是完全兼容 C的,其次C++与C是可以相互调用的,那么建立这些前提的就是 要明确告诉 c/c++ 编译器,我这个代码是C语言的,因此需要 extern "C"
来告诉 C++
我这个代码是C语言的,编译器就会按照C语言的规范去链接!
返过来,C语言他没有 extern "C"
这个关键词,那么C++引用了C函数的代码,因此需要 extern "C"
可以修饰 #include ${c的头文件}
,也就是说告诉编译器,这些申明用C语言的规范去链接!
其实上面非常的绕,需要大家亲自体会一下!其次C与C++语法不完全一样,有些时候在做这种集成开发的时候容易混了!
下面这里有个例子,就是集成libprotobuf
实现解析 protobuf
文件,目前应该Go开源社区里面没有做集成的!
前置准备
- 下载
protobuf
, 具体如何本地构建protobuf
的链接库,可以直接看我们的这个项目 - 学会用CMake等工具构建代码
- 掌握C/C++/Go的基本语法
- 项目地址: https://github.com/anthony-dong/protobuf
项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ├── CMakeLists.txt ├── README.md ├── cgo.go # cgo go语言实现 ├── cgo.h # cgo c头文件 ├── deps # 依赖 │ ├── README.md │ ├── darwin_x86_64 │ ├── include # 引用的第三方头文件 │ └── linux_x86_64 # 静态依赖 │ ├── libprotobuf.a │ └── vendor.go # 解决go vendor 问题 ├── errors.go ├── go.mod ├── go.sum ├── option.go ├── pb_include.h ├── pb_parser.cpp # 核心业务逻辑 ├── pb_parser.go # 对外接口 ├── pb_parser.h # 核心业务逻辑 ├── utils.go └── vendor.go # 解决go vendor 问题
|
大概就是C++写业务逻辑,然后 C++ 的接口 转成 C接口, C->GO的翻译!
实现功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package main
import ( "fmt"
"github.com/anthony-dong/protobuf" )
func main() { tree, err := protobuf.NewProtobufDiskSourceTree("internal/test/idl_example") if err != nil { panic(err) } idlConfig := new(protobuf.IDLConfig) idlConfig.IDLs = tree idlConfig.Main = "service/im.proto" idlConfig.IncludePath = []string{"desc", "."}
desc, err := protobuf.ParsePBMultiFileDesc(idlConfig, protobuf.WithJsonTag(), protobuf.WithSourceCodeInfo(), protobuf.WithGoogleProtobuf(), protobuf.WithRequireSyntaxIdentifier(), ) if err != nil { panic(err) } fmt.Println(protobuf.MessageToJson(desc, true)) }
|
注意点
- 尽可能的使用 unsafe 操作避免内存拷贝,尤其是数据大的情况,效果优秀
- 简单函数尽可能的用GO实现
- 注意内存管理和回收,避免直接暴露给使用者
- C++的技巧可以参考我的这篇文章: https://anthony-dong.github.io/2023/04/06/fd8e40efcdb71f2be44fb720dc582d67/
- C 语言实际上没有太多要学习的,是最简单的语言了,没啥难度,无非注意内存分配!
- C++ 翻译 C 会存在有些类对象转换不来或者拷贝代价太高,尽可能的使用
void指针
避免拷贝!
参考