ch2-cgo
2.1 快速入门
基于C标准库函数输出字符串
1 | // hello.go |
- 通过
import "C"
语句启用CGO特性,go build
命令会在编译和链接阶段启动gcc编译器。紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的C语言代码。 - 包含C语言的
<stdio.h>
头文件。 - 通过CGO包的
C.CString
函数将Go语言字符串转为C语言字符串, - 调用CGO包的
C.puts
函数向标准输出窗口打印转换后的C字符串。
使用自己的C函数
自定义SayHello
的C函数:
1 | package main |
引用外部的C文件:
将SayHello
函数放到当前目录下的一个C语言源文件中
1 | // hello.go |
1 | // hello.c |
因为是编写在独立的C文件中,为了允许外部引用,所以需要去掉函数的
static
修饰符。
- 既然可以将
SayHello
函数放到独立的C文件中,自然就可以引用对应的C文件编译打包为静态库或动态库文件。 - 如果是以静态库或动态库方式引用
SayHello
函数的话,需要将对应的C源文件移出当前目录(CGO构建程序会自动构建当前目录下的C源文件,从而导致C函数名冲突)。
C代码的模块化
模块化编程的核心是面向程序接口编程
(这里的接口并不是Go语言的interface,而是API的概念)。
1 | // hello.h |
1 | // hello.c |
1 | // main.go |
导出Go语言函数给C语言函数调用
1 | // hello.h |
1 | // hello.go |
1 | // main.go |
- 为了适配CGO导出的C语言函数,我们禁止了在函数的声明语句中的const修饰符。
- 通过CGO的
//export SayHello
指令将Go语言实现的函数SayHello
导出为C语言函数。
_GoString_
预定义类型
正常我们使用CGO如下:
1 | package main |
在Go1.10中CGO新增加了一个_GoString_
预定义的C语言类型,用来表示Go语言字符串。
1 | // +build go1.10 |
2.2 CGO基础
cgo将当前包引用的C语言符号都放到了虚拟的C包中,同时当前包依赖的其它Go语言包内部可能也通过cgo引入了相似的虚拟C包。
比如我们希望在Go中定义一个C语言字符指针对应的CChar类型,然后增加一个GoString方法返回Go语言字符串:
1 | package cgo_helper |
现在我们可能会想在其它的Go语言包中也使用这个辅助函数:
1 | package main |
- 上面代码是不能正常工作的,原因是两个文件的
C.cs
的类型其实是不同的。- main包引入的
*C.char
类型是*main.C.char
, - cgo_helper包引入的
*C.char
类型是*cgo_helper.C.char
。
- main包引入的
- 在Go语言中方法是依附于类型存在的,不同Go包中引入的虚拟的C包的类型却是不同的(
main.C
不等于cgo_helper.C
),这导致从它们延伸出来的Go类型也是不同的类型(*main.C.char
不等于*cgo_helper.C.char
),这最终导致了前面代码不能正常工作。
简单来说,
- 一个包如果在公开的接口中直接使用了
*C.char
等类似的虚拟C包的类型,那么其它的Go包是无法直接使用这些类型的,除非这个Go包同时也提供了*C.char
类型的构造函数。- 因为这些诸多因素,如果想在go test环境直接测试这些cgo导出的类型也会有相同的限制。
#cgo
语句
- 在
import "C"
语句前的注释中可以通过#cgo
语句设置编译阶段和链接阶段的相关参数。 - 编译阶段的参数主要用于定义相关宏和指定头文件检索路径。
- 链接阶段的参数主要是指定库文件检索路径和要链接的库文件。
1 | // #cgo CFLAGS: -DPNG_DEBUG=1 -I./include |
CFLAGS部分,
-D
部分定义了宏PNG_DEBUG,值为1;-I
定义了头文件包含的检索目录。
LDFLAGS部分,
-L
指定了链接时库文件检索目录,-l
指定了链接时需要链接png库。
#cgo
指令还支持条件选择,当满足某个操作系统或某个CPU架构类型时后面的编译或链接选项生效。
下面是分别针对windows和非windows下平台的编译和链接选项:
1 | // #cgo windows CFLAGS: -DX86=1 |
其中在windows平台下,编译前会预定义X86宏为1;在非widnows平台下,在链接阶段会要求链接math数学库。这种用法对于在不同平台下只有少数编译选项差异的场景比较适用。
2.3 类型转换
Go语言和C语言类型对比:
C语言类型 | CGO类型 | Go语言类型 |
---|---|---|
char | C.char | byte |
singed 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 |
- CGO中,虽然C语言的
int
固定为4字节的大小,但是Go语言自己的int
和uint
却在32位和64位系统下分别对应4个字节和8个字节大小。 - 如果需要在C语言中访问Go语言的
int
类型,可以通过GoInt
类型访问,GoInt
类型在CGO工具生成的_cgo_export.h
头文件中定义。 - 下面是64位环境下,
_cgo_export.h
头文件生成的Go数值类型的定义:
1 | typedef signed char GoInt8; |
Go 字符串和切片
在CGO生成的_cgo_export.h
头文件中还会为Go语言的字符串、切片、字典、接口和管道等特有的数据类型生成对应的C语言类型:
1 | typedef struct { const char *p; GoInt n; } GoString; |
不过需要注意的是,其中只有字符串和切片在CGO中有一定的使用价值,因为CGO为他们的某些GO语言版本的操作函数生成了C语言版本,因此二者可以在Go调用C语言函数时马上使用;而CGO并未针对其他的类型提供相关的辅助函数,且Go语言特有的内存模型导致我们无法保持这些由Go语言管理的内存指针,所以它们C语言环境并无使用的价值。
结构体、联合、枚举类型
结构体的内存布局按照C语言的通用对齐规则。
结构体的简单用法如下:
1 | /* |
如果结构体的成员名字中碰巧是Go语言的关键字,可以通过在成员名开头添加下划线来访问:
1 | /* |
但是如果有2个成员:一个是以Go语言关键字命名,另一个刚好是以下划线和Go语言关键字命名,那么以Go语言关键字命名的成员将无法访问(被屏蔽):
1 | /* |
- C语言结构体中
位字段成员
无法在Go语言中访问,如果需要操作位字段成员,需要通过在C语言中定义辅助函数来完成。 - C语言结构体中
零长数组成员
无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)
来访问。
1 | /* |
在C语言中,我们无法直接访问Go语言定义的结构体类型。
对于联合类型,我们可以通过C.union_xxx
来访问。但是Go语言中并不支持C语言联合类型,所以它们会被转为对应大小的字节数组。
1 | /* |
如果需要操作C语言的联合类型变量,一般有三种方法:
- 在C语言中定义辅助函数;
- 通过Go语言的”encoding/binary”手工解码成员(需要注意大端小端问题);
- 使用
unsafe
包强制转型为对应类型(这是性能最好的方式)。
下面展示通过unsafe
包访问联合类型成员的方式:
1 | /* |
虽然
unsafe
包访问最简单、性能也最好,但是对于有嵌套联合类型的情况处理会导致问题复杂化。对于复杂的联合类型,推荐通过在C语言中定义辅助函数的方式处理。
对于枚举类型,我们可以通过C.enum_xxx
来访问C语言中定义的enum xxx
结构体类型。
1 | /* |
在C语言中,枚举类型底层对应int
类型,支持负数类型的值。我们可以通过C.ONE
、C.TWO
等直接访问定义的枚举值。
数组、字符串和切片
- 在C语言中,数组名其实对应于一个指针,指向特定类型特定长度的一段内存,但是这个指针不能被修改;当把数组名传递给一个函数时,实际上传递的是数组第一个元素的地址。为了讨论方便,我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。
- 在Go语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。
Go语言和C语言的数组、字符串和切片之间的相互转换,可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换。
CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:
1 | // Go string to C string |
- 其中
C.CString
针对输入的Go字符串,克隆一个C语言格式的字符串;返回的字符串由C语言的malloc
函数分配,不使用时需要通过C语言的free
函数释放。C.CBytes
函数的功能和C.CString
类似,用于从输入的Go语言字节切片克隆一个C语言版本的字节数组,同样返回的数组需要在合适的时候释放。C.GoString
用于将从NULL结尾的C语言字符串克隆一个Go语言字符串。C.GoStringN
是另一个字符数组克隆函数。C.GoBytes
用于从C语言数组,克隆一个Go语言字节切片。
该组辅助函数都是以克隆的方式运行。当Go语言字符串和切片向C语言转换时,克隆的内存由C语言的malloc
函数分配,最终可以通过free
函数释放。当C语言字符串或数组向Go语言转换时,克隆的内存由Go语言分配管理。通过该组转换函数,转换前和转换后的内存依然在各自的语言环境中,它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。