CGO,基本数据类型转换2 和 函数调用

Golang
339
0
0
2022-11-12

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类型

滴水穿石,一步一步的学

参考资料:

GO 高级编程