Go 博客

C? Go? Cgo!

Andrew Gerrand
2011 年 3 月 17 日

引言

Cgo 允许 Go 包调用 C 代码。给定一个包含一些特殊功能的 Go 源文件,cgo 会输出 Go 和 C 文件,这些文件可以合并成一个 Go 包。

以一个例子开始,这是一个 Go 包,它提供了两个函数 - RandomSeed - 分别包装了 C 的 randomsrandom 函数。

package rand

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

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}

让我们从导入语句开始,看看这里发生了什么。

rand 包导入了 "C",但你会在标准 Go 库中找不到这样的包。那是因为 C 是一个“伪包”,一个 cgo 解释为对 C 命名空间引用的特殊名称。

rand 包包含对 C 包的四个引用:对 C.randomC.srandom 的调用,对 C.uint(i) 的转换,以及 import 语句。

Random 函数调用标准 C 库的 random 函数并返回结果。在 C 中,random 返回一个 C 类型 long 的值,cgo 将其表示为 C.long 类型。在使用此包外部的 Go 代码之前,必须将其转换为 Go 类型,使用普通的 Go 类型转换。

func Random() int {
    return int(C.random())
}

这是一个使用临时变量来更清晰地说明类型转换的等效函数。

func Random() int {
    var r C.long = C.random()
    return int(r)
}

Seed 函数在某种程度上做了相反的事情。它接受一个常规的 Go int,将其转换为 C 的 unsigned int 类型,然后将其传递给 C 函数 srandom

func Seed(i int) {
    C.srandom(C.uint(i))
}

请注意,cgo 将 unsigned int 类型识别为 C.uint;有关这些数字类型名称的完整列表,请参阅 cgo 文档

这个例子中我们还没有检查的一个细节是 import 语句上面的注释。

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

Cgo 识别此注释。任何以 #cgo 开头并后跟空格的行都会被删除;这些成为 cgo 的指令。其余的行在编译包的 C 部分时用作头文件。在这种情况下,这些行只是一个单独的 #include 语句,但它们几乎可以是任何 C 代码。#cgo 指令用于在构建包的 C 部分时为编译器和链接器提供标志。

有一个限制:如果你的程序使用了任何 //export 指令,那么注释中的 C 代码只能包含声明(extern int f();),而不能包含定义(int f() { return 1; })。你可以使用 //export 指令使 Go 函数可供 C 代码访问。

#cgo//export 指令在 cgo 文档中有详细说明。

字符串和其他

与 Go 不同,C 没有显式的字符串类型。C 中的字符串由一个以零结尾的字符数组表示。

Go 和 C 字符串之间的转换是通过 C.CStringC.GoStringC.GoStringN 函数完成的。这些转换会复制字符串数据。

下一个例子实现了一个 Print 函数,它使用 C 的 stdio 库中的 fputs 函数将字符串写入标准输出。

package print

// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

func Print(s string) {
    cs := C.CString(s)
    C.fputs(cs, (*C.FILE)(C.stdout))
    C.free(unsafe.Pointer(cs))
}

C 代码进行的内存分配不会被 Go 的内存管理器知道。当你使用 C.CString(或任何 C 内存分配)创建 C 字符串时,你必须记住在完成后通过调用 C.free 来释放内存。

调用 C.CString 会返回指向字符数组起始位置的指针,所以在函数退出之前,我们将其转换为 unsafe.Pointer 并通过 C.free 释放内存分配。cgo 程序中的一个常见模式是在分配后立即 defer 释放(尤其是在后续代码比单个函数调用更复杂时),如下面对 Print 的重写。

func Print(s string) {
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.fputs(cs, (*C.FILE)(C.stdout))
}

构建 cgo 包

要构建 cgo 包,只需像往常一样使用 go buildgo install。go 工具会识别特殊的 "C" 导入,并自动为这些文件使用 cgo。

更多 cgo 资源

有关 C 伪包和构建过程的更多详细信息,请参阅 cgo 命令文档。Go 树中的 cgo 示例演示了更高级的概念。

最后,如果你好奇所有这些是如何在内部工作的,可以看看运行时包的 cgocall.go 的介绍性注释。

下一篇文章:Gobs of data
上一篇文章:Go 变得更稳定
博客索引