Cgo介绍

本文翻译自《C? Go? Cgo!》。

Andrew Gerrand

2011/03/17

介绍

Cgo可以让Go包调用C代码。给定一个使用一些特殊特性编写的Go源文件,cgo输出Go和C文件,它们可以组合成单个Go包。

举个例子,这里有一个Go包,它提供了两个函数——RandomSeed——用来包装C的random函数和srrandom函数。

package rand

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

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

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

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

rand包导入“C”,但你会发现在标准Go库中没有这个包。这是因为C是一个“伪包(pseudo-package)”,一个被cgo解释为引用C命名空间的特殊名称。

rand包包含对C包的四个引用:对C.randomC.srrandom的调用、C.uint(i)import "C"语句。

Random函数调用标准C库的random函数并返回结果。在C语言中,random返回一个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函数srrandom

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

注意,cgo知道C语言的unsigned int类型用C.uint来表示;有关这些数值类型的名称的完整列表,请参阅cgo文档

我们还没有研究这个例子的一个细节是import语句上方的注释。

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

Cgo认识这一注释。任何以#cgo开头、后跟一个空格字符的行都将被删除;这些都成为cgo的指令。在编译Go包的C语言部分代码时,剩余的行用作C语言头部。在上例中,这些行只是一个#include语句,但它们几乎可以是任何C代码。在构建Go包的C语言部分时,#cgo指令用于为编译器和链接器提供标志。

有一个限制:如果你的程序使用任何//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))
}

Go的内存管理器看不见由C代码进行的内存分配。因此当你使用C.CString(或任何C内存分配)创建一个C字符串时,你必须记住在完成调用后使用C.free释放内存。

C.CString的调用返回一个指向char数组头部的指针,我们将其转换为一个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相关资源

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

最后,如果你想知道在内部这一切是如何工作的,请查看运行时包的cgocall.go的介绍性注释。

C和C++有符号数溢出是一种未定义行为

本文翻译自《Signed Overflow》。

C和C++语言标准规定,有符号数的溢出是未定义的行为。在C99标准的第6.5节。在C++98标准中,它位于第5节[expr]的第5段。这意味着正确的C/C++程序在计算表达式时绝不能生成有符号数溢出。这也意味着编译器可能会假设程序永远不会产生有符号数溢出。

gcc在做优化时很早就利用了这一事实。例如,考虑这个函数:

int f(int x) { return 0x7ffffff0 < x && x + 32 < 0x7fffffff; }

如果有符号算术只是在溢出时回绕,则这等价于0x7ffffff0 < x,因为将32加到这样的值将产生负数。然而,即使是2001年发布的gcc 2.95.3,也会将此函数优化为只返回0的代码。这是有效的,因为编译器可能会假设在正确的程序中不会发生有符号数溢出,并且x + 32得到一个负数的情况只能从有符号数溢出发生。

最近gcc已经开始使用有符号数溢出这一未定义行为来实现更好的边界测试。特别是,考虑这样的代码:

int f()
{
int i;
int j = 0;
for (i = 1; i > 0; i += i)
++j;
return j;
}

这段代码假设如果它不断地将一个正数加倍,它最终会得到一个负数。也就是说,它期望有符号数溢出以某种方式运行。当前主流的gcc会认为在正确的程序中不会发生有符号数的溢出,并将这段代码编译成无限循环。

一些gcc用户对这种优化感到惊讶,并且在2007年初左右提出了强烈抗议。有一段时间有人建议gcc编译应该始终使用-fwrapv选项-fwrapv告诉编译器将有符号数的溢出视为回绕。这是Java定义有符号数溢出的方式。该选项是在2003年为gcc 3.3引入的。

-fwrapv的缺点是它会抑制优化。大多数程序不会产生有符号数溢出,而且正如我们所见,正确的程序都不会产生。当编译器可以假定不会发生有符号数溢出时,它可以生成更好的代码。这尤其出现在循环中。当循环使用有符号整数索引时,编译器可以做得更好,因为它不必考虑索引溢出的可能性。

考虑这个简单的例子:

int f(int i) { int j, k = 0; for (j = i; j < i + 10; ++j) ++k; return k; }

此函数返回什么?当编译器可以假设有符号数溢出是未定义行为时,此函数将编译为只返回10的代码。使用-fwrapv选项,代码就不那么简单了,因为i可能恰好有一个像0x7ffffff8这样的值,它会在循环期间溢出。虽然这是一个简单的例子,但很明显,像这样的循环总是在实际代码中发生。

然而,gcc确实需要回应社区用户的担忧。我在gcc 4.2中引入了两个新选项。第一个是-fno-strict-overflow,告诉编译器,它可能不会假设有符号数溢出是未定义行为。第二个是-Wstrict-overflow,告诉编译器在使用有符号数溢出这个未定义行为来实现优化的情况下发出警告。使用这些选项,可以检测程序中发生有符号数溢出的情况,并且可以禁用依赖有符号数溢出进行的优化,直到程序修复。-Wstrict-overflow甚至发现了一个很小的情况,即gcc本身依赖于有符号数溢出这个未定义行为,在处理常数0x80000000的除法时。

这自然会引出一个问题:-fwrapv-fno-strict-overflow之间有什么区别?在不使用普通二进制补码算法的处理器上有一个明显的区别:-fwrap需要二进制补码溢出,而-fno-strict-overflow则不需要。然而,目前还没有这种处理器被普遍使用。在实践中,我认为这两个选项生成的代码的行为始终相同。然而,它们对优化器的影响不同。例如,此代码:

int f(int x) { return (x + 0x7fffffff) * 2; }

使用-fwrapv-fno-strict-overflow编译出不同汇编代码。之所以出现这种差异,是因为-fwrapv精确地指定了溢出的行为方式,而-fno-strict-overflow只是说编译器不应该优化溢出。对于当前的编译器,使用-fwrapv(和-O2 -fomit-frame-pointer),我可以看到:

movl $1, %eax
subl 4(%esp), %eax
addl %eax, %eax
negl %eax
ret

而使用-fno-strict-overflow选项我看到:

movl 4(%esp), %eax
leal -2(%eax,%eax), %eax
ret

运算结果一样,但是使用了不同的算法。