转载

深入Go语言 - 10

本章介绍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的头文件:

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++编译器提供 CFLAGSCPPFLAGSCXXFLAGSLDFLAGS 设置,同时也可以提供一些编译的 约束 ,比如为特定的平台的参数:

// #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 。

CPPFLAGSLDFLAGS 可以通过 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之间的类型对应关系。

  • 如果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 等方法和类型。

举个栗子:

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

参考

  • https://golang.org/cmd/cgo/
  • https://github.com/golang/go/wiki/cgo
  • http://akrennmair.github.io/golang-cgo-slides/#1
  • http://dave.cheney.net/2016/01/18/cgo-is-not-go
  • http://dominik.honnef.co/posts/2015/06/statically_compiled_go_programs__always__even_with_cgo__using_musl/
  • http://blog.giorgis.io/cgo-examples
  • http://blog.madewithdrew.com/post/statically-linking-c-to-go/
  • https://github.com/hyper-carrot/go_command_tutorial/blob/master/0.13.md
  • https://www.goinggo.net/2013/08/using-c-dynamic-libraries-in-go-programs.html
原文  http://colobu.com/2016/06/30/dive-into-go-10/
正文到此结束
Loading...