cgo讲义及练习
独立风华 · · 9565 次点击 · · 开始浏览前提条件:
了解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。
五、综合示例
正在写。
六、练习
以后我会制作一些习题。
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传
收入到我管理的专栏 新建专栏
前提条件:
了解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。
五、综合示例
正在写。
六、练习
以后我会制作一些习题。