CGO 涉及的数据类型转换包含一下内容:
- 数值类型
- 字符串和切片类型
- 结构体、联合体、枚举类型‘
- 数组类型
- 指针类型
- 数组和指针间的转换
- 切片和切片之间的转换
前面 3 个咱们在上一篇短文已经梳理到了,接下来继续
数组类型
C 语言里面:
- 数组
C 语言里面,数组名对应一个指针,指向特定类型特定长度的一段内存,但是这个指针不能被修改
C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定
- 字符串
是一个 char 类型的数组
- 切片
C 语言没有切片的概念
GO 语言里面:
- 数组
数组是一种值类型,而且数组的长度是数组类型的一个部分
- 字符串
就是一段长度确定的只读byte类型的内存
- 切片
是一个简单的动态数组
从上面我们可以看出来,C 语言 和 GO 语言的数组,切片,字符串的相互转换,就可以是指针和指针指向的内存长度的转换
CGO 官方给咱们提供了如下 5 个函数,用于 C 语言和 GO 语言互相转换:
- func C.CString(string) *C.char
C.CString
将传入的 go 字符串,克隆成一个 C 格式的字符串,克隆出来的字符串是使用 C 语言中 malloc 开辟出来的,因此我们用完了这个函数,需要手动去释放内存
- func C.CBytes([]byte) unsafe.Pointer
C.CBytes
用于将输入的 go byte 类型的数组(切片),克隆并转换成 C 语言的指针,指针的是一个数组,需要开辟空间,不用的时候,也是需要手动释放
- func C.GoString(*C.char) string
C.GoString
将 C 的字符串克隆成 GO 的 string , GO 里面自己会释放内存
- func C.GoStringN(*C.char, C.int) string
C.GoStringN
,将C 具体某个长度的字符串转换成 GO 的 string, GO 里面自己会释放内存
- func C.GoBytes(unsafe.Pointer, C.int) []byte
C.GoBytes
将 C 的数组,转换成 GO 的切片
小结:
上述一组官方提供的函数,GO 语言和 C 语言相互转换都是通过克隆的方式实现
GO 转 C
C 是通过 malloc 的方式 在 C 自己的空间中开辟内存,因此我们不需要使用的时候,需要释放
C 转 GO
也是通过克隆的方式,但是 GO 有自己的内存管理,不需要我们手动释放内存
上述函数的优势
- 以克隆的方式进行转换,内存都是在各自语言内开辟的,内存管理简单
上述函数的劣势
- 相互转换都需要额外开辟内存,增大了系统开销
指针和指针间的转换
在 cgo 里面,如何实现指针和指针间的转换呢?
C 语言里面:
指针是 C 语言的灵魂,C 语言里面,不同类型指针间可以强制转换,指针间的自由转换也是 cgo 代码中经常要解决的第一个重要的问题
GO 语言里面:
不同类型的转换非常严格,任何 C 语言中可能出现的警告信息在Go语言中都可能是错误。
GO 里面不同类型的指针是禁止转换的,但是有了 CGO 就打破了这种禁锢,可以使用 unsafe.pointer 指针类型作为桥梁来进行转换
例如
var a *A
var b *B
b = (*b)(unsafe.Pointer(a)) // *A => *B
a = (*a)(unsafe.Pointer(b)) // *B => *A
数值和指针间的转换
在 cgo 里面,如何实现数值和指针的转换呢?
GO 语言里面:
禁止将数值类型直接转为指针类型
但是有了 cgo ,我们就有办法了,go 中 对unsafe.Pointer
指针类型特别定义了一个uintptr 类型 ,我们仍然是将他们作为桥梁,进行转换成我们目的指针
例如,咱们一个 GO 里面的 int32 的数值,如何转换成 C 里面的 指针呢?
就像上面说到的,咱们利用好这个桥梁,将 int32 转成 uintptr,再转成 unsafe.pointer,最后转成 C 的 char 指针
切片和切片之间的转换
在 cgo 里面,如何实现切片和切片之间的转换呢?
切片和切片之间的转换就要用到 GO 里面 reflect 包提供的数据结构了,
因为 GO 里面,数组或者切片已经不是指针类型了,需要通利用 reflect 里面的数据结构来进行转换,如下:
//它的引用不会被垃圾回收,因此程序必须使用正确类型的指向底层数据的指针
type StringHeader struct {
Data uintptr
Len int
}
//它的引用不会被垃圾回收,因此程序必须使用正确类型的指向底层数据的指针
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
切片 A 如何转换成 切片 B?
我们可以这样来进行转换,先构造一个空的切片
var p []int
var q []string
pk := (*reflect.SliceHeader)(unsafe.Pointer(&p))
qk := (*reflect.SliceHeader)(unsafe.Pointer(&q))
再用原来的数据来填充这个空切片,此处需要注意 len 和 cap 的计算和赋值
pk.Data = qk.Data
pk.Len = qk.Len * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
pk.Cap = qk.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
函数调用
基本的函数调用我们在上一篇短文已经演示过了,咱们来看看其他的
- C 函数自身的返回值,在 GO 里面是如何应用的
C 函数自身的返回值,在 GO 里面是如何应用的
咱们写一个有返回值的 C 函数,然后 GO 再去调用:
C 语言不支持多个返回结果,但是 GO 语言支持返回过个结果,CGO 里面 我们可以用 <errno.h>
标准库里面的 errno
宏用于返回错误状态
package main
/*
#include <errno.h>
static int testadd(int a, int b) {
if (a < 0){
errno = EINVAL;
return 0;
}
return a+b;
}
*/
import "C"
import "fmt"
func main() {
v0, err0 := C.testadd(-2, 1)
fmt.Println(v0, err0)
v1, err1 := C.testadd(1, 2)
fmt.Println(v1, err1)
}
执行结果如下:
0 invalid argument
3 <nil>
若 C 的函数是无返回值的,我们同样是可以这样处理的。
例如可以是这样
v, _ := C.xxx()
fmt.Printf("%#v", v) // 输出为 main._Ctype_void{}
fmt.Println(C.xxx()) // 输出为 [] 0长的数组类型[0]byte
咱们实际实践了之后,发现 C 语言的v oid 类型对应的是当前的 main 包中的_Ctype_void
类型
滴水穿石,一步一步的学
参考资料: