choupiaoyi 2020-05-29
C.int对应C语言的int类型
。有些C语言的类型是由多个关键字组成,但通过虚拟的“C”包访问C语言类型时名称部分不能有空格字符
,比如unsigned int
不能直接通过C.unsigned int
访问。因此CGO为C语言的基础数值类型都提供了相应转换规则,比如C.uint对应C语言的unsigned int
。int和long类型都是对应4个字节的内存大小
,size_t类型可以当作Go语言uint无符号整数
类型对待。C语言的int固定为4字节的大小
,但是Go语言自己的int和uint却在32位和64位系统下分别对应4个字节和8个字节大小
。如果需要在C语言中访问Go语言的int类型,可以通过GoInt
类型访问,GoInt类型在CGO工具生成的_cgo_export.h
头文件中定义。其实在_cgo_export.h头文件中,每个基本的Go数值类型都定义了对应的C语言类型,它们一般都是以单词Go为前缀。下面是64位环境下,_cgo_export.h头文件生成的Go数值类型的定义,其中GoInt和GoUint类型分别对应GoInt64和GoUint64:typedef signed char GoInt8; typedef unsigned char GoUint8; typedef short GoInt16; typedef unsigned short GoUint16; typedef int GoInt32; typedef unsigned int GoUint32; typedef long long GoInt64; typedef unsigned long long GoUint64; typedef GoInt64 GoInt; typedef GoUint64 GoUint; typedef float GoFloat32; typedef double GoFloat64;
GoInt
和GoUint
之外,我们并不推荐直接访问GoInt32、GoInt64等类型
。更好的做法是通过C语言的C99标准引入的<stdint.h>头文件。为了提高C语言的可移植性,在<stdint.h>文件中,不但每个数值类型都提供了明确内存大小,而且和Go语言的类型命名更加一致。<stdint.h>
头文件类型对比如表unsigned short
不能直接通过C.unsigned short
访问)。但是,在<stdint.h>
中通过使用C语言的typedef关键字将unsigned short重新定义为uint16_t
这样一个单词的类型后,我们就可以通过C.uint16_t
访问原来的unsigned short
类型了。对于比较复杂的C语言类型,推荐使用typedef关键字提供一个规则的类型命名,这样更利于在CGO中访问。_cgo_export.h
头文件中还会为Go语言的字符串、切片、字典、接口和管道等特有的数据类型生成对应的C语言类型:typedef struct { const char *p; GoInt n; } GoString; typedef void *GoMap; typedef void *GoChan; typedef struct { void *t; void *v; } GoInterface; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
字符串和切片
在CGO中有一定的使用价值,因为CGO为他们的某些GO语言版本的操作函数生成了C语言版本
,因此二者可以在Go调用C语言函数时马上使用;而CGO并未针对其他的类型提供相关的辅助函数,且Go语言特有的内存模型导致我们无法保持这些由Go语言管理的内存指针,所以它们C语言环境并无使用的价值。//export helloString func helloString(s string) {} //export helloSlice func helloSlice(s []byte) {}
_cgo_export.h
头文件会包含以下的函数声明:extern void helloString(GoString p0); extern void helloSlice(GoSlice p0);
_cgo_export.h
头文件产生依赖,而这个头文件是动态输出的。_GoString_
预定义类型,可以降低在cgo代码中可能对_cgo_export.h头文件产生的循环依赖的风险。我们可以调整helloString函数的C语言声明为:extern void helloString(_GoString_ p0);
_GoString_
是预定义类型,我们无法通过此类型直接访问字符串的长度和指针等信息。Go1.10同时也增加了以下两个函数用于获取字符串结构中的长度和指针信息:size_t _GoStringLen(_GoString_ s); const char *_GoStringPtr(_GoString_ s);
不能作为匿名成员被嵌入到Go语言的结构体中
。在Go语言中,我们可以通过C.struct_xxx
来访问C语言中定义的struct xxx
结构体类型。结构体的内存布局按照C语言的通用对齐规则,在32位Go语言环境C语言结构体也按照32位对齐规则,在64位Go语言环境按照64位的对齐规则。对于指定了特殊对齐规则的结构体,无法在CGO中访问。/* struct A { int i; float f; }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a.i) fmt.Println(a.f) }
下划线
来访问:/* struct A { int type; // type 是 Go 语言的关键字 }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a._type) // _type 对应 type }
/* struct A { int type; // type 是 Go 语言的关键字 float _type; // 将屏蔽CGO对 type 成员的访问 }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a._type) // _type 对应 _type }
操作位字段成员
,需要通过在C语言中定义辅助函数来完成。对应零长数组的成员
,无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)来访问。/* struct A { int size: 10; // 位字段无法访问 float arr[]; // 零长的数组也无法访问 }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a.size) // 错误: 位字段无法访问 fmt.Println(a.arr) // 错误: 零长的数组也无法访问 }
C.union_xxx
来访问C语言中定义的union xxx
类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组
。/* #include <stdint.h> union B1 { int i; float f; }; union B2 { int8_t i8; int64_t i64; }; */ import "C" import "fmt" func main() { var b1 C.union_B1; fmt.Printf("%T\n", b1) // [4]uint8 var b2 C.union_B2; fmt.Printf("%T\n", b2) // [8]uint8 }
定义辅助函数
;第二种是通过Go语言的"encoding/binary"
手工解码成员(需要注意大端小端问题);第三种是使用unsafe
包强制转型为对应类型(这是性能最好的方式)。下面展示通过unsafe包访问联合类型成员的方式:/* #include <stdint.h> union B { int i; float f; }; */ import "C" import "fmt" func main() { var b C.union_B; fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b))) fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b))) }
C.enum_xxx
来访问C语言中定义的enum xxx
结构体类型。/* enum C { ONE, TWO, }; */ import "C" import "fmt" func main() { var c C.enum_C = C.TWO fmt.Println(c) fmt.Println(C.ONE) fmt.Println(C.TWO) }
// 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). 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). 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语言字节切片。当Go语言字符串和切片向C语言转换时,克隆的内存由C语言的malloc函数分配,最终可以通过free函数释放
。当C语言字符串或数组向Go语言转换时,克隆的内存由Go语言分配管理
。通过该组转换函数,转换前和转换后的内存依然在各自的语言环境中,它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单
,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销
。type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int }
package main /* #include <string.h> char arr[10]; char *s = "Hello"; */ import "C" import ( "fmt" "reflect" "unsafe" ) 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 fmt.Println(arr0) var s0 string var s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0)) s0Hdr.Data = uintptr(unsafe.Pointer(C.s)) s0Hdr.Len = int(C.strlen(C.s)) fmt.Println(s0) // 通过切片语法转换 arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10] fmt.Println(arr1) sLen := int(C.strlen(C.s)) s1 := string((*[31]byte)(unsafe.Pointer(C.s))[:sLen:sLen]) fmt.Println(s1) }
typedef struct { const char *p; GoInt n; } GoString; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
GoString
和GoSlice
来访问Go语言的字符串和切片。如果是Go语言中数组类型,可以将数组转为切片后再行转换。如果字符串或切片对应的底层内存空间由Go语言的运行时管理,那么在C语言中不能长时间保存Go内存对象
。直接强制转换语法进行指针间的转换
。但是cgo经常要面对的是2个完全不同类型的指针间的转换,原则上这种操作在纯Go语言代码是严格禁止
的。var p *X var q *Y q = (*Y)(unsafe.Pointer(p)) // *X => *Y p = (*X)(unsafe.Pointer(q)) // *Y => *X
unsafe.Pointer
作为中间桥接类型实现不同类型指针之间的转换。unsafe.Pointer
指针类型类似C语言中的void*
类型的指针。unsafe.Pointr
指针类型特别定义了一个uintptr
类型。我们可以uintptr
为中介,实现数值类型到unsafe.Pointr
指针类型到转换。再结合前面提到的方法,就可以实现数值和指针的转换了。int32
类型到C语言的char*
字符串指针类型的相互转换:package main // char * Cc; import "C" import "unsafe" var goi int32 func main() { goi = int32(uintptr(unsafe.Pointer(&C.Cc))) C.Cc = (* C.char)(unsafe.Pointer(uintptr(goi))) }
int32到uintptr
类型,然后是uintptr到unsafe.Pointr
指针类型,最后是unsafe.Pointr指针类型到*C.char
类型。var p []X var q []Y pHdr := (*reflect.SliceHeader)(unsafe.Pointer(&p)) qHdr := (*reflect.SliceHeader)(unsafe.Pointer(&q)) pHdr.Data = qHdr.Data pHdr.Len = qHdr.Len * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0]) pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
如果X和Y类型的大小不同,需要重新设置Len和Cap属性
。需要注意的是,如果X或Y是空类型,上述代码中可能导致除0错误
,实际代码需要根据情况酌情处理。