虽然我们可以在Go源码文件中直接定义C类型、变量和C函数,但从代码结构上来讲,在Go源文件中大量编写C代码并不是Go推荐的惯用法。那么如何将C函数和变量定义从Go源码中分离出去单独定义呢?我们很容易想到将C的代码以共享库的形式提供给Go源码。
Go提供了#cgo指示符,可以用它指定Go源码在编译后与哪些共享库进行链接。我们来看一下例子:
// go-cgo/foo.go
package main
// #cgo CFLAGS: -I${SRCDIR}
// #cgo LDFLAGS: -L${SRCDIR} -lfoo
// #include
// #include
// #include "foo.h"
import "C"
import "fmt"
func main() {
fmt.Println(C.count)
C.foo()
}
我们看到在上面的例子中,通过#cgo指示符告诉Go编译器在当前源码目录(${SRCDIR}会在编译过程中自动转换为当前源码所在目录的绝对路径)下查找头文件foo.h,并链接当前源码目录下的libfoo共享库。C.count变量和C.foo函数的定义都在libfoo共享库中。
我们来创建这个共享库:
// chapter9/sources/go-cgo/foo.h
extern int count;
void foo();
// chapter9/sources/go-cgo/foo.c
include "foo.h"
int count = 6;
void foo() {
printf("I am foo!\n");
}
$gcc -c foo.c
$ar rv libfoo.a foo.o
我们用ar工具成功创建了一个静态共享库文件libfoo.a。
接下来构建并运行foo.go:
$go build foo.go
$./foo
6
I am foo!
我们看到foo.go成功链接到libfoo.a并生成最终的二进制文件foo。
Go同样支持链接动态共享库,我们用下面的命令将上面的foo.c编译为一个动态共享库:
$gcc -c foo.c
//$gcc -shared -Wl,-soname,libfoo.so -o libfoo.so foo.o (在linux上)
$gcc -shared -o libfoo.so foo.o
重新编译foo.go,并查看(在Linux上可以使用ldd,在macOS上使用otool)重新生成的二进制文件foo的动态共享库依赖情况:
$> go build foo.go
$otool -L foo
foo:
libfoo.so (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1)
有一点值得注意的是,Go支持多返回值,而C并不支持,因此当将C函数用在多返回值的Go调用中时,C的errno将作为函数返回值列表中最后那个error返回值返回。下面是个例子:
// chapter9/sources/go-cgo/c_errno.go
package main
// #include
// #include
// #include
// int foo(int i) {
// errno = 0;
// if (i > 5) {
// errno = 8;
// return i - 5;
// } else {
// return i;
// }
//}
import "C"
import "fmt"
func main() {
i, err := C.foo(C.int(8))
if err != nil {
fmt.Println(err)
} else {
fmt.Println(i)
}
}
[kod.chinaweiyuan.com)
[kod.dtjxzx.com)
[kod.1d-cn.com)
[kod.cd-shanye.com)
[kod.hrbshenou.com)
[kod.wogou168.com)
[kod.aoshageya.com)
运行这个例子:
$go run c_errno.go
exec format error
exec format error就是errno为8时的错误描述信息。我们可以在C运行时库的errno.h中找到errno=8与这段描述信息的联系:
define ENOEXEC 8 / Exec format error /
cgo构建引用c的静态链接库