常用的cgo类型
数值类型
C | CGO | Go |
---|---|---|
char | C.char | byte |
singed char | C.schar | int8 |
unsigned char | C.uchar | uint8 |
short | C.short | int16 |
int | C.int | int32 |
long | C.long | int32 |
long long int | C.longlong | int64 |
float | C.float | float32 |
double | C.double | float64 |
size_t | C.size_t | uint |
结构体, 联合, 枚举类型
结构体
在 Go 当中, 可以通过 C.struct_xxx
来访问C语言中定义的 struct xxx
结构体类型.
结构体的内存按照C语言的通用对齐规则, 在 32 位Go语言环境 C 语言结构也按照 32 位对齐规则, 在 64 位Go语言环境按照 64 位对齐规则. 对于指定了特殊对齐规则的结构体, 无法在 CGO 中访问.
/*
struct A {
int i;
float f;
};
*/
import "C"
func main() {
var a C.struct_A
fmt.Println(a.i)
}
结构体当中出现了 Go 语言的关键字, 通过下划线的方式进行访问.
/*
struct A {
int type;
char chan;
};
*/
import "C"
func main() {
var a C.struct_A
fmt.Println(a._type)
fmt.Println(a._chan)
}
如果有两个成员, 一个是以 Go 语言关键字命名, 另外一个刚好是以下划线和Go语言关键字命名, 那么以 Go 语言关键字命名的成员将无法访问(被屏蔽)
C 语言结构体中 位字段
对应的成员无法在 Go 语言当中访问. 如果需要操作 位字段
成员, 需要通过在 C 语言当中定义辅助函数来完成. 对应 零长数组
的成员, 无法在 Go 语言中直接访问数组的元素, 但其中 零长数组
的成员所在的位置偏移量依然可以通过 unsafe.Offset(a.arr)
来访问.
/*
struct A {
int size:10; // 位字段无法访问
float arr[]; // 零长的数组也无法访问
}
*/
import "C"
func main() {
var a C.struct_A
fmt.Println(a.size)
fmt.Println(a.arr)
}
在 C 语言当中, 无法直接访问 Go 语言定义的结构体类型
联合类型
对于联合类型,可以通过 C.union_xxx
来访问 C 语言中定义的 union xxx
类型. 但是 Go 语言中并不支持C语言联合类型, 它们会被转换为对应大小的字节数组.
/*
#include <stdint.h>
union B {
int i;
float f;
};
union C {
int8_t i8;
int64_t i64;
};
*/
import "C"
func main() {
var b C.union_B
fmt.Printf("%T \n", b) // [4]uint8
var c C.union_C
fmt.Printf("%T \n", c) // [8]uint8
}
如果需要操作C语言的联合类型变量, 一般有三种办法:
第一种是在C语言当中定义辅助函数;
第二种是通过 Go 语言的 encoding/binary
手工解码成员(需要注意大小端的问题)
第三种是使用 unsafe
包强制转换为对应的类型(性能最好的方式)
var ub C.union_B
fmt.Println("b.i:", binary.LittleEndian.Uint32(ub[:]))
fmt.Println("b.f:", binary.LittleEndian.Uint32(ub[:]))
fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&ub)))
fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&ub)))
虽然unsafe包访问最简单, 性能也最好, 但是对于有嵌套联合类型的情况处理会导致问题复杂化. 对于复杂的联合类型, 推荐通过在C语言中定义辅助函数的方式处理.
枚举类型
对于枚举类型, 通过 C.enum_xxx
访问 C 语言当中定义的 enum xxx
结构体类型
/*
enum C {
ONE,
TWO,
};
*/
import "C"
main(){
var c C.enum_C = C.TWO
fmt.Println(c)
fmt.Println(C.ONE)
}
在 C 语言当中, 枚举类型底层对应 int 类型, 支持负数类型的值. 可以通过 C.ONE, C.TWO 等直接访问定义的枚举值.
数组, 字符串和切片
在 C 语言当中, 数组名其实对应着一个指针, 指向特定类型特定长度的一段内存, 但是这个指针不能被修改.
当把数组名传递给一个函数时, 实际上传递的是数组第一个元素的地址.
C 语言的字符串是一个 char 类型的数组, 字符串的长度需要根据表示结尾的NULL字符的位置确定.
Go 当中, 数组是一种值类型, 而且数组的长度是数组类型的一部分.
Go 当中, 字符串对应一个长度确定的只读 byte 类型的内存.
Go 当中, 切片是一个简化版的动态数组.
Go 语言 和 C 语言的数组, 字符串和切片之间的相互转换可以简化为 Go 语言的切片和 C 语言中指向一定长度内存的指针
之间的转换.
// Go String to C String
func C.CString(string) *C.char
// Go []byte to C Array
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.CString 针对输入 Go 字符串, 克隆一个 C 语言格式的字符串; 返回的字符串由 C 语言的 malloc 函数分配, 不使用时需要通过 C 语言的 free 函数释放.
C.Cbytes 函数和 C.CString 类似, 针对输入的 Go 语言切片克一个 C 语言版本的字符数组.
C.GoString 用于将从 NULL 结尾的 C 语言字符串克隆一个 Go语言字符串.
C.GoStringN 是另一个字符数组克隆函数.
C.GoBytes 用于从 C 语言数组, 克隆一个 Go 语言字节切片.
当 C 语言字符串或数组向 Go 语言转换时, 克隆的内存由 Go 语言分配管理. 通过该组转换函数, 转换前和转换后的内存依然在各自的语言环境中, 它们并没有跨域 Go 语言和 C 语言.
克隆方式实现转换的优点是接口和内存管理简单. 缺点是克隆需要分配新的内存和复制操作都会导致额外的开销.
reflect 中字符串和切片的定义:
type StringHeader struct{
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
如果不希望单独分配内存, 可以在Go当中直接访问C的内存空间:
/**
#include <string.h>
char arr[10];
cahr *s = "Hello";
**/
import "C"
func main() {
// 通过 reflect.SliceHeader 转换
var arr0 []byte
var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0))
arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0])
arr0Hdr.Len = 10
arr0Hdr.Cap = 10
// 通过切片语法转换
arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10]
var s0 string
var s0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&s0))
s0Hdr.Data = uintptr(unsafe.Pointer(C.s))
s0Hdr.Len = int(C.strlen(C.s))
sLen := int(C.strlen(C.s))
s1 := string(*[31]byte)(unsafe.Pointer(C.s))[:SLen:SLen]
}
Go 字符串是只读的, 用户需要自己保证 Go 字符串在使用期间, 底层对应的 C 字符串内容不会发生变化, 内存不会被 提前释放掉.
指针间的转换
在 C 语言中, 不同类型的指针是可以显式或隐式转换的, 如果是隐式只是会在编译时给出一些警告信息.
Go 语言对于不同类型的转换非常严格, 任何 C 语言中可能出现的警告信息在 Go 语言中都可能是错误!
指针是 C 语言的灵魂, 指针间的自由转换也是 cgo 代码中经常要解决的第一个问题.
在 Go 语言中两个指针的类型完全一致则不需要转换可以直接使用. 如果一个指针类型是用 type 命令在另一个指针类型基础上构建的, 换言之 两个指针是 "底层结构完全相同" 的指针
, 那么可以通过直接强制转换语法进行指针间的转换. 但是 cgo 经常要面对是2个完全不同类型的指针间的转换, 原则上这种操作在纯Go 语言代码是严格禁止的.
var p *X
var q *Y
q = (*X)(unsafe.Pointer(p)) // *X => *Y
p = (*Y)(unsafe.Pointer(q)) // *Y => *X
为了实现 X 类型和 Y 类型的指针的转换, 需要借助 unsafe.Pointer
作为中间桥接类型实现不同类型指针之间的转换. unsafe.Pointer
指针类型类似 C 语言中的 void*
类型的指针.
指针简单转换流程图:
数值和指针的转换
为了严格控制指针的使用, Go 语言禁止将数值类型直接转为指针类型! 不过, Go 语言针对 unsafe.Pointer
指针类型特别定义了一个 unitptr
类型. 可以以 unitptr
为中介, 实现数值类型到 unsafe.Pointer
指针类型的转换. 再结合前面提到的方法, 就可以实现数值类型和指针的转换了.
int32 类型到 C 语言的 char*
字符串指针类型的相互转换:
切片间的转换
在 C 语言当中数组也是一种指针, 因此两个不同类型数组之间的转换和指针类型间转换基本类似.
在 Go 语言当中, 数组或数组对应的切片不再是指针类型, 因此无法直接实现不同类型的切片之间的转换.
在 Go 的 reflect
包提供了切片类型的底层结构, 再结合前面不同类型直接的指针转换, 可以实现 []X到 []Y 类型的切片转换.
var p []X
var q []Y
pHdr := (*reflect.SliceHeader)(unsafe.Pointer(&p))
qHdr := (*reflect.SliceHeader)(unsafe.Pointer(&q))
qHdr.Data = pHdr.Data
qHdr.Len = pHdr.Len * int(unsafe.Sizeof(p[0])) / int(unsafe.Sizeof(q[0]))
qHdr.Cap = pHdr.Len * int(unsafe.Sizeof(p[0])) / int(unsafe.Sizeof(q[0]))