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语言中指向一定长度内存的指针之间的转换。
- 通过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
}
- 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错误,实际代码需要根据情况酌情处理。
五、综合示例
正在写。
六、练习
以后我会制作一些习题。