Cgo,Go与C交互的详细介绍

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 nameC name
go namec name
C.char, C.scharsigned char
C.ucharunsigned char
C.short, C.ushortunsigned short
C.int, C.uintunsigned int
C.long,C.ulongunsigned long
C.longlonglong long
C.ulonglongunsigned long long
C.float, C.double, C.complexfloatcomplex float
C.complexdoubleomplex double
unsafe.Pointervoid*
__int128_t and __uint128_t[16]byte
C.struct_xxxstruct
C.union_xxxunion

关于字符串的两个特别的方法

可以在序言中声明以下两个特别的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++规则进行符号修饰。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值