Cgo,详细介绍Go与C交互详细过程
文章目录
概念解释
- Cgo是Go语言提供的一个工具,它本身是一个可执行文件,当我们调用go build指令编译项目的时候,Cgo会在需要处理C代码的时候被自动使用
- Cgo依赖Gcc工作
- Cgo本身可以被直接执行,并提供了一系列可选指令选项帮助程序员查找问题
使用Cgo在Go中直接编写C代码
package main
/*
#include <stdio.h>
void PrintHello()
{
printf("hello world")
}
*/
import "C"
func main() {
C.PrintHello()
}
上面这段代码通过调用C标准库中的printf函数向标准输出输出hello world字符串
/*
#include <stdio.h>
void PrintHello()
{
printf("hello world")
}
*/
- 这段被注释的内容被称之为 “序言”,或是"序文"(preamble),可以在序言中直接编写任意的C代码,或引入标准库的头文件,或是要使用的库文件的头文件
- import "C"其中的C并不是一个真正的go包,称为伪包,用来帮助Cgo识别C代码,需要注意的是在序文结束的后的 import “C” 必须紧跟在序言后面,不能有空行,否则会编译出错
- 序言中声明的C函数在Go中进行调用的时候要用C.xxx的形式,所有引入的C函数,变量,以及类型,在使用的时候都要以大写的C.作为前缀
- 所有的C类型都应该局限在使用了 import "C"的包中,避免暴露在包外
Cgo工作的基本过程
对上面的helloworld示例程序执行 go tool cgo main.go
会看到Cgo生成的一系列中间文件
打开main.cgo2.c可以找到上面的代码片段,首先cgo把我们定义在序言中的c代码完整的搬运到了这里,包括引入的头文件和我们定义的PrintHello函数
在文件的末尾,cgo为我们生成了新的函数 _cgo_bd85ba2d6721_Cfunc_PrintHello 并在这个函数中调用我们之前在序言中定义的PrintHello函数
打开_cgo_gotypes.go文件,这个文件属于main包,所以其中定义函数可以被 go 的 main 函数调用,可以看到在第32行,编译制导语句go:cgo_import_static 将上文中提到的 cgo 生成的c函数 _cgo_bd85ba2d6721_Cfunc_PrintHello 引入并和字节型变量__cgofn__cgo_bd85ba2d6721_Cfunc_PrintHello 对齐,并用一个新的变量 _cgo_bd85ba2d6721_Cfunc_PrintHello 保存这个字节型变量的地址,从而实现在Go中拿到C函数的地址,在37行定义了一个新的go函数 func _Cfunc_PrintHello,并在其中调用了之前通过字节变量拿到的C函数
再对 main.cgo1.go 文件进行考察,这是被cgo改写后的go文件,这个被改写的go文件是最终交给go编译器的文件,可以看出 import “C” 语句已经被移除,并引入了 unsafe 包, 在main函数中调用了在上文中提到的 _Cfunc_PrintHello 函数
总结一下:
针对我们这个例子,cgo首先扫描 import “C” 的文件,并将序言部分的C代码拷贝到一个C文件中,这个C文件应该会被gcc编译成 .o 等待后续链接操作,使用 nm _cgo.o 指令,可以看到其中有 PrintHello 函数的定义可以证实我们的猜测,cgo同时会生成一个中间的Go文件,使用编译制导指令在链接期获取C函数的地址,并使用一个新的go函数包装对C函数的调用,cgo会生成一个新的main文件,这个文件是真正被Go编译器处理的main文件,并将其中 import “C” 语句移除,并将以 C.xxx 形式调用的C函数替换为cgo自己生成的Go函数
基础类型转换
这个表展示了常见的数据类型在C和Go中名称
Go name | C name |
---|---|
go name | c name |
C.char, C.schar | signed char |
C.uchar | unsigned char |
C.short, C.ushort | unsigned short |
C.int, C.uint | unsigned int |
C.long,C.ulong | unsigned long |
C.longlong | long long |
C.ulonglong | unsigned long long |
C.float, C.double, C.complexfloat | complex float |
C.complexdouble | omplex double |
unsafe.Pointer | void* |
__int128_t and __uint128_t | [16]byte |
C.struct_xxx | struct |
C.union_xxx | union |
关于字符串的两个特别的方法
可以在序言中声明以下两个特别的C方法
size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);
他们的参数类型是_GoString_ s,第一个方法返回Go字符串的长度,第二个方法返回指向这个字符串的char*指针,下面为示例代码
package main
/*
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 这两个函数要声明在序言中
size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);
void PrintGoStringInfo(char* s, size_t count)
{
// 注意,s尾部没有"\0", 在C中直接当字符串处理会出错
char* buf = malloc((count + 1) * sizeof(char)); //
memset(buf, 0, count + 1);
memcpy(buf, s, count);
printf("%s\n", buf);
printf("sizeof goString: %ld\n", count);
free(buf);
}
*/
import "C"
func main() {
str := "hello world"
C.PrintGoStringInfo(C._GoStringPtr(str), C._GoStringLen(str))
}
-
需要注意的是_GoStringPtr返回的char*尾部是不包含的\0的在C中直接当字符串处理会出错
-
这两个函数仅可在序言中使用,不能在其他的C文件中使用,C代码绝不能通过_GoStringPtr返回的指针修改其指向的内容
注意:这两个函数可以很方便的将Go string转换为C的char*,如果使用_GoStringPtr传入一个临时的string到C中,在C中应拷贝一份副本到C内存中,尤其是在一些异步调用的过程中,从官方关于cgo的文档看来,这两个函数似乎并不保证传入的临时Go string类型不会被gc回收
结构体
- C中定义的结构体字段名有可能和Go中的关键字冲突,这些发生冲突的字段会被自动加上下划线作为前缀,访问这种字段的时候要用这样的形式:x._type
- 在C中的一些字段无法在Go中表达,如位域和未对其的结构,在Go的结构体中,这些字段会被忽略,但会在下一个字段之前或者结构体的结尾之前留下相应的空白空间
需要给结构体中的数组进行赋值可以用以下方法
/*
typedef struct {
int a[32];
} STRU_A
// 须包含这两个库文件
#include <stdlib.h>
#include <stringlh>
*/
import "C"
name = "abcd"
struA := C.STRU_A{}
cName = C.CString(name)
defer C.free(unsafe.Pointer(cName))
C.memcpy(unsafe.Pointer(&struA.cName), unsafe.Pointer(cName), C.size_t(len(name)))
类型转换方法
// 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).
// 这个方法会在C的堆上分配内存,需要使用C.free释放,需要包含stdlib.h
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).
// 这里同样需要使用C.free释放内存
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.malloc并不是直接调用C中的malloc,而是调用了一个包装了一个Go的辅助函数,其包装了C库中的malloc,并保证永远不会返回nil。如果C的malloc表示用尽内存,这个辅助函数就会使程序崩溃,就像Go自身用尽内存发生崩溃,因为C的malloc不能失败,所以他没有返回errno的两个结果的形式
c中的sizeof并不能以C.sizeof的形式使用,而是应该用C.size_T的形式使用,T是C中的类型名
函数指针和回调
go调用C的函数指针
go不能直接调用C的函数指针,但可以调用C的函数,也可以持有C的函数指针,如果go想调用一个C的函数指针,可以将C的指针传入go中,go再将这个指针通过一个C接口送到C侧,然后由C侧执行并返回结果
package main
// typedef int (*intFunc) ();
//
// int
// bridge_int_func(intFunc f)
// {
// return f();
// }
//
// int fortytwo()
// {
// return 42;
// }
import "C"
import "fmt"
func main() {
f := C.intFunc(C.fortytwo)
fmt.Println(int(C.bridge_int_func(f)))
// Output: 42
}
C回调go的函数
C可以调用go中被//export标记的导出的函数
C文件
typedef void(*cbtype)();
void registerCallback(cbtype cb);
go文件
/*
// 在序言中声明一次,这是为了让cgo能够 “看到” 这个C函数,否则无法通过编译
void callbackFunc();
*/
import "C"
func foo() {
C.registerCallback(C.cbtype(C.callbackFunc))
}
//export callbackFunc
func callbackFunc() {
fmt.Println("go callback func")
}
关于C数据作为参数
在C中,向函数传入一个固定大小的数组作为参数,需要一个指向数组第一个元素的指针。C编译器知到这样的调用约定,并相应的调整调用方式。但在Go中并非如此,你必须明确的传入指向数组第一个元素的指针C.f(&C.x[0])
译者注:这里指的是C中数组名传入函数后变为指向首元素的指针
可变参数
调用可变参C函数是不被支持的,但可以通过使用C函数包装的方法来规避这个问题,如下
package main
// #include <stdio.h>
// #include <stdlib.h>
//
// static void myprint(char* s) {
// printf("%s\n", s);
// }
import "C"
import "unsafe"
func main() {
cs := C.CString("Hello from stdio")
C.myprint(cs)
C.free(unsafe.Pointer(cs))
}
C引用Go
Go方法可以按照以下方法导出给C代码使用
//export MyFunction
func MyFunction(arg1, arg2 int, arg3 string) int64 {...}
//export MyFunction2
func MyFunction2(arg1, arg2 int, arg3 string) (int64, *C.char) {...}
他们带C代码中以如下形式使用
extern GoInt64 MyFunction(int arg1, int arg2, GoString arg3);
extern struct MyFunction2_return MyFunction2(int arg1, int arg2, GoString arg3);
在生成的 _cogo_export.h 头文件,在序言以及所有拷贝自cgo导入的文件内容之后。有多返回值的函数会被映射为返回一个结构体
并不是所有的go类型都可以被映射到C类型,Go的 struct 不被支持;使用C的Struct,Go的array类型不被支持,使用C的指针
可以使用C类型 GoString 调用一个需要传入go字符串的go函数,如上文所述。GoString 类型会在序言中被自动定义, 注意,C类型无法创建这种类型的值,这种方式只有在从Go向C传递string值,和返回Go中时有用
动态库和静态库
cgo只能引入纯c语法的头文件,C的头文件中只能有C语言的语法,不能出现C++特性的语法,比如重载,默认参数等,在C侧的接口实现如果使用C++写,需要使用extern "C"将要实现的接口声明一次,这样可以告诉编译器不要将接口按照C++规则进行符号修饰。