go cgo intro


原文链接: go cgo intro

本文由 简悦 SimpRead 转码, 原文地址 https://studygolang.com/articles/2629

前提条件:

了解 Go 语言和 C 语言的基本知识和基本用法。

一、什么是 cgo

简单地说,cgo 是在 Go 语言中使用 C 语言代码的一种方式。

二、为什么要有 cgo

C 语言经过数十年发展,经久不衰,各个方面的开源代码、闭源库已经非常丰富。这无疑是一块巨大的宝藏,对于一门现代编程语言而言,如何用好现成的 C 代码就显得极为重要。

三、如何使用

3.1 系统配置

要想使用 cgo,你的计算机上必须有 GCC,并且将 gcc 编译器的可执行文件所在的目录添加到 PATH 这个环境变量中。例如,我的 gcc.exe 在 C:\mingw64\bin 下,所以,要把 C:\mingw64\bin 这个目录添加到 PATH。

3.2 C 假包

我们知道,Go 语言以包为代码的逻辑单元。如果要在 Go 代码中使用 C 代码,也要为 C 代码单独设立一个 “包” 并将其导入:

import "C"

C 是一个假包,包的性质它一般也有。例如可以用 “包名. 符号名” 的方式使用其中的变量或类型。

var n C.int

这行代码,定义了一个 C 语言 int 类型的变量,与用

var conn net.Conn

定义一个 net.Conn 类型的变量没什么语法上的不同。

如果紧挨着 import "C" 这行上方,加入连续若干行注释,在注释中编写 C 代码,这些 C 代码就作为 C 包的内容。例如:

/*
int PlusOne(int n)
{
	return n + 1;
}
*/
import "C"

在 Go 代码中就可以调用 PlusOne 这个函数,再例如:

/*
#include <stdio.h>
*/
import "C"

在 Go 代码中就可以调用头文件 stdio.h 中的函数。

除此之外,还可以把你的 C 源文件放到要使用它的 Go 源文件的同一目录,然后在 C 包中包含(include)对应的头文件。例如,我有 C 源文件 ys_origin.c 和头文件 ys_origin.h,而我要在 ys_origin.go 中调用 ys_origin.c 中的函数,那么,我可以这么做:

/*
include "ys_origin.h"
*/
import "C"

func FuncOne(a int, b string) error {
    // ......
    C.LevelUp()
    // ......
}

下面讲解具体用法。

四、具体介绍

C 语言的数据结构有数字类型(整数和浮点数)、函数、数组、指针、结构体、联合体,很多第三方库的 API 函数也要求提供回掉函数。那就一一道来。

4.1 变量(全局变量)

使用 C 中的全局变量很简单,只要 “C. 变量名” 就可以。

/*
int g_a = 7;
*/
import "C"

func TestVar() {
    fmt.Println(C.g_a) // 7
    C.g_a = 42
    fmt.Println(C.g_a) // 42

    var n int32
    n = int32(C.g_a) + 11
    fmt.Println(n) // 53
}

值得注意的是,Go 不认为 C.int 与 int32 或 int 是同一种类型,所以不能把 C.int 类型的变量直接赋值给 int32 类型的变量,如果要这么做,必须进行类型转换。

4.2 函数

用 “C. 函数名” 来调用函数。

/*
int Sum(int a, int b)
{
    return a + b;
}
*/
import "C"

func TestFunction() {
    var a int32 = 12
    var b int32 = 44
    var s int32 = int32(C.Sum(C.int(a), C.int(b)))
    fmt.Println(s) // 56
}

4.3 数组

数组的用法和变量是一样的。代码用到了 C99 的数组初始化方式。

/*
int a[10] = { [2] = 12, [4] = 77, [7] = 241 };
*/
import "C"

func TestArray() {
    for _, v := range C.a {
        fmt.Printf("%d ", v) // 0 0 12 0 77 0 0 241 0 0
    }
    fmt.Printf("\n")
    C.a[5] = 100
    fmt.Println(C.a[5]) // 100
}

4.4 指针

设 C 代码中有 int * 类型的指针 p,利用 * C.p 就可以获取到它所指向的变量的值(和 C 语言中指针的用法相同),利用 *C.p 则可以修改它所指向的变量的值。

Go 为了安全起见,不允许 * A 和 * B 两种指针直接相互转换。如果想把 C 的指针转换成 Go 的指针,必须使用 unsafe 包中的 Pointer 类型作为媒介。任何指针类型可以转换成 unsafe.Pointer,反之亦然。正如 unsafe 这个包名所示,它是不安全的。设 p 为一个 B * 类型的 C 指针,将其转换为 * T 类型的 Go 指针的方法是:

(*T)(unsafe.Pointer(C.p))

一定要注意的是,T 占据内存的大小不能超过 B 的,否则可能发生 “内存不能为 read” 等各种意外情况。

/*
int b = 6;
int *p = &b;
*/
import "C"

func TestPointer() {
    fmt.Println("b = ", C.b) // b = 6
    *C.p = 92
    fmt.Println("b = ", C.b) // b = 92

    p := (*int32)(unsafe.Pointer(C.p))
    *p = 22
    fmt.Println("b = ", C.b) // b = 22
}

4.5 结构体

在 C 代码中定义了结构体类型 T 之后,在 Go 代码中看到的将会是 C.struct_T 而不是 C.T。当然,如果将结构体 typedef,就不用再写那个 “struct_” 了。

/*
struct POINT_ALPHA
{
    int x;
    int y;
};
typedef struct _POINT_BETA
{
    int x;
    int y;
} POINT_BETA;
*/
import "C"

func TestStruct() {
    var pa C.struct_POINT_ALPHA
    pa.x = 6
    pa.y = 90
    fmt.Println(pa) // {6 90}

    var pb C.POINT_BETA
    pb.x = 33
    pb.y = -10
    fmt.Println(pb) // {33 -10}
}

4.6 联合体

Go 中使用 C 的联合体是比较少见的,而且稍显麻烦,因为 Go 将 C 的联合体视为字节数组。比方说,下面的联合体 LARGE_INTEGER 被视为 [8]byte。

typedef long LONG;
typedef unsigned long DWORD;
typedef long long LONGLONG;

typedef union _LARGE_INTEGER {
    struct {
        DWORD LowPart;
        LONG HighPart;
    };
    struct {
        DWORD LowPart;
        LONG HighPart;
    } u;
    LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER;

所以,如果一个 C 的函数的某个参数的类型为 LARGE_INTEGER,我们可以给它一个 [8]byte 类型的实参,反之亦然。

那么,如果一个 C 函数要求传入一个联合体,我们应该构建一个字节数组作为实参。

/*
typedef long LONG;
typedef unsigned long DWORD;
typedef long long LONGLONG;

typedef union _LARGE_INTEGER {
    struct {
        DWORD LowPart;
        LONG HighPart;
    };
    struct {
        DWORD LowPart;
        LONG HighPart;
    } u;
    LONGLONG QuadPart;
}LARGE_INTEGER, *PLARGE_INTEGER;

void SetNum(LARGE_INTEGER li)
{
    li.u.LowPart = 1;
    li.u.HighPart = 4;
}
*/
import "C"

func TestUnion() {
    var li C.LARGE_INTEGER // 等价于: var li [8]byte
    var b [8]byte = li     // 正确,因为[8]byte和C.LARGE_INTEGER相同
    C.SetNum(b)               // 参数类型为LARGE_INTEGER,可以接收[8]byte
    li[0] = 75
    fmt.Println(li) // [75 0 0 0 0 0 0 0]
    li[4] = 23
    ShowByteArray(li) // 参数类型为[8]byte,可以接收C.LARGE_INTEGER
}

func ShowByteArray(b [8]byte) {
    fmt.Println(b)
}

4.7 回调函数

函数可以看成内存中的一段数据,而 C 语言的函数名代表函数的首地址。向一个函数传递一个回调函数,实际上是把一个函数的首地址传过去。为此,我们需要下面两个函数:

syscall.NewCallback
syscall.NewCallbackCDecl

这两个函数的参数都是一个 interface{},返回值都是一个 uintptr。它们虽然接受 interface{} 类型的参数,但必须传递一个 Go 函数,而且传入的 Go 函数的返回值的大小(size)必须和 uintptr 相同。它们根据一个 Go 函数(内存中的一段数据),生成一个 C 函数(内存中的另一段数据),并将这个 C 函数的首地址返回。两者的不同点是,前者生成的 C 函数是符合stdcall 调用约定的,后者生成的 C 函数是符合cdecl 调用约定的。

在获得函数的首地址之后,还不能直接把它传给 C 函数,因为 C 的指向函数的指针在 Go 中被视为 *[0]byte,所以要转换一下。

C 代码:

#include <stdint.h>
#ifndef NULL
#define NULL ((void*)0)
#endif

typedef uintptr_t(__stdcall* GIRL_PROC)(unsigned int);
typedef uintptr_t(__cdecl* GIRL_PROC_CDECL)(unsigned int);

unsigned int Func1(unsigned int n, GIRL_PROC gp)
{
    if (gp == NULL)
    {
        return 0;
    }
    return (unsigned int)((*gp)(n));
}

unsigned int Func2(unsigned int n, GIRL_PROC_CDECL gp)
{
    if (gp == NULL)
    {
        return 0;
    }
    return (unsigned int)((*gp)(n));
}

Go 代码:

func TestCallback() {
    f1 := syscall.NewCallback(PlusOne)
    f2 := syscall.NewCallbackCDecl(PlusTwo)
    var m uint32 = 20
    var n uint32 = 80

    // Func1 __stdcall
    fmt.Println(C.Func1(C.uint(m), (*[0]byte)(unsafe.Pointer(f1)))) // 21

    // Func2 __cdecl
    fmt.Println(C.Func2(C.uint(n), (*[0]byte)(unsafe.Pointer(f2)))) // 82
}

func PlusOne(n uint32) uintptr {
    return uintptr(n + 1)
}

func PlusTwo(n uint32) uintptr {
    return uintptr(n + 2)
}

C.Func1 的第二个参数类型为函数,所以要传入一个 *[0]byte。

字符串

Golang 通过 CGO 机制能很方便的调用 C 语言。本文介绍一下如何在 Go 中调用稍稍复杂一点 C 函数,例如: char* f(int, int*)

首先看一个最简单的例子,将 Golang 中的一个字符串传入 C 函数中:

package main

import "C"

import "unsafe"

func main() {
    s := "Hello Cgo"
	cs := C.CString(s)
	defer C.free(unsafe.Pointer(cstr))
    C.print(cs)
}

注意上述程序中的关键语句cs := C.CString(s)是将一个 Golang 的字符串转换为 C 语言字符串,该 C 语言字符串是由 C 函数 malloc 从堆中分配的,因此后续需要调用 C.free 释放内存。

函数

然后,我们看看如何调用一个复杂一点的 C 函数?例如: char* f(int, int*) ,返回一个char*指针,并且有一个参数也是返回值int*。请直接看下面的例子:

package main

import "C"
import "unsafe"
import "fmt"

func main() {
	rlen := C.int(0)
	len := 10
	cstr := C.xmalloc(C.int(len), &rlen)
	defer C.free(unsafe.Pointer(cstr))
	gostr := C.GoStringN(cstr, rlen)
	fmt.Printf("retlen=%v\n", rlen)
	println(gostr)
}

xmalloc函数的第二个参数是int*,这里设计为一个输入、输出参数。我们在 Golang 中使用 C.int 类型的指针就可以; 其返回值是一个char*,在 Golang 中就是 *C.char,由于返回值是指针,其内存由 malloc 分配,因此需要在 Golang 中对其内存进行释放。

再然后,我们看看如何调用一个返回结构体的 C 函数?例如:struct MyString xmalloc(int len)。请看示例代码:

package main

import "C"
import "unsafe"
import "fmt"

func main() {
	len := 10
	str := C.xmalloc(C.int(len))
	defer C.free(unsafe.Pointer(str.s))
	gostr := C.GoStringN(str.s, str.len)
	fmt.Printf("retlen=%v\n", str.len)
	println(gostr)
}

数组、字符串和切片

在C语言中,数组名其实对应于一个指针,指向特定类型特定长度的一段内存,但是这个指针不能被修改;当把数组名传递给一个函数时,实际上传递的是数组第一个元素的地址。为了讨论方便,我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。

在Go语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。

Go语言和C语言的数组、字符串和切片之间的相互转换可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换。

  1. 通过CGO转换函数 在各自空间保留副本, 使用完成后自己释放内存。
    CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:
    ```go
    // 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
    func C.CBytes([]byte) unsafe.Pointer

func C.GoString(*C.char) string
func C.GoStringN(*C.char, C.int) string
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语言。克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。

在reflect包中有字符串和切片的定义:
```go
type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
  1. GO直接访问C内存空间中的数组和字符串

如果不希望单独分配内存,可以在Go语言中直接访问C语言的内存空间:

/*
static char arr[10];
static char *s = "Hello";
*/
import "C"
import "fmt"

func main() {
    // 1. 在GO中访问C的数组
	// 通过 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]
    
    // 2. 在GO中访问C的字符串, 创建go的 StringHeader,然后把C中的地址赋值过来。
	var s0 string
	var s0Hdr := (*reflect.StringHeader)(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[0]))[:sLen:sLen])
}

因为Go语言的字符串是只读的,用户需要自己保证Go字符串在使用期间,底层对应的C字符串内容不会发生变化、内存不会被提前释放掉。

在CGO中,会为字符串和切片生成和上面结构对应的C语言版本的结构体:

typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
在C语言中可以通过GoString和GoSlice来访问Go语言的字符串和切片。如果是Go语言中数组类型,可以将数组转为切片后再行转换。如果字符串或切片对应的底层内存空间由Go语言的运行时管理,那么在C语言中不能长时间保存Go内存对象。

切片间的转换

在C语言中数组也一种指针,因此两个不同类型数组之间到转换和指针间转换基本类似。但是在Go语言中,数组或数组对应到切片都不再是指针类型,因为我们也就无法直接实现不同类型到切片之间的转换。

不过Go语言的reflect包提供了切片类型到底层结构,再结合前面讨论到不同类型之间到指针转换技术就可以实现[]X和[]Y类型的切片转换:

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错误,实际代码需要根据情况酌情处理。

五、综合示例

正在写。

六、练习

以后我会制作一些习题。

`