深入Go语言 - 10

cgo的介绍

目录 [−]

  1. Go代码调用C函数
  2. 调用动态链接库

本章介绍Go如何调用C代码,以及如何调用动态链接库。

如果你正准备使用Go开发你的程序,或者你正将一个C构建的项目转换成Go项目,请尽量使用Go构建你的项目,而不是偷巧的导入C代码,尽量保持Go项目的纯粹,原因可以查看cgo 和 Go 语言是两码事,文末的参考文档中也有这篇文章的原始英文。

但是,有些情况下,我们不得不使用C代码构建,那么我们就可以使用cgo技术。

Go代码调用C函数

cgo可以让Go代码调用C代码。

C代码被封装进“package C”中,你可以访问C实现的类型C.size_t、 变量C.stdout 和 方法C.putchar,即使它们的首字母是小写的。

在代码import "C"之前有注释(紧接着这个import),那么这个注释称之为preamble(序言、开场白)。它可以包含编译C package的头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"time"
)
func main() {
C.srandom(C.uint(time.Now().UTC().UnixNano()))
for i := 0; i < 10; i++ {
fmt.Printf("%d ", int(C.random()))
}
}

preamble还可以包含C代码,你可以在C代码中定义变量和函数,它们可以在Go代码中通过包C来引用。C代码中的静态变量不能在G中使用,但是静态函数可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cgoexample
/*
#include <stdio.h>
#include <stdlib.h>
void myprint(char* s) {
printf("%s\n", s);
}
*/
import "C"
import "unsafe"
func Example() {
cs := C.CString("Hello from stdio\n")
C.myprint(cs)
C.free(unsafe.Pointer(cs))
}

你可以在Go官方代码库中看到这样的例子, 比如misc/cgo/stdio

工具cmd/tool将包含导入包C的Go文件转换成几个Go文件和C文件。如果你运行go tool cgo main1.go转换上面的例子,你会发现在本地文件夹下生成了一个_obj的文件夹:

1
2
3
smallnestMBP:ch9 smallnest$ ls _obj/
_cgo_.o _cgo_export.h _cgo_gotypes.go main1.cgo1.go
_cgo_export.c _cgo_flags _cgo_main.c main1.cgo2.c

它会包含一个编译器在编译这些C文件后生成的目标文件cgo.o。

在实际开发中,我们不会直接调用cgo工具,因为go build会自动完成这一切,让我们编译这个程序go build main1.go或者直接运行go run main1.go

1
2
smallnestMBP:ch9 smallnest$ go run main1.go
991076780 1985136578 1492569085 555504684 104261718 1646436258 1683793209 1521143308 547922631 1875795366

这是引用C的标准库,我们不需要额外的编译参数设置,要引入特定的库,我们还需要设置一些额外的参数。

我们可以使用#cgo指令符(directive)为C/C++编译器提供 CFLAGSCPPFLAGSCXXFLAGSLDFLAGS 设置,同时也可以提供一些编译的约束,比如为特定的平台的参数:

1
2
3
4
5
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"

开发C/C++程序的程序员和经常使用make工具链的开发者应该对这些参数很熟悉了, flags给编译器提供开关,比如指定头文件的位置等, ldflags提供链接选项,比如提供库的位置。

CFLAGS 用来给 C 编译器提供开关。
CXXFLAGS 用来给 C++ 编译器提供开关。
CPPFLAGS 用来给C预处理提供开关,对 C / C++ 都有效。
LDFLAGS 用来指定链接选项,比如链接库的位置,以及使用哪些链接库。

我们在编译C文件的时候,一般会经过四个步骤: 预处理、编译、汇编和链接,你可以看到这些开发参数的用处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 预处理
$(CC) $(CPPFLAGS) $(CFLAGS) -E main.c -o main.i
// 编译
$(CC) $(CPPFLAGS) $(CFLAGS) -S main.i -o main.s
// 汇编, "-c"选项表示不执行链接步骤
$(CC) $(CPPFLAGS) $(CFLAGS) -c main.s -o main.o
// 也可以将前面的三个步骤合起来(预处理,编译,汇编)
$(CC) $(CPPFLAGS) $(CFLAGS) -c main.c -o main.o
// 然后将目标文件链接为最终的结果
$(CC) $(LDFLAGS) main.o -o main
// 也可以一次完成上面的步骤。
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) main.c -o main

gcc可用的开关可以查看它的文档: Invoking-GCC

CPPFLAGSLDFLAGS可以通过 pkg-config 工具获得:

1
2
3
// #cgo pkg-config: png cairo
// #include <png.h>
import "C"

编译的时候,四个环境变量会增加它们的flag到编译参数中,这适合设置通用的,包无关的编译参数。

还有一个变量 ${SRCDIR} 用来指代原文件所在的文件夹的绝对路径,这允许你将预先编译好的静态库放在本地文件夹中,让编译器可以找到这些库以便正确的链接。比如包foo在文件夹/go/src/foo下:

1
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo

上面的指令等价于:

1
// #cgo LDFLAGS: -L/go/src/foo/libs -lfoo

可以看一个使用libsqlite3库的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
/*
#cgo pkg-config: sqlite3
#include <sqlite3.h>
#include <stdlib.h>
*/
import "C"
import "fmt"
type Conn struct {
db *C.sqlite3
}
func main() {
var c Conn
fmt.Println(c.db)
}

实际上,你不使用#cgo pkg-config: sqlite3也可以,因为在我们的机器上(Mac OS X),libsqlite3被安装在标准的路径中,库在/usr/lib中,头文件安装在/usr/include文件下,如果你为PKG_CONFIG_PATH指定了特殊的文件夹,你可以使用这个指令:

1
2
smallnestMBP:ch9 smallnest$ pkg-config --libs --cflags protobuf
-D_THREAD_SAFE -I/usr/local/Cellar/protobuf/2.6.1/include -L/usr/local/Cellar/protobuf/2.6.1/lib -lprotobuf -D_THREAD_SAFE

当Go工具访问一个或者多个Go文件导入包C的时候, 它也会查找其它的非Go的文件并把它们编译到Go包中 以 .c, .s, .S结尾的C文件或者汇编文件使用C编译器编译,以.cc, .cpp, .cxx结尾的文件以C++编译器编译以.h, .hh, .hpp, .hxx文件不会独立编译,但是这些头文件如果有改动,相应的C和C++文件会重新被编译。默认的C和C++编译器可以通过CC 和 CXX 环境变量改变。

所以文件夹下的汇编语言也可以被编译。

交叉编译的时候cgo被禁止,如果想启用,设置CGO_ENABLED=1。还需要额外的设置,比如C交叉编译器。

下面以一个计算圆周率的前1000位的例子看看我们自己实现的C库如何被我们的 Go代码实现 (假定所有的文件都在同一个文件夹下,这样编译和使用动态库时比较方便):
首先是计算Pi的C代码 pi.c,函数calc用来计算Pi的值,返回结果是一个C的字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int a=10000, b, c=2800, d, e, f[2801], g,i;
char r[1000];
char* pr = r;
char* calc() {
for(;b-c;)
f[b++]=a/5;
//for(;d=0,g=c*2;c-=14,printf("%.4d",e+d/a),e=d%a)
for(;d=0,g=c*2;c-=14,sprintf(pr,"%.4d",e+d/a),pr +=4,e=d%a)
for(b=c;d+=f[b]*a,f[b]=d%--g,d/=g--,--b;d*=b);
return r;
}

编译成动态库:

1
gcc -shared -fPIC -olibpi.dylib pi.c

定义一个头文件pi.h

1
char* calc();

我们可以写一个C程序 test.c 调用这个动态库,测试一下:

1
2
3
4
5
6
#include "pi.h"
#include <stdio.h>
int main() {
printf("%s\n", calc());
}

编译执行一下,确保动态库没有问题:

1
gcc -L. -I. -lpi test.c -o test

现在就可以在Go代码中使用这个库了。写一个Go文件 main3.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
/*
#cgo CFLAGS: -I${SRCDIR}
#cgo LDFLAGS: -L${SRCDIR} -lpi
#include "pi.h"
*/
import "C"
import "fmt"
func main() {
fmt.Println("计算PI值:")
v := C.GoString(C.calc())
fmt.Println(v)
}

编译:go build main3.go,因为动态库和生成的可执行文件main3在同一个目录下,没有问题,执行main3:

1
2
3
smallnestMBP:ch9 smallnest$ ./main3
计算PI:
31415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185

上面这个计算Pi的例子我们将C的字符串转换成Go的字符串。 cgo定义了Go和C之间的类型对应关系。

  • 如果C的struct的字段类型是Go的关键字,如type, 那么在Go代码中可以在字段前加关键字如x._type
  • C中的整数类型已经在包C中定义,如C.charC.shortC.ushortC.intC.uintC.longlongC.float,不一一列举,请看参考文档1
  • 访问C的structunionenum类型需要加类型前缀struct_union_enum_,如C.struct_stat
  • 访问C中的类型T的size用 C.sizeof_T,如C.sizeof_struct_stat
  • Go不支持C的union的概念,只是把它作为相同长度的字节数组
  • Go的Struct不能嵌入C的类型
  • Go的API不应该再暴露C的类型给外部
  • 调用C的函数可以进行多值赋值,一个值作为返回值,一个作为errno
  • 当前不支持C的函数指针
  • C中参数是固定长度的数组,可以把数组名传递给函数,但是Go代码调用中必须显示地将指针指向数组的第一个元素,如C.f(&C.x[0])

对应的类型转换:

char -->  C.char -->  byte
signed char -->  C.schar -->  int8
unsigned char -->  C.uchar -->  uint8
short int -->  C.short -->  int16
short unsigned int -->  C.ushort -->  uint16
int -->  C.int -->  int
unsigned int -->  C.uint -->  uint32
long int -->  C.long -->  int32 or int64
long unsigned int -->  C.ulong -->  uint32 or uint64
long long int -->  C.longlong -->  int64
long long unsigned int -->  C.ulonglong -->  uint64
float -->  C.float -->  float32
double -->  C.double -->  float64
wchar_t -->  C.wchar_t  -->  
void * -> unsafe.Pointer

项目giorgisio/cgo提供了一些Go调用C代码各种类型的例子。

调用动态链接库

对于Windows环境,Go提供了直接加载动态链接库的方法。 首先syscall包下实现了LoadDLLFindProcRelease方法,可以加载动态链接库以及得到相应的函数。

另外包golang.org/x/sys/windows提供了更多的方法,如LoadLibraryLoadLibraryExDLLLazyDLL等方法和类型。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
h, err := windows.LoadLibrary("kernel32.dll")
if err != nil {
abort("LoadLibrary", err)
}
defer windows.FreeLibrary(h)
proc, err := windows.GetProcAddress(h, "GetVersion")
if err != nil {
abort("GetProcAddress", err)
}
r, _, _ := syscall.Syscall(uintptr(proc), 0, 0, 0, 0)
major := byte(r)
minor := uint8(r >> 8)
build := uint16(r >> 16)
print("windows version ", major, ".", minor, " (Build ", build, ")\n")

其它平台我还没有发现官方的调用.so或者.dylib的方法, 但是我看到有第三方的作者写了相应的库,提供类似C中的dlopen和dlsym方法:
Runtime dynamic library loader

还有go-ffi,也提供了dlopen和dlsym的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// dl-open a library: here, libm on macosx
lib, err := ffi.NewLibrary("libm.dylib")
handle_err(err)
// get a handle to 'cos', with the correct signature
cos, err := lib.Fct("cos", ffi.Double, []Type{ffi.Double})
handle_err(err)
// call it
out := cos(0.).Float()
println("cos(0.)=", out)
err = lib.Close()
handle_err(err)

参考

评论