本章介绍Go如何调用C代码,以及如何调用动态链接库。
如果你正准备使用Go开发你的程序,或者你正将一个C构建的项目转换成Go项目,请尽量使用Go构建你的项目,而不是偷巧的导入C代码,尽量保持Go项目的纯粹,原因可以查看 cgo 和 Go 语言是两码事 ,文末的参考文档中也有这篇文章的原始英文。
但是,有些情况下,我们不得不使用C代码构建,那么我们就可以使用cgo技术。
cgo可以让Go代码调用C代码。
C代码被封装进“package C”中,你可以访问C实现的类型 C.size_t
、 变量 C.stdout
和 方法 C.putchar
,即使它们的首字母是小写的。
在代码 import "C"
之前有注释(紧接着这个import),那么这个注释称之为 preamble
(序言、开场白)。它可以包含编译C package的头文件:
packagemain /* #include <stdlib.h> */ import"C" import( "fmt" "time" ) funcmain() { C.srandom(C.uint(time.Now().UTC().UnixNano())) fori :=0; i <10; i++ { fmt.Printf("%d ",int(C.random())) } }
preamble还可以包含C代码,你可以在C代码中定义变量和函数,它们可以在Go代码中通过包C来引用。C代码中的静态变量不能在G中使用,但是静态函数可以。
packagecgoexample /* #include <stdio.h> #include <stdlib.h> void myprint(char* s) { printf("%s/n", s); } */ import"C" import"unsafe" funcExample() { 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的文件夹:
smallnestMBP:ch9 smallnest$ ls _obj/ _cgo_.o _cgo_export.h _cgo_gotypes.go main1.cgo1.go _cgo_export.c_cgo_flags _cgo_main.cmain1.cgo2.c
它会包含一个编译器在编译这些C文件后生成的目标文件 cgo .o。
在实际开发中,我们不会直接调用cgo工具,因为 go build
会自动完成这一切,让我们编译这个程序 go build main1.go
或者直接运行 go run main1.go
:
smallnestMBP:ch9 smallnest$ gorun main1.go 991076780198513657814925690855555046841042617181646436258168379320915211433085479226311875795366
这是引用C的标准库,我们不需要额外的编译参数设置,要引入特定的库,我们还需要设置一些额外的参数。
我们可以使用 #cgo
指令符(directive)为C/C++编译器提供 CFLAGS 、 CPPFLAGS 、 CXXFLAGS 和 LDFLAGS 设置,同时也可以提供一些编译的 约束 ,比如为特定的平台的参数:
// #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文件的时候,一般会经过四个步骤: 预处理、编译、汇编和链接,你可以看到这些开发参数的用处:
// 预处理 $(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 。
CPPFLAGS
、 LDFLAGS
可以通过 pkg-config 工具获得:
// #cgo pkg-config: png cairo // #include <png.h> import"C"
编译的时候,四个环境变量会增加它们的flag到编译参数中,这适合设置通用的,包无关的编译参数。
还有一个变量 ${SRCDIR} 用来指代原文件所在的文件夹的绝对路径,这允许你将预先编译好的静态库放在本地文件夹中,让编译器可以找到这些库以便正确的链接。比如包foo在文件夹/go/src/foo下:
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
上面的指令等价于:
// #cgo LDFLAGS: -L/go/src/foo/libs -lfoo
可以看一个使用libsqlite3库的例子:
packagemain /* #cgo pkg-config: sqlite3 #include <sqlite3.h> #include <stdlib.h> */ import"C" import"fmt" typeConnstruct{ db *C.sqlite3 } funcmain() { varc Conn fmt.Println(c.db) }
实际上,你不使用 #cgo pkg-config: sqlite3
也可以,因为在我们的机器上(Mac OS X),libsqlite3被安装在标准的路径中,库在/usr/lib中,头文件安装在/usr/include文件下,如果你为PKG_CONFIG_PATH指定了特殊的文件夹,你可以使用这个指令:
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的字符串:
#include<stdio.h> inta=10000, b, c=2800, d, e, f[2801], g,i; charr[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); returnr; }
编译成动态库:
gcc -shared -fPIC -olibpi.dylib pi.c
定义一个头文件 pi.h
:
char* calc();
我们可以写一个C程序 test.c
调用这个动态库,测试一下:
#include"pi.h" #include<stdio.h> intmain() { printf("%s/n", calc()); }
编译执行一下,确保动态库没有问题:
gcc -L. -I. -lpi test.c -otest
现在就可以在Go代码中使用这个库了。写一个Go文件 main3.go
:
packagemain /* #cgo CFLAGS: -I${SRCDIR} #cgo LDFLAGS: -L${SRCDIR} -lpi #include "pi.h" */ import"C" import"fmt" funcmain() { fmt.Println("计算PI值:") v := C.GoString(C.calc()) fmt.Println(v) }
编译: go build main3.go
,因为动态库和生成的可执行文件 main3
在同一个目录下,没有问题,执行main3:
smallnestMBP:ch9 smallnest$./main3 计算PI值: 31415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185
上面这个计算Pi的例子我们将C的字符串转换成Go的字符串。 cgo定义了Go和C之间的类型对应关系。
type
, 那么在Go代码中可以在字段前加关键字如 x._type
C.char
、 C.short
、 C.ushort
、 C.int
、 C.uint
、 C.longlong
、 C.float
,不一一列举,请看参考文档1 struct
、 union
、 enum
类型需要加类型前缀 struct_
、 union_
、 enum_
,如C.struct_stat 对应的类型转换:
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包下实现了 LoadDLL
、 FindProc
、 Release
方法,可以加载动态链接库以及得到相应的函数。
另外包 golang.org/x/sys/windows
提供了更多的方法,如 LoadLibrary
、 LoadLibraryEx
、 DLL
、 LazyDLL
等方法和类型。
举个栗子:
h, err := windows.LoadLibrary("kernel32.dll") iferr !=nil{ abort("LoadLibrary", err) } deferwindows.FreeLibrary(h) proc, err := windows.GetProcAddress(h, "GetVersion") iferr !=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
参考