cgo讲义及练习

独立风华 · · 8172 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前提条件:

了解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。


五、综合示例

正在写。


六、练习

以后我会制作一些习题。


有疑问加站长微信联系(非本文作者)

本文来自:开源中国博客

感谢作者:独立风华

查看原文:cgo讲义及练习

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

8172 次点击  
加入收藏 微博
下一篇:golang misc
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传