cgo 的魔法


cgo 访问 c 标准库

当我们需要用 go 调用 c 语言写的接口时,就需要用 cgo 进行连接,cgo 属于 go 语言的一个特殊工具,直接通过一个例子来看

package main

/*         
#include <stdio.h>
#include <stdlib.h>
*/
import "C"     
// 上面注释部分是导入 c 的标准库头文件, 必须用注释包裹在内且在 import "C" 上面一行,
// go编译器读到上面的注释时,会自行跳转至 gcc 编译, 所以注释中不能出现除 c 语言以外的语法
// "C" 并不是 go 的基础包,相当于一个虚拟包,import 相当于一个启用开关,该句表示该程序已经启用 cgo 的特性,并且只能单独一行,不能和其他包一块导入

import "unsafe"    
// unsafe 包如其名,并不安全,该包常用于 c 和 go 之间的指针转换和地址运算         

func main() {
  // CString() 是 go 内部的接口,可以转换 go 的字符串为 c 语言字符串格式 char*
  cs := C.CString("Hello, World\n")
  // 因为 CString 使用 c 的 malloc 申请空间,go GC 不会清理,用完后需要手动 free
  // unsafe.Pointer 相当于 c 的 void*,这里做了强制转换
  defer C.free(unsafe.Pointer(cs))
  // 调用c接口方式为 C.api(), 
  C.puts(cs)
}

想要看具体的实现,可以通过命令行 go tool cgo [filename.go] 查看编译过程中间生成的文件,看到具体的类型转换,更多 cgo 命令用法见官方cgo

cgo 类型转换

go 传给 c 的参数都必须使用 C.cType 转换成 c 的类型,这里着重谈一下 string 的转换,更多类型转换请阅读 Go语言高级编程

因为 c 并没有 string 类型,只有 char[], 所以 go 内置了 string 的转换函数,如下所示,可以根据需要自行选择

// 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

上述的函数,均是通过拷贝的方式进行数据转换,将 go string转为 c char*时,使用 malloc 申请内存空间并拷贝数据到对应的地址,通过手动调用 free 释放内存;将 c char* 转换为 string 时,go 自行申请内存空间,并将 c 的数据拷贝出来,内存交由 GC 处理。通过拷贝 可以使 go 的内存和 c 的内存互不干扰,简化了内存管理和接口调用,但是内存申请和拷贝也增加了性能的损耗。

除了上述字符串、字节数组以外的类型转换,都是 c 和 go 共用地址,取决于内存是谁申请的,如下面代码所示:

package main

/*
int arr[10];
void add(int *a, int n) {
  for (int i = 0; i< n; i++) {
    printf("%d ",a[i]);
    a[i]++;
  }
}
*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  arr1 := (*[10]int32)(unsafe.Pointer(&C.arr[0]))[:10:10] // 将 C.arr 转换成[10]int32 数组,然后从数组中截取长度和容量均为10切片,
  arr1[1] = 123           // 修改会直接修改 c 的内存
  fmt.Printf("%+v %[1]T %v\n", arr1, C.arr) // [0 123 0 0 0 0 0 0 0 0]   []int32 [0 123 0 0 0 0 0 0 0 0]
  C.arr[2] = C.int(456)
  fmt.Printf("%+v %[1]T %v\n", arr1, C.arr) // [0 123 456 0 0 0 0 0 0 0] []int32 [0 123 456 0 0 0 0 0 0 0]
  arr1 = append(arr1, 1)  // 这里 arr1 切片已经扩容,重新分配内存,脱离了 C.arr
  arr1[3] = 1
  C.arr[3] = C.int(789)
  fmt.Printf("%+v %v len(arr1):%d cap(arr1):%d\n", arr1, C.arr, len(arr1), cap(arr1)) // [0 123 456 1 0 0 0 0 0 0 1] [0 123 456 789 0 0 0 0 0 0] len(arr1):11 cap(arr1):20

  a := []int32{1, 2, 3}
  C.add((*C.int)(unsafe.Pointer(&a[0])), C.int(len(a)))
  fmt.Println(a)  // [2 3 4]
}

所以需要额外注意, go 传给 c 的参数若是像 slicemap 等内存可能发生改变的类型,尽可能不要发生扩容现象,或者申请内存时,直接用 C.malloc() 申请固定大小 c 的地址,确保内存不会发生改变,

unsafe.Pointer

unsafe 包是 go 提供的针对类型安全操作,具体接口查看官网doc,这里主要谈一下 unsafe.Pointer 类型

在使用 cgo 过程中不可避免要传递指针参数,因为 go 的指针类型不能像 c 的指针类型可以隐式转换,go 需要用到类似于 void*unsafe.Pointer 类型强制转换成 c 可以用的指针,而且可以转换任意类型的指针,而且可以绕过类型检测读写内存,所以,使用时需要格外小心。 Pointer 有如下四种特殊操作:

- 任何类型的指针值都可以转换为 Pointer。 
- Pointer 可以转换为任何类型的指针值。 
- uintptr 可以转换为指针。 
- 指针可以转换为 uintptr。 

Pointer转换实例

下面内容翻译自 go unsafe

以下涉及 Pointer 的模式是有效的。不使用这些模式的代码今天可能无效,或者将来可能无效。甚至下面的有效模式也带有重要的警告。

运行 go vet 可以帮助查找不符合这些模式的 Pointer 用法,但是运行 go vet 没警告不能保证这些代码是有效。

1. 将 *T1 转换为指向 *T2 的指针

假设T2不大于T1,并且两个共享相同的内存布局,则此转换允许将一种类型的数据重新解释为另一种类型的数据。一个示例是math.Float64bits的实现:

func Float64bits(f float64) uint64 {
  return *(*uint64)(unsafe.Pointer(&f))
}

2. 将Pointer转换为uintptr(但不转换回Pointer

Pointer 转换为 uintptr 会生成所指向的值的内存地址(整数)。这种 uintptr 的通常用法是打印它。通常,将 uintptr 转换回 Pointer 是无效的。
uintptr是整数,而不是引用。 将 Pointer 转换为 uintptr 会创建一个没有指针语义的整数值。 即使uintptr保留了某个对象的地址,垃圾回收器也不会在对象移动时更新该uintptr的值,该uintptr也不会使该对象被回收。

其余的模式枚举了从uintptrPointer的唯一有效转换。

3. 用算术将Pointer转换为uintptr并返回

如果p指向已分配的对象,则可以通过转换为 uintptr,添加偏移量并将其转换回 Pointer 的方式将其推进对象。

p = unsafe.Pointer(uintptr(p) + offset)

此模式最常见的用法是访问数组的结构或元素中的字段:

// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))

// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

以这种方式从指针添加和减去偏移量都是有效的。通常使用 &^ 舍入指针(通常用于对齐)也是有效的。在所有情况下,结果都必须继续指向原始分配的对象。
与C语言不同,将指针移到其原始分配的末尾是无效的:

// INVALID: end points outside allocated space.
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))

// INVALID: end points outside allocated space.
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))

注意uintptrPointer 两个转换必须出现在相同的表达式中,并且它们之间只有中间的算术运算:

// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := uintptr(p)
p = unsafe.Pointer(u + offset)

注意:待转换的指针必须指向已分配的对象,不能为nil。

// INVALID: conversion of nil pointer
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)

4. 调用 syscall.Syscall 时将指针转换为 uintptr

syscall 包中的 Syscall 函数将其 uintptr 参数直接传递给操作系统,然后,操作系统可以根据调用的详细信息将其中一些参数重新解释为指针。 也就是说,系统调用实现正在将某些参数从 uintptr 隐式转换回指针。
如果必须将指针参数转换为 uintptr 用作参数,则该转换必须出现在调用表达式本身中:

syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

编译器通过安排保留引用的分配对象(如果有的话),直到调用完成(即使从类型本身而言)也不会移动,来处理在汇编中实现的函数的调用的参数列表中转换为uintptrPointer。 似乎在调用过程中不再需要该对象。
为了使编译器能够识别这种模式,转换必须出现在参数列表中:

// INVALID: uintptr cannot be stored in variable
// before implicit conversion back to Pointer during system call.
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))

5. 将 reflect.Value.Pointerreflect.Value.UnsafeAddr 的结果从 uintptr 转换为 Pointer

包反射的名为 PointerUnsafeAddrValue 方法返回类型 uintptr 而不是 unsafe.Pointer,以防止调用者在不首先导入 "unsafe" 的情况下将结果更改为任意类型。 但是,这意味着结果很脆弱,必须在调用后立即使用相同的表达式将其转换为Pointer:

p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

与上述情况一样,在转换之前存储结果是无效的:

// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))

6. 将一个 reflect.SliceHeaderreflect.StringHeader 数据字段与指针进行转换

与前面的情况一样,反射数据结构 SliceHeaderStringHeader 将字段 Data 声明为 uintptr,以防止调用者在不首先导入 "unsafe" 的情况下将结果更改为任意类型。 但是,这意味着 SliceHeaderStringHeader 仅在解释实际切片或字符串值的内容时才有效。

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n

在这种用法中,hdr.Data 实际上是在字符串标题中引用基础指针的另一种方法,而不是 uintptr 变量本身。
通常,reflect.SliceHeaderreflect.StringHeader 只能用作指向实际切片或字符串的 *reflect.SliceHeader*reflect.StringHeader,而不能用作纯结构。 程序不应声明或分配这些结构类型的变量。

// INVALID: a directly-declared header will not hold Data as a reference.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p possibly already lost

文章作者: MaZhuang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 MaZhuang !
  目录