前提条件:
了解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 AAA(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.AAA(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。
五、综合示例
正在写。
六、练习
以后我会制作一些习题。
有疑问加站长微信联系(非本文作者)