Cgo学习
Cgo 的诞生是为了继承C/C++积累了半个世纪的软件财富,这样的话我们可以方便的在Go项目中使用这些财富!具体信息可以看官方文档 ,本文会介绍如何使用Cgo,如何将C++项目集成到Go中,有兴趣可以直接看我自己用Cgo写的一个项目,成熟度还可以: github.com/anthony-don…
Cgo 真的完美吗
Cgo 顾名思义,是C与GO的一个桥梁,但是C与GO的调度模型、内存模型不太一样,就会导致这个桥梁会有一些性能、内存损耗,例如Go的用户代码都跑在goroutine(有栈协程)中,但是C跑在原生的线程中,就会导致要进行一次线程的切换,由 goroutine -> Native-Thread -> goroutine ,所以应该尽量避免使用一些耗时比较长的c程序!
TODO:后续补充JNI的性能开销!!
下面我们可以对比一下简单的Go和Cgo差异
package test
/*
int sum_c (int x, int y){
return x + y ;
}
*/
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的结果,
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等工具来限制最大并发数量 !
// main.go 文件
/*
#include <unistd.h>
int sum_c (int x, int y){
sleep(60);
return x + y ;
}
*/
import "C"
func sum_c(x, y int) int {
return int(C.sum_c(C.int(x), C.int(y)))
}
// main_test.go
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()
}
// ps -mq ${pid} | wc -l
熟悉Cgo基本写法
例子
代码分为两部分,一部分是C代码(注意: 必须是C,不能是C++),一部分是Go代码,其次一定要 import "C"
package main
/*
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void c_print_str(const char* str){
printf("c_print_str: %s\n",str);
}
void c_print_str_size(const char* str,int len){
printf("c_print_str_size: ");
for (int x=0; x<len; x++){
printf("%c",*str);
str=str+1;
}
printf("\n");
}
int c_str_len(const char* str){
return strlen(str);
}
const char* c_new_str() {
char* str = (char*)malloc(5 * sizeof(char));
str[0] = 'a';
str[1] = '\0';
str[2] = 'b';
str[3] = '\0';
str[4] = 'c';
return str;
}
typedef struct __CStruct {
char* name;
int age;
} CStruct;
void print_CStruct(CStruct* ss) {
printf("name: %s, age: %d\n", ss->name, ss->age);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
{
// C语言中认为'\0'是一个字符串的结尾符,也就是说字符串会额外多一个size(char)来存储'\0',但是Go语言不是!
gstr := "hello world\u00001111"
// 创建一个C的 char* 字符串,这里会涉及到一次内存的拷贝,原因是为了安全,同时你也需要free掉!
cstr := C.CString(gstr)
defer C.free(unsafe.Pointer(cstr))
// 调用C函数
C.c_print_str(cstr)
C.c_print_str_size(cstr, C.int(len(gstr)))
// 注意: C中基本类型转换Go直接强转即可,最好转成对应类型
fmt.Println("int(C.c_str_len(cstr)): ", int(C.c_str_len(cstr)))
fmt.Println("len(gstr): ", len(gstr))
}
{
// 获取C的字符串
cstr := C.c_new_str()
defer C.free(unsafe.Pointer(cstr))
// 默认GoString遵循的C的实现,也就是遇到'\0'就截断了,所以输出了 a
printStr("C.GoString(cstr)", C.GoString(cstr))
// 就是由于上诉的原因,因此人家开发了一个C.GoStringN函数,就是你需要显示告诉我C中char*的长度!
printStr("C.GoStringN(cstr, 5)", C.GoStringN(cstr, 5))
// char* -> []byte 转换
var data []byte = C.GoBytes(unsafe.Pointer(cstr), C.int(5))
for _, elem := range data {
fmt.Printf("char: %U\n", elem)
}
// 它是一个切片!
// 注意:切片也可以转换成char数组
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
,输出:
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 都需要涉及到数据的拷贝!
import "C"
const cStrEnd = string('\u0000')
// unsafe string GO -> C
func unsafeCString(str string) *C.char {
// C语言的字符串是以\u0000 结尾的,所以这里注意了. 需要手动加一个结尾符号
if index := strings.IndexByte(str, '\u0000'); index == -1 {
str = str + cStrEnd
}
header := (*reflect.StringHeader)(unsafe.Pointer(&str))
return (*C.char)(unsafe.Pointer(header.Data))
}
// unsafe []byte GO -> C
// 注意 []byte 长度大于0
func unsafeCBytes(str []byte) *C.char {
return (*C.char)(unsafe.Pointer(&str[0]))
}
// unsafeGoBytes []byte C->GO
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的基本语法
- 项目地址: github.com/anthony-don…
项目结构
├── 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的翻译!
实现功能
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))
}
// 运行: CGO_ENABLED=1 go run main.go
注意点
- 尽可能的使用 unsafe 操作避免内存拷贝,尤其是数据大的情况,效果优秀
- 简单函数尽可能的用GO实现
- 注意内存管理和回收,避免直接暴露给使用者
- C++的技巧可以参考我的这篇文章: anthony-dong.github.io/2023/04/06/…
- C 语言实际上没有太多要学习的,是最简单的语言了,没啥难度,无非注意内存分配!
- C++ 翻译 C 会存在有些类对象转换不来或者拷贝代价太高,尽可能的使用
void指针
避免拷贝!
参考
- Cgo 官方文档: pkg.go.dev/cmd/cgo@go1…
- Go语言高级编程:chai2010.cn/advanced-go…
转载自:https://juejin.cn/post/7236946141060628535