cgo 系列文章之cgo类型 (二)

常用的cgo类型

数值类型

CCGOGo
charC.charbyte
singed charC.scharint8
unsigned charC.ucharuint8
shortC.shortint16
intC.intint32
longC.longint32
long long intC.longlongint64
floatC.floatfloat32
doubleC.doublefloat64
size_tC.size_tuint

结构体, 联合, 枚举类型

结构体

在 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]))
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值