ch2-cgo

2.1 快速入门

基于C标准库函数输出字符串

1
2
3
4
5
6
7
8
9
// hello.go
package main

//#include <stdio.h>
import "C"

func main() {
C.puts(C.CString("Hello, World\n"))
}
  • 通过import "C"语句启用CGO特性,go build命令会在编译和链接阶段启动gcc编译器。紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的C语言代码。
  • 包含C语言的<stdio.h>头文件。
  • 通过CGO包的C.CString函数将Go语言字符串转为C语言字符串,
  • 调用CGO包的C.puts函数向标准输出窗口打印转换后的C字符串。

使用自己的C函数

自定义SayHello的C函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

/*
#include <stdio.h>

static void SayHello(const char* s) {
puts(s);
}
*/
import "C"

func main() {
C.SayHello(C.CString("Hello, World\n"))
}

引用外部的C文件:

SayHello函数放到当前目录下的一个C语言源文件中

1
2
3
4
5
6
7
8
9
// hello.go
package main

//void SayHello(const char* s);
import "C"

func main() {
C.SayHello(C.CString("Hello, World\n"))
}
1
2
3
4
5
6
// hello.c
#include <stdio.h>

void SayHello(const char* s) {
puts(s);
}

因为是编写在独立的C文件中,为了允许外部引用,所以需要去掉函数的static修饰符。

  • 既然可以将SayHello函数放到独立的C文件中,自然就可以引用对应的C文件编译打包为静态库或动态库文件。
  • 如果是以静态库或动态库方式引用SayHello函数的话,需要将对应的C源文件移出当前目录(CGO构建程序会自动构建当前目录下的C源文件,从而导致C函数名冲突)。

C代码的模块化

模块化编程的核心是面向程序接口编程(这里的接口并不是Go语言的interface,而是API的概念)。

1
2
// hello.h
void SayHello(const char* s);
1
2
3
4
5
6
7
8
// hello.c

#include "hello.h"
#include <stdio.h>

void SayHello(const char* s) {
puts(s);
}
1
2
3
4
5
6
7
8
9
// main.go
package main

//void SayHello(const char* s);
import "C"

func main() {
C.SayHello(C.CString("Hello, World\n"))
}

导出Go语言函数给C语言函数调用

1
2
// hello.h
void SayHello(/*const*/ char* s);
1
2
3
4
5
6
7
8
9
10
11
// hello.go
package main

import "C"

import "fmt"

//export SayHello
func SayHello(s *C.char) {
fmt.Print(C.GoString(s), "123")
}
1
2
3
4
5
6
7
8
9
// main.go
package main

//void SayHello(const char* s);
import "C"

func main() {
C.SayHello(C.CString("Hello, World\n"))
}
  • 为了适配CGO导出的C语言函数,我们禁止了在函数的声明语句中的const修饰符。
  • 通过CGO的//export SayHello指令将Go语言实现的函数SayHello导出为C语言函数。

_GoString_预定义类型

正常我们使用CGO如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

//void SayHello(char* s);
import "C"

import (
"fmt"
)

func main() {
C.SayHello(C.CString("Hello, World\n"))
}

//export SayHello
func SayHello(s *C.char) {
fmt.Print(C.GoString(s))
}

在Go1.10中CGO新增加了一个_GoString_预定义的C语言类型,用来表示Go语言字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// +build go1.10

package main

//void SayHello(_GoString_ s);
import "C"

import (
"fmt"
)

func main() {
C.SayHello("Hello, World\n")
}

//export SayHello
func SayHello(s string) {
fmt.Print(s)
}

2.2 CGO基础

cgo将当前包引用的C语言符号都放到了虚拟的C包中,同时当前包依赖的其它Go语言包内部可能也通过cgo引入了相似的虚拟C包。

比如我们希望在Go中定义一个C语言字符指针对应的CChar类型,然后增加一个GoString方法返回Go语言字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cgo_helper

//#include <stdio.h>
import "C"

type CChar C.char

func (p *CChar) GoString() string {
return C.GoString((*C.char)(p))
}

func PrintCString(cs *C.char) { // 这里的cs的类型是*cgo_helper.C.char
C.puts(cs)
}

现在我们可能会想在其它的Go语言包中也使用这个辅助函数:

1
2
3
4
5
6
7
8
9
package main

//static const char* cs = "hello";
import "C"
import "./cgo_helper"

func main() {
cgo_helper.PrintCString(C.cs) // 这里的C.cs的类型是*main.C.char
}
  • 上面代码是不能正常工作的,原因是两个文件的C.cs的类型其实是不同的。
    • main包引入的*C.char类型是*main.C.char
    • cgo_helper包引入的*C.char类型是*cgo_helper.C.char
  • 在Go语言中方法是依附于类型存在的,不同Go包中引入的虚拟的C包的类型却是不同的(main.C不等于cgo_helper.C),这导致从它们延伸出来的Go类型也是不同的类型(*main.C.char不等于*cgo_helper.C.char),这最终导致了前面代码不能正常工作。

简单来说,

  • 一个包如果在公开的接口中直接使用了*C.char等类似的虚拟C包的类型,那么其它的Go包是无法直接使用这些类型的,除非这个Go包同时也提供了*C.char类型的构造函数。
  • 因为这些诸多因素,如果想在go test环境直接测试这些cgo导出的类型也会有相同的限制。

#cgo语句

  • import "C"语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。
  • 编译阶段的参数主要用于定义相关宏和指定头文件检索路径。
  • 链接阶段的参数主要是指定库文件检索路径和要链接的库文件。
1
2
3
4
// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"
  • CFLAGS部分,

    • -D部分定义了宏PNG_DEBUG,值为1;

      -I定义了头文件包含的检索目录。

  • LDFLAGS部分,

    • -L指定了链接时库文件检索目录,
    • -l指定了链接时需要链接png库。

#cgo指令还支持条件选择,当满足某个操作系统或某个CPU架构类型时后面的编译或链接选项生效。

下面是分别针对windows和非windows下平台的编译和链接选项:

1
2
// #cgo windows CFLAGS: -DX86=1
// #cgo !windows LDFLAGS: -lm

其中在windows平台下,编译前会预定义X86宏为1;在非widnows平台下,在链接阶段会要求链接math数学库。这种用法对于在不同平台下只有少数编译选项差异的场景比较适用。

2.3 类型转换

Go语言和C语言类型对比:

C语言类型 CGO类型 Go语言类型
char C.char byte
singed char C.schar int8
unsigned char C.uchar uint8
short C.short int16
unsigned short C.ushort uint16
int C.int int32
unsigned int C.uint uint32
long C.long int32
unsigned long C.ulong uint32
long long int C.longlong int64
unsigned long long int C.ulonglong uint64
float C.float float32
double C.double float64
size_t C.size_t uint
  • CGO中,虽然C语言的int固定为4字节的大小,但是Go语言自己的intuint却在32位和64位系统下分别对应4个字节和8个字节大小。
  • 如果需要在C语言中访问Go语言的int类型,可以通过GoInt类型访问,GoInt类型在CGO工具生成的_cgo_export.h头文件中定义。
  • 下面是64位环境下,_cgo_export.h头文件生成的Go数值类型的定义:
1
2
3
4
5
6
7
8
9
10
11
12
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef float GoFloat32;
typedef double GoFloat64;

Go 字符串和切片

在CGO生成的_cgo_export.h头文件中还会为Go语言的字符串、切片、字典、接口和管道等特有的数据类型生成对应的C语言类型:

1
2
3
4
5
typedef struct { const char *p; GoInt n; } GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

不过需要注意的是,其中只有字符串和切片在CGO中有一定的使用价值,因为CGO为他们的某些GO语言版本的操作函数生成了C语言版本,因此二者可以在Go调用C语言函数时马上使用;而CGO并未针对其他的类型提供相关的辅助函数,且Go语言特有的内存模型导致我们无法保持这些由Go语言管理的内存指针,所以它们C语言环境并无使用的价值。

结构体、联合、枚举类型

结构体的内存布局按照C语言的通用对齐规则。

结构体的简单用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
struct A {
int i;
float f;
};
*/
import "C"
import "fmt"

func main() {
var a C.struct_A
fmt.Println(a.i)
fmt.Println(a.f)
}

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

1
2
3
4
5
6
7
8
9
10
11
12
/*
struct A {
int type; // type 是 Go 语言的关键字
};
*/
import "C"
import "fmt"

func main() {
var a C.struct_A
fmt.Println(a._type) // _type 对应 type
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
struct A {
int type; // type 是 Go 语言的关键字
float _type; // 将屏蔽CGO对 type 成员的访问
};
*/
import "C"
import "fmt"

func main() {
var a C.struct_A
fmt.Println(a._type) // _type 对应 _type
}
  • C语言结构体中位字段成员无法在Go语言中访问,如果需要操作位字段成员,需要通过在C语言中定义辅助函数来完成。
  • C语言结构体中零长数组成员无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)来访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
struct A {
int size: 10; // 位字段无法访问
float arr[]; // 零长的数组也无法访问
};
*/
import "C"
import "fmt"

func main() {
var a C.struct_A
fmt.Println(a.size) // 错误: 位字段无法访问
fmt.Println(a.arr) // 错误: 零长的数组也无法访问
}

在C语言中,我们无法直接访问Go语言定义的结构体类型。

对于联合类型,我们可以通过C.union_xxx来访问。但是Go语言中并不支持C语言联合类型,所以它们会被转为对应大小的字节数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
#include <stdint.h>

union B1 {
int i;
float f;
};

union B2 {
int8_t i8;
int64_t i64;
};
*/
import "C"
import "fmt"

func main() {
var b1 C.union_B1;
fmt.Printf("%T\n", b1) // [4]uint8

var b2 C.union_B2;
fmt.Printf("%T\n", b2) // [8]uint8
}

如果需要操作C语言的联合类型变量,一般有三种方法:

  • 在C语言中定义辅助函数;
  • 通过Go语言的”encoding/binary”手工解码成员(需要注意大端小端问题);
  • 使用unsafe包强制转型为对应类型(这是性能最好的方式)。

下面展示通过unsafe包访问联合类型成员的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
#include <stdint.h>

union B {
int i;
float f;
};
*/
import "C"
import "fmt"

func main() {
var b C.union_B;
fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b)))
fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b)))
}

虽然unsafe包访问最简单、性能也最好,但是对于有嵌套联合类型的情况处理会导致问题复杂化。对于复杂的联合类型,推荐通过在C语言中定义辅助函数的方式处理。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
enum C {
ONE,
TWO,
};
*/
import "C"
import "fmt"

func main() {
var c C.enum_C = C.TWO
fmt.Println(c)
fmt.Println(C.ONE)
fmt.Println(C.TWO)
}

在C语言中,枚举类型底层对应int类型,支持负数类型的值。我们可以通过C.ONEC.TWO等直接访问定义的枚举值。

数组、字符串和切片

  • 在C语言中,数组名其实对应于一个指针,指向特定类型特定长度的一段内存,但是这个指针不能被修改;当把数组名传递给一个函数时,实际上传递的是数组第一个元素的地址。为了讨论方便,我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。
  • 在Go语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。

Go语言和C语言的数组、字符串和切片之间的相互转换,可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换

CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte
  • 其中C.CString针对输入的Go字符串,克隆一个C语言格式的字符串;返回的字符串由C语言的malloc函数分配,不使用时需要通过C语言的free函数释放。C.CBytes函数的功能和C.CString类似,用于从输入的Go语言字节切片克隆一个C语言版本的字节数组,同样返回的数组需要在合适的时候释放。C.GoString用于将从NULL结尾的C语言字符串克隆一个Go语言字符串。C.GoStringN是另一个字符数组克隆函数。C.GoBytes用于从C语言数组,克隆一个Go语言字节切片。

该组辅助函数都是以克隆的方式运行。当Go语言字符串和切片向C语言转换时,克隆的内存由C语言的malloc函数分配,最终可以通过free函数释放。当C语言字符串或数组向Go语言转换时,克隆的内存由Go语言分配管理。通过该组转换函数,转换前和转换后的内存依然在各自的语言环境中,它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。