Go与C的桥梁:CGO入门剖析与实践

作者:panhuili,腾讯 IEG 后台开发工程师

创新互联建站10多年成都定制网站服务;为您提供网站建设,网站制作,网页设计及高端网站定制服务,成都定制网站及推广,对小搅拌车等多个方面拥有多年的营销推广经验的网站建设公司。

Go 作为当下最火的开发语言之一,它的优势不必多说。Go 对于高并发的支持,使得它可以很方便的作为独立模块嵌入业务系统。有鉴于我司大量的 C/C++存量代码,如何将 Go 和 C/C++进行打通就尤为重要。Golang 自带的 CGO 可以支持与 C 语言接口的互通。本文首先介绍了 cgo 的常见用法,然后根据底层代码分析其实现机制,最后在特定场景下进行 cgo 实践。

一、CGO 快速入门

1.1、启用 CGO 特性

在 golang 代码中加入 import “C” 语句就可以启动 CGO 特性。这样在进行 go build 命令时,就会在编译和连接阶段启动 gcc 编译器。

 
 
 
 
  1. // go.1.15// test1.go
  2. package main
  3. import "C"      // import "C"更像是一个关键字,CGO工具在预处理时会删掉这一行
  4. func main() {
  5. }

使用 -x 选项可以查看 go 程序编译过程中执行的所有指令。可以看到 golang 编译器已经为 test1.go 创建了 CGO 编译选项

 
 
 
 
  1. [root@VM-centos ~/cgo_test/golink2]# go build -x test1.go
  2. WORK=/tmp/go-build330287398
  3. mkdir -p $WORK/b001/
  4. cd /root/cgo_test/golink2
  5. CGO_LDFLAGS='"-g" "-O2"' /usr/lib/golang/pkg/tool/linux_amd64/cgo -objdir $WORK/b001/ -importpath command-line-arguments -- -I $WORK/b001/ -g -O2 ./test1.go    # CGO编译选项
  6. cd $WORK
  7. gcc -fno-caret-diagnostics -c -x c - -o /dev/null || true
  8. gcc -Qunused-arguments -c -x c - -o /dev/null || true
  9. gcc -fdebug-prefix-map=a=b -c -x c - -o /dev/null || true
  10. gcc -gno-record-gcc-switches -c -x c - -o /dev/null || true
  11. .......

1.2、Hello Cgo

通过 import “C” 语句启用 CGO 特性后,CGO 会将上一行代码所处注释块的内容视为 C 代码块,被称为序文(preamble)。

 
 
 
 
  1. // test2.go
  2. package main
  3. //#include         //  序文中可以链接标准C程序库
  4. import "C"
  5. func main() {
  6.     C.puts(C.CString("Hello, Cgo\n"))
  7. }

在序文中可以使用 C.func 的方式调用 C 代码块中的函数,包括库文件中的函数。对于 C 代码块的变量,类型也可以使用相同方法进行调用。

test2.go 通过 CGO 提供的 C.CString 函数将 Go 语言字符串转化为 C 语言字符串,最后再通过 C.puts 调用 中的 puts 函数向标准输出打印字符串。

1.3 cgo 工具

当你在包中引用 import "C",go build 就会做很多额外的工作来构建你的代码,构建就不仅仅是向 go tool compile 传递一堆 .go 文件了,而是要先进行以下步骤:

1)cgo 工具就会被调用,在 C 转换 Go、Go 转换 C 的之间生成各种文件。

2)系统的 C 编译器会被调用来处理包中所有的 C 文件。

3)所有独立的编译单元会被组合到一个 .o 文件。

4)生成的 .o 文件会在系统的连接器中对它的引用进行一次检查修复。

cgo 是一个 Go 语言自带的特殊工具,可以使用命令 go tool cgo 来运行。它可以生成能够调用 C 语言代码的 Go 语言源文件,也就是说所有启用了 CGO 特性的 Go 代码,都会首先经过 cgo 的"预处理"。

对 test2.go,cgo 工具会在同目录生成以下文件

 
 
 
 
  1. _obj--|
  2.       |--_cgo.o             // C代码编译出的链接库
  3.       |--_cgo_main.c        // C代码部分的main函数
  4.       |--_cgo_flags         // C代码的编译和链接选项
  5.       |--_cgo_export.c      //
  6.       |--_cgo_export.h      // 导出到C语言的Go类型
  7.       |--_cgo_gotypes.go    // 导出到Go语言的C类型
  8.       |--test1.cgo1.go      // 经过“预处理”的Go代码
  9.       |--test1.cgo2.c       // 经过“预处理”的C代码

二、CGO 的 N 种用法

CGO 作为 Go 语言和 C 语言之间的桥梁,其使用场景可以分为两种:Go 调用 C 程序 和 C 调用 Go 程序。

2.1、Go 调用自定义 C 程序

 
 
 
 
  1. // test3.go
  2. package main
  3. /*
  4. #cgo LDFLAGS: -L/usr/local/lib
  5. #include 
  6. #include 
  7. #define REPEAT_LIMIT 3              // CGO会保留C代码块中的宏定义
  8. typedef struct{                     // 自定义结构体
  9.     int repeat_time;
  10.     char* str;
  11. }blob;
  12. int SayHello(blob* pblob) {  // 自定义函数
  13.     for ( ;pblob->repeat_time < REPEAT_LIMIT; pblob->repeat_time++){
  14.         puts(pblob->str);
  15.     }
  16.     return 0;
  17. }
  18. */
  19. import "C"
  20. import (
  21.     "fmt"
  22.     "unsafe"
  23. )
  24. func main() {
  25.     cblob := C.blob{}                               // 在GO程序中创建的C对象,存储在Go的内存空间
  26.     cblob.repeat_time = 0
  27.     cblob.str = C.CString("Hello, World\n")         // C.CString 会在C的内存空间申请一个C语言字符串对象,再将Go字符串拷贝到C字符串
  28.     ret := C.SayHello(&cblob)                       // &cblob 取C语言对象cblob的地址
  29.     fmt.Println("ret", ret)
  30.     fmt.Println("repeat_time", cblob.repeat_time)
  31.     C.free(unsafe.Pointer(cblob.str))               // C.CString 申请的C空间内存不会自动释放,需要显示调用C中的free释放
  32. }

CGO 会保留序文中的宏定义,但是并不会保留注释,也不支持#program,C 代码块中的#program 语句极可能产生未知错误。

CGO 中使用 #cgo 关键字可以设置编译阶段和链接阶段的相关参数,可以使用 ${SRCDIR} 来表示 Go 包当前目录的绝对路径。

使用 C.结构名 或 C.struct_结构名 可以在 Go 代码段中定义 C 对象,并通过成员名访问结构体成员。

test3.go 中使用 C.CString 将 Go 字符串对象转化为 C 字符串对象,并将其传入 C 程序空间进行使用,由于 C 的内存空间不受 Go 的 GC 管理,因此需要显示的调用 C 语言的 free 来进行回收。详情见第三章。

2.2、Go 调用 C/C++模块

2.2.1、简单 Go 调 C

直接将完整的 C 代码放在 Go 源文件中,这种编排方式便于开发人员快速在 C 代码和 Go 代码间进行切换。

 
 
 
 
  1. // demo/test4.go
  2. package main
  3. /*
  4. #include 
  5. int SayHello() {
  6.  puts("Hello World");
  7.     return 0;
  8. }
  9. */
  10. import "C"
  11. import (
  12.     "fmt"
  13. )
  14. func main() {
  15.     ret := C.SayHello()
  16.     fmt.Println(ret)
  17. }

但是当 CGO 中使用了大量的 C 语言代码时,将所有的代码放在同一个 go 文件中即不利于代码复用,也会影响代码的可读性。此时可以将 C 代码抽象成模块,再将 C 模块集成入 Go 程序中。

2.2.2、Go 调用 C 模块

将 C 代码进行抽象,放到相同目录下的 C 语言源文件 hello.c 中

 
 
 
 
  1. // demo/hello.c
  2. #include 
  3. int SayHello() {
  4.  puts("Hello World");
  5.     return 0;
  6. }

在 Go 代码中,声明 SayHello() 函数,再引用 hello.c 源文件,就可以调起外部 C 源文件中的函数了。同理也可以将C 源码编译打包为静态库或动态库进行使用。

 
 
 
 
  1. // demo/test5.go
  2. package main
  3. /*
  4. #include "hello.c"
  5. int SayHello();
  6. */
  7. import "C"
  8. import (
  9.     "fmt"
  10. )
  11. func main() {
  12.     ret := C.SayHello()
  13.     fmt.Println(ret)
  14. }

test5.go 中只对 SayHello 函数进行了声明,然后再通过链接 C 程序库的方式加载函数的实现。那么同样的,也可以通过链接 C++程序库的方式,来实现 Go 调用 C++程序。

2.2.3、Go 调用 C++模块

基于 test4。可以抽象出一个 hello 模块,将模块的接口函数在 hello.h 头文件进行定义

 
 
 
 
  1. // demo/hello.h
  2. int SayHello();

再使用 C++来重新实现这个 C 函数

 
 
 
 
  1. // demo/hello.cpp
  2. #include 
  3. extern "C" {
  4.     #include "hello.h"
  5. }
  6. int SayHello() {
  7.  std::cout<<"Hello World";
  8.     return 0;
  9. }

最后再在 Go 代码中,引用 hello.h 头文件,就可以调用 C++实现的 SayHello 函数了

 
 
 
 
  1. // demo/test6.go
  2. package main
  3. /*
  4. #include "hello.h"
  5. */
  6. import "C"
  7. import (
  8.     "fmt"
  9. )
  10. func main() {
  11.     ret := C.SayHello()
  12.     fmt.Println(ret)
  13. }

CGO 提供的这种面向 C 语言接口的编程方式,使得开发者可以使用是任何编程语言来对接口进行实现,只要最终满足 C 语言接口即可。

2.3、C 调用 Go 模块

C 调用 Go 相对于 Go 调 C 来说要复杂多,可以分为两种情况。一是原生 Go 进程调用 C,C 中再反调 Go 程序。另一种是原生 C 进程直接调用 Go。

2.3.1、Go 实现的 C 函数

如前述,开发者可以用任何编程语言来编写程序,只要支持 CGO 的 C 接口标准,就可以被 CGO 接入。那么同样可以用 Go 实现 C 函数接口。

在 test6.go 中,已经定义了 C 接口模块 hello.h

 
 
 
 
  1. // demo/hello.h
  2. void SayHello(char* s);

可以创建一个 hello.go 文件,来用 Go 语言实现 SayHello 函数

 
 
 
 
  1. // demo/hello.go
  2. package main
  3. //#include 
  4. import "C"
  5. import "fmt"
  6. //export SayHello
  7. func SayHello(str *C.char) {
  8.     fmt.Println(C.GoString(str))
  9. }

CGO 的//export SayHello 指令将 Go 语言实现的 SayHello 函数导出为 C 语言函数。这样再 Go 中调用 C.SayHello 时,最终调用的是 hello.go 中定义的 Go 函数 SayHello

 
 
 
 
  1. // demo/test7.go
  2. // go run ../demo
  3. package main
  4. //#include "hello.h"
  5. import "C"
  6. func main() {
  7.     C.SayHello(C.CString("Hello World"))
  8. }

Go 程序先调用 C 的 SayHello 接口,由于 SayHello 接口链接在 Go 的实现上,又调到 Go。

看起来调起方和实现方都是 Go,但实际执行顺序是 Go 的 main 函数,调到 CGO 生成的 C 桥接函数,最后 C 桥接函数再调到 Go 的 SayHello。这部分会在第四章进行分析。

2.3.2、原生 C 调用 Go

C 调用到 Go 这种情况比较复杂,Go 一般是便以为 c-shared/c-archive 的库给 C 调用。

 
 
 
 
  1. // demo/hello.go
  2. package main
  3. import "C"
  4. //export hello
  5. func hello(value string)*C.char {   // 如果函数有返回值,则要将返回值转换为C语言对应的类型
  6.     return C.CString("hello" + value)
  7. }
  8. func main(){
  9.     // 此处一定要有main函数,有main函数才能让cgo编译器去把包编译成C的库
  10. }

如果 Go 函数有多个返回值,会生成一个 C 结构体进行返回,结构体定义参考生成的.h 文件

生成 c-shared 文件 命令

 
 
 
 
  1. go build -buildmode=c-shared -o hello.so hello.go

在 C 代码中,只需要引用 go build 生成的.h 文件,并在编译时链接对应的.so 程序库,即可从 C 调用 Go 程序

 
 
 
 
  1. // demo/test8.c
  2. #include 
  3. #include 
  4. #include "hello.h"                       //此处为上一步生成的.h文件
  5. int main(){
  6.     char c1[] = "did";
  7.     GoString s1 = {c1,strlen(c1)};       //构建Go语言的字符串类型
  8.     char *c = hello(s1);
  9.     printf("r:%s",c);
  10.     return 0;
  11. }

编译命令

 
 
 
 
  1. gcc -o c_go main.c hello.so

C 函数调入进 Go,必须按照 Go 的规则执行,当主程序是 C 调用 Go 时,也同样有一个 Go 的 runtime 与 C 程序并行执行。这个 runtime 的初始化在对应的 c-shared 的库加载时就会执行。因此,在进程启动时就有两个线程执行,一个 C 的,一 (多)个是 Go 的。

三、类型转换

想要更好的使用 CGO 必须了解 Go 和 C 之间类型转换的规则

3.1、数值类型

在 Go 语言中访问 C 语言的符号时,一般都通过虚拟的“C”包进行。比如 C.int,C.char 就对应与 C 语言中的 int 和 char,对应于 Go 语言中的 int 和 byte。

C 语言和 Go 语言的数值类型对应如下:

Go 语言的 int 和 uint 在 32 位和 64 位系统下分别是 4 个字节和 8 个字节大小。它在 C 语言中的导出类型 GoInt 和 GoUint 在不同位数系统下内存大小也不同。

如下是 64 位系统中,Go 数值类型在 C 语言的导出列表

 
 
 
 
  1. // _cgo_export.h
  2. typedef signed char GoInt8;
  3. typedef unsigned char GoUint8;
  4. typedef short GoInt16;
  5. typedef unsigned short GoUint16;
  6. typedef int GoInt32;
  7. typedef unsigned int GoUint32;
  8. typedef long long GoInt64;
  9. typedef unsigned long long GoUint64;
  10. typedef GoInt64 GoInt;
  11. typedef GoUint64 GoUint;
  12. typedef __SIZE_TYPE__ GoUintptr;
  13. typedef float GoFloat32;
  14. typedef double GoFloat64;
  15. typedef float _Complex GoComplex64;
  16. typedef double _Complex GoComplex128;

需要注意的是在 C 语言符号名前加上 Ctype, 便是其在 Go 中的导出名,因此在启用 CGO 特性后,Go 语言中禁止出现以Ctype 开头的自定义符号名,类似的还有Cfunc等。

可以在序文中引入_obj/_cgo_export.h 来显式使用 cgo 在 C 中的导出类型

 
 
 
 
  1. // test9.go
  2. package main
  3. /*
  4. #include "_obj/_cgo_export.h"                       // _cgo_export.h由cgo工具动态生成
  5. GoInt32 Add(GoInt32 param1, GoInt32 param2) {       // GoInt32即为cgo在C语言的导出类型
  6.  return param1 + param2;
  7. }
  8. */
  9. import "C"
  10. import "fmt"
  11. func main() {
  12.  // _Ctype_                      // _Ctype_ 会在cgo预处理阶段触发异常,
  13.  fmt.Println(C.Add(1, 2))
  14. }

如下是 64 位系统中,C 数值类型在 Go 语言的导出列表

 
 
 
 
  1. // _cgo_gotypes.go
  2. type _Ctype_char int8
  3. type _Ctype_double float64
  4. type _Ctype_float float32
  5. type _Ctype_int int32
  6. type _Ctype_long int64
  7. type _Ctype_longlong int64
  8. type _Ctype_schar int8
  9. type _Ctype_short int16
  10. type _Ctype_size_t = _Ctype_ulong
  11. type _Ctype_uchar uint8
  12. type _Ctype_uint uint32
  13. type _Ctype_ulong uint64
  14. type _Ctype_ulonglong uint64
  15. type _Ctype_void [0]byte

为了提高 C 语言的可移植性,更好的做法是通过 C 语言的 C99 标准引入的头文件,不但每个数值类型都提供了明确内存大小,而且和 Go 语言的类型命名更加一致。

3.2、切片

Go 中切片的使用方法类似 C 中的数组,但是内存结构并不一样。C 中的数组实际上指的是一段连续的内存,而 Go 的切片在存储数据的连续内存基础上,还有一个头结构体,其内存结构如下

因此 Go 的切片不能直接传递给 C 使用,而是需要取切片的内部缓冲区的首地址(即首个元素的地址)来传递给 C 使用。使用这种方式把 Go 的内存空间暴露给 C 使用,可以大大减少 Go 和 C 之间参数传递时内存拷贝的消耗。

 
 
 
 
  1. // test10.go
  2. package main
  3. /*
  4. int SayHello(char* buff, int len) {
  5.     char hello[] = "Hello Cgo!";
  6.     int movnum = len < sizeof(hello) ? len:sizeof(hello);
  7.     memcpy(buff, hello, movnum);                        // go字符串没有'\0',所以直接内存拷贝
  8.     return movnum;
  9. }
  10. */
  11. import "C"
  12. import (
  13.     "fmt"
  14.     "unsafe"
  15. )
  16. func main() {
  17.     buff := make([]byte, 8)
  18.     C.SayHello((*C.char)(unsafe.Pointer(&buff[0])), C.int(len(buff)))
  19.     a := string(buff)
  20.     fmt.Println(a)
  21. }

3.3 字符串

Go 的字符串与 C 的字符串在底层的内存模型也不一样:

Go 的字符串并没有以'\0' 结尾,因此使用类似切片的方式,直接将 Go 字符串的首元素地址传递给 C 是不可行的。

3.3.1、Go 与 C 的字符串传递

cgo 给出的解决方案是标准库函数 C.CString(),它会在 C 内存空间内申请足够的空间,并将 Go 字符串拷贝到 C 空间中。因此 C.CString 申请的内存在 C 空间中,因此需要显式的调用 C.free 来释放空间,如 test3。

如下是 C.CString()的底层实现

 
 
 
 
  1. func _Cfunc_CString(s string) *_Ctype_char {        // 从Go string 到 C char* 类型转换
  2.  p := _cgo_cmalloc(uint64(len(s)+1))
  3.  pp := (*[1<<30]byte)(p)
  4.  copy(pp[:], s)
  5.  pp[len(s)] = 0
  6.  return (*_Ctype_char)(p)
  7. }
  8. //go:cgo_unsafe_args
  9. func _cgo_cmalloc(p0 uint64) (r1 unsafe.Pointer) {
  10.  _cgo_runtime_cgocall(_cgo_bb7421b6328a_Cfunc__Cmalloc, uintptr(unsafe.Pointer(&p0)))
  11.  if r1 == nil {
  12.   runtime_throw("runtime: C malloc failed")
  13.  }
  14.  return
  15. }

_Cfunc_CString

_Cfunc_CString 是 cgo 定义的从 Go string 到 C char* 的类型转换函数

1)使用_cgo_cmalloc 在 C 空间内申请内存(即不受 Go GC 控制的内存)

2)使用该段 C 内存初始化一个[]byte 对象

3)将 string 拷贝到[]byte 对象

4)将该段 C 空间内存的地址返回

它的实现方式类似前述,切片的类型转换。不同在于切片的类型转换,是将 Go 空间内存暴露给 C 函数使用。而_Cfunc_CString 是将 C 空间内存暴露给 Go 使用。

_cgo_cmalloc

定义了一个暴露给 Go 的 C 函数,用于在 C 空间申请内存

与 C.CString()对应的是从 C 字符串转 Go 字符串的转换函数 C.GoString()。C.GoString()函数的实现较为简单,检索 C 字符串长度,然后申请相同长度的 Go-string 对象,最后内存拷贝。

如下是 C.GoString()的底层实现

 
 
 
 
  1. //go:linkname _cgo_runtime_gostring runtime.gostring
  2. func _cgo_runtime_gostring(*_Ctype_char) string
  3. func _Cfunc_GoString(p *_Ctype_char) string {           // 从C char* 到 Go string 类型转换
  4.  return _cgo_runtime_gostring(p)
  5. }
  6. //go:linkname gostring
  7. func gostring(p *byte) string {             // 底层实现
  8.  l := findnull(p)
  9.  if l == 0 {
  10.   return ""
  11.  }
  12.  s, b := rawstring(l)
  13.  memmove(unsafe.Pointer(&b[0]), unsafe.Pointer(p), uintptr(l))
  14.  return s
  15. }

3.3.2、更高效的字符串传递方法

C.CString 简单安全,但是它涉及了一次从 Go 到 C 空间的内存拷贝,对于长字符串而言这会是难以忽视的开销。

Go 官方文档中声称 string 类型是”不可改变的“,但是在实操中可以发现,除了常量字符串会在编译期被分配到只读段,其他的动态生成的字符串实际上都是在堆上。

因此如果能够获得 string 的内存缓存区地址,那么就可以使用类似切片传递的方式将字符串指针和长度直接传递给 C 使用。

查阅源码,可知 String 实际上是由缓冲区首地址 和 长度构成的。这样就可以通过一些方式拿到缓存区地址。

 
 
 
 
  1. type stringStruct struct {
  2.  str unsafe.Pointer  //str首地址
  3.  len int             //str长度
  4. }

test11.go 将 fmt 动态生成的 string 转为自定义类型 MyString 便可以获得缓冲区首地址,将地址传入 C 函数,这样就可以在 C 空间直接操作 Go-String 的内存空间了,这样可以免去内存拷贝的消耗。

 
 
 
 
  1. // test11.go
  2. package main
  3. /*
  4. #include 
  5. int SayHello(char* buff, int len) {
  6.     char hello[] = "Hello Cgo!";
  7.     int movnum = len < sizeof(hello) ? len:sizeof(hello);
  8.     memcpy(buff, hello, movnum);
  9.     return movnum;
  10. }
  11. */
  12. import "C"
  13. import (
  14.     "fmt"
  15.     "unsafe"
  16. )
  17. type MyString struct {
  18.  Str *C.char
  19.  Len int
  20. }
  21. func main() {
  22.     s := fmt.Sprintf("             ")
  23.     C.SayHello((*MyString)(unsafe.Pointer(&s)).Str, C.int((*MyString)(unsafe.Pointer(&s)).Len))
  24.     fmt.Print(s)
  25. }

这种方法背离了 Go 语言的设计理念,如非必要,不要把这种代码带入你的工程,这里只是作为一种“黑科技”进行分享。

3.4、结构体,联合,枚举

cgo 中结构体,联合,枚举的使用方式类似,可以通过 C.struct_XXX 来访问 C 语言中 struct XXX 类型。union,enum 也类似。

3.4.1、结构体

如果结构体的成员名字中碰巧是 Go 语言的关键字,可以通过在成员名开头添加下划线来访问

如果有 2 个成员:一个是以 Go 语言关键字命名,另一个刚好是以下划线和 Go 语言关键字命名,那么以 Go 语言关键字命名的成员将无法访问(被屏蔽)

C 语言结构体中位字段对应的成员无法在 Go 语言中访问,如果需要操作位字段成员,需要通过在 C 语言中定义辅助函数来完成。对应零长数组的成员(C 中经典的变长数组),无法在 Go 语言中直接访问数组的元素,但同样可以通过在 C 中定义辅助函数来访问。

结构体的内存布局按照 C 语言的通用对齐规则,在 32 位 Go 语言环境 C 语言结构体也按照 32 位对齐规则,在 64 位 Go 语言环境按照 64 位的对齐规则。对于指定了特殊对齐规则的结构体,无法在 CGO 中访问。

 
 
 
 
  1. // test11.go
  2. package main
  3. /*
  4. struct Test {
  5.     int a;
  6.     float b;
  7.     double type;
  8.     int size:10;
  9.     int arr1[10];
  10.     int arr2[];
  11. };
  12. int Test_arr2_helper(struct Test * tm ,int pos){
  13.     return tm->arr2[pos];
  14. }
  15. #pragma  pack(1)
  16. struct Test2 {
  17.     float a;
  18.     char b;
  19.     int c;
  20. };
  21. */
  22. import "C"
  23. import "fmt"
  24. func main() {
  25.     test := C.struct_Test{}
  26.     fmt.Println(test.a)
  27.     fmt.Println(test.b)
  28.     fmt.Println(test._type)
  29.     //fmt.Println(test.size)        // 位数据
  30.     fmt.Println(test.arr1[0])
  31.     //fmt.Println(test.arr)         // 零长数组无法直接访问
  32.     //Test_arr2_helper(&test, 1)
  33.     test2 := C.struct_Test2{}
  34.     fmt.Println(test2.c)
  35.     //fmt.Println(test2.c)          // 由于内存对齐,该结构体部分字段Go无法访问
  36. }

3.4.2、联合

Go 语言中并不支持 C 语言联合类型,它们会被转为对应大小的字节数组。

如果需要操作 C 语言的联合类型变量,一般有三种方法:第一种是在 C 语言中定义辅助函数;第二种是通过 Go 语言的"encoding/binary"手工解码成员(需要注意大端小端问题);第三种是使用unsafe包强制转型为对应类型(这是性能最好的方式)。

test12 给出了 union 的三种访问方式

 
 
 
 
  1. // test12.go
  2. package main
  3. /*
  4. #include 
  5. union SayHello {
  6.  int Say;
  7.  float Hello;
  8. };
  9. union SayHello init_sayhello(){
  10.     union SayHello us;
  11.     us.Say = 100;
  12.     return us;
  13. }
  14. int SayHello_Say_helper(union SayHello * us){
  15.     return us->Say;
  16. }
  17. */
  18. import "C"
  19. import (
  20.     "fmt"
  21.     "unsafe"
  22.     "encoding/binary"
  23. )
  24. func main() {
  25.     SayHello := C.init_sayhello()
  26.     fmt.Println("C-helper ",C.SayHello_Say_helper(&SayHello))           // 通过C辅助函数
  27.     buff := C.GoBytes(unsafe.Pointer(&SayHello), 4)
  28.     Say2 := binary.LittleEndian.Uint32(buff)
  29.     fmt.Println("binary ",Say2)                 // 从内存直接解码一个int32
  30.     fmt.Println("unsafe modify ", *(*C.int)(unsafe.Pointer(&SayHello)))     // 强制类型转换
  31. }

3.4.3、枚举

对于枚举类型,可以通过C.enum_xxx来访问 C 语言中定义的enum xxx结构体类型。

使用方式和 C 相同,这里就不列例子了

3.5、指针

在 Go 语言中两个指针的类型完全一致则不需要转换可以直接通用。如果一个指针类型是用 type 命令在另一个指针类型基础之上构建的,换言之两个指针底层是相同完全结构的指针,那么也可以通过直接强制转换语法进行指针间的转换。

但是 C 语言中,不同类型的指针是可以显式或隐式转换。cgo 经常要面对的是 2 个完全不同类型的指针间的转换,实现这一转换的关键就是 unsafe.Pointer,类似于 C 语言中的 Void*类型指针。

使用这种方式就可以实现不同类型间的转换,如下是从 Go - int32 到 *C.char 的转换。

四、内部机制

go tool cgo 是分析 CGO 内部运行机制的重要工具,本章根据 cgo 工具生成的中间代码,再辅以 Golang 源码中 runtime 部分,来对 cgo 的内部运行机制进行分析。

cgo 的工作流程为:代码预处理 -> gcc 编译 -> Go Complier 编译。其产生的中间文件如图所示

4.1、Go 调 C

Go 调 C 的过程比较简单。test13 中定义了一个 C 函数 sum,并在 Go 中调用了 C.sum。

 
 
 
 
  1. package main
  2. //int sum(int a, int b) { return a+b; }
  3. import "C"
  4. func main() {
  5.  println(C.sum(1, 1))
  6. }

下面是 cgo 工具产生的中间文件,最重要的是 test13.cgo1.go,test13.cgo1.c,_cgo_gotypes.go

test13.cgo1.go

test13.cgo1.go 是原本 test13.go 被 cgo 处理之后的文件。

 
 
 
 
  1. // Code generated by cmd/cgo; DO NOT EDIT.
  2. //line test4.go:1:1
  3. package main
  4. //int sum(int a, int b) { return a+b; }
  5. import _ "unsafe"
  6. func main() {
  7.  println(( /*line :7:10*/_Cfunc_sum /*line :7:14*/)(1, 1))
  8. }

这个文件才是 go complier 真正编译的代码。可以看到原本的C.sum 被改写为_Cfunc_sum,_Cfunc_sum的定义在_cgo_gotypes.go 中。

_cgo_gotypes.go

 
 
 
 
  1. // Code generated by cmd/cgo; DO NOT EDIT.
  2. package main
  3. import "unsafe"
  4. import _ "runtime/cgo"
  5. import "syscall"
  6. var _ syscall.Errno
  7. func _Cgo_ptr(ptr unsafe.Pointer) unsafe.Pointer { return ptr }
  8. //go:linkname _Cgo_always_false runtime.cgoAlwaysFalse
  9. var _Cgo_always_false bool              //  永远为 false
  10. //go:linkname _Cgo_use runtime.cgoUse
  11. func _Cgo_use(interface{})              // 返回一个 Error
  12. type _Ctype_int int32                   // CGO类型导出
  13. type _Ctype_void [0]byte                // CGO类型导出
  14. //go:linkname _cgo_runtime_cgocall runtime.cgocall
  15. func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32            // Go调C的入口函数
  16. //go:linkname _cgo_runtime_cgocallback runtime.cgocallback
  17. func _cgo_runtime_cgocallback(unsafe.Pointer, unsafe.Pointer, uintptr, uintptr)     //  回调入口
  18. //go:linkname _cgoCheckPointer runtime.cgoCheckPointer
  19. func _cgoCheckPointer(interface{}, interface{})             // 检查传入C的指针,防止传入了指向Go指针的Go指针
  20. //go:linkname _cgoCheckResult runtime.cgoCheckResult
  21. func _cgoCheckResult(interface{})               //  检查返回值,防止返回了一个Go指针
  22. //go:cgo_import_static _cgo_53efb99bd95c_Cfunc_sum
  23. //go:linkname __cgofn__cgo_53efb99bd95c_Cfunc_sum _cgo_53efb99bd95c_Cfunc_sum
  24. var __cgofn__cgo_53efb99bd95c_Cfunc_sum byte                // 指向C空间的sum函
  25. var _cgo_53efb99bd95c_Cfunc_sum = unsafe.Pointer(&__cgofn__cgo_53efb99bd95c_Cfunc_sum)  // 将sum函数指针赋值给_cgo_53efb99bd95c_Cfunc_sum
  26. //go:cgo_unsafe_args
  27. func _Cfunc_sum(p0 _Ctype_int, p1 _Ctype_int) (r1 _Ctype_int) {
  28.  _cgo_runtime_cgocall(_cgo_53efb99bd95c_Cfunc_sum, uintptr(unsafe.Pointer(&p0))) // 将参数塞到列表中,调用C函数
  29.  if _Cgo_always_false {
  30.   _Cgo_use(p0)            // 针对编译器的优化操作,为了将C函数的参数分配在堆上,实际永远不会执行
  31.   _Cgo_use(p1)
  32.  }
  33.  return
  34. }

_cgo_gotypes.go 是 Go 调 C 的精髓,这里逐段分析。

_Cgo_always_false & _Cgo_use

 
 
 
 
  1. //go:linkname _Cgo_always_false runtime.cgoAlwaysFalse
  2. var _Cgo_always_false bool              //  永远为 false
  3. //go:linkname _Cgo_use runtime.cgoUse
  4. func _Cgo_use(interface{})              // 返回一个 Error
  5. ..........
  6. if _Cgo_always_false {
  7.  _Cgo_use(p0)            // 针对编译器的优化操作,为了将C函数的参数分配在堆上,实际永远不会执行
  8.  _Cgo_use(p1)
  9. }

_Cgo_always_false 是一个"常量",正常情况下永远为 false。

_Cgo_use的函数实现如下

 
 
 
 
  1. // runtime/cgo.go
  2. func cgoUse(interface{}) { throw("cgoUse should not be called") }

Go 中变量可以分配在栈或者堆上。栈中变量的地址会随着 go 程调度,发生变化。堆中变量则不会。

而程序进入到 C 空间后,会脱离 Go 程的调度机制,所以必须保证 C 函数的参数分配在堆上。

Go 通过在编译器里做逃逸分析来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。

由于栈上内存存在不需要 gc,内存碎片少,分配速度快等优点,所以 Go 会将变量更多的放在栈上。

_Cgo_use以 interface 类型为入参,编译器很难在编译期知道,变量最后会是什么类型,因此它的参数都会被分配在堆上。

_cgo_runtime_cgocall

 
 
 
 
  1. //go:linkname _cgo_runtime_cgocall runtime.cgocall
  2. func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32            // Go调C的入口函数

_cgo_runtime_cgocall是从 Go 调 C 的关键函数,这个函数里面做了一些调度相关的安排。

 
 
 
 
  1. // Call from Go to C.
  2. //
  3. // This must be nosplit because it's used for syscalls on some
  4. // platforms. Syscalls may have untyped arguments on the stack, so
  5. // it's not safe to grow or scan the stack.
  6. //
  7. //go:nosplit
  8. func cgocall(fn, arg unsafe.Pointer) int32 {
  9.  if !iscgo && GOOS != "solaris" && GOOS != "illumos" && GOOS != "windows" {
  10.   throw("cgocall unavailable")
  11.  }
  12.  if fn == nil {
  13.   throw("cgocall nil")
  14.  }
  15.  if raceenabled {                // 数据竞争检测,与CGO无瓜
  16.   racereleasemerge(unsafe.Pointer(&racecgosync))
  17.  }
  18.  mp := getg().m
  19.  mp.ncgocall++           // 统计 M 调用CGO次数
  20.  mp.ncgo++               // 周期内调用次数
  21.  // Reset traceback.
  22.  mp.cgoCallers[0] = 0

    分享文章:Go与C的桥梁:CGO入门剖析与实践
    文章转载:http://www.shufengxianlan.com/qtweb/news43/162243.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联