CGO使用指南

文章目录
  1. 1. 序言
    1. 1.1. #cgo指令
  2. 2. 数据类型
    1. 2.1. Go 语言中引用 C 语言类型
      1. 2.1.1. float
      2. 2.1.2. double
      3. 2.1.3. struct
      4. 2.1.4. union
      5. 2.1.5. enum
      6. 2.1.6. void*
    2. 2.2. C-Go 类型转换
      1. 2.2.1. 基本数值类型转换
      2. 2.2.2. Go 类型字符串转换成C 类型
      3. 2.2.3. C 类型字符串转换成Go 类型
      4. 2.2.4. Go 字节切片转换成 C 数组
  3. 3. 函数
    1. 3.1. Go 语言调用 C 语言函数
    2. 3.2. C 语言调用 Go 语言函数
  4. 4. 进一步阅读

Go 提供一个名为C的伪包(pseudo-package)用来与 C/C++ 语言进行交互操作,这种Go语言与C语言交互的机制叫做 CGO。通过 CGO 我们可以在 Go 语言中调用 C/C++ 代码,也可以在 C/C++ 代码中调用Go语言。CGO 本质就是 Go 实现的 FFI(全称为Foreign function interface,用来描述一种编程语言编写的程序可以调用另一种编程语言编写的服务的机制)解决方案。

当 Go 代码中加入import C语句来导入C这个不存在的包时候,会启动 CGO 特性。此后在 Go 代码中我们可以使用C.前缀来引用C语言中的变量、类型,函数等。启动 CGO 特性时候,需要确保环境变量 CGO_ENABLED 值是1,我们可以通过go env CGO_ENABLED查看该环境变量的值,通过go env -w CGO_ENABLED=1用来设置该环境变量值。

序言

我们可以给import C语句添加注释,在注释中可以引入C的头文件,以及定义和声明函数和变量,此后我们可以在 Go 代码中引用这些函数和变量。这种注释称为 序言(preamble) 。需要注意的是 序言和import C语句之间不能有换行,序言中的静态变量是不能被Go代码引用的,而静态函数是可以的。

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

/*
#include <stdio.h>
#include <stdlib.h>

static void myprint(char* s) {
printf("%s\n", s);
}
*/
import "C"
import "unsafe"

func main() {
cs := C.CString("hello world")
C.myprint(cs)
C.free(unsafe.Pointer(cs))
}

如果Go 代码中存在import C,那么编译时候Go 编译器会寻找代码目录中其他非Go文件,对于后缀为.c.s.S的文件,会使用C编译器进行编译。后缀为.cc.cpp.cxx 文件使用C编译器编译。对于.h.hh.hpp.hxx后缀的文件,它们是C/C的头文件,不会单独编译,如果更改了这些头文件,包括非Go代码都会重新编译。

执行go build命令时候,我们可以使用-n(-x也可以,只不过它会执行相关编译命令)选项查看所有要执行的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vagrant@vagrant:/tmp/cgo$ go build -n

#
# _/tmp/cgo
#

mkdir -p $WORK/b001/
cd /tmp/cgo
TERM='dumb' CGO_LDFLAGS='"-g" "-O2"' /usr/local/go1.14/pkg/tool/linux_amd64/cgo -objdir $WORK/b001/ -importpath _/tmp/cgo -- -I $WORK/b001/ -g -O2 ./main.go
cd $WORK
gcc -fno-caret-diagnostics -c -x c - -o /dev/null || true
gcc -Qunused-arguments -c -x c - -o /dev/null || true
gcc -fdebug-prefix-map=a=b -c -x c - -o /dev/null || true
gcc -gno-record-gcc-switches -c -x c - -o /dev/null || true
cd $WORK/b001
TERM='dumb' gcc -I /tmp/cgo -fPIC -m64 -pthread -fmessage-length=0 -I ./ -g -O2 -o /tmp/cgo/$WORK/b001/_x001.o -c _cgo_export.c
cd $WORK
gcc -fno-caret-diagnostics -c -x c - -o /dev/null || true
gcc -Qunused-arguments -c -x c - -o /dev/null || true
gcc -fdebug-prefix-map=a=b -c -x c - -o /dev/null || true
gcc -gno-record-gcc-switches -c -x c - -o /dev/null || true
cd $WORK/b001
...

从上面可以看到在CGO模式下,gcc参加了编译工作。需要注意同Go包一样,C 语言编译后也会缓存起来,下次编译时候直接使用,如果我们之前构建过应用,在代码没有变动情况下,使用go build -n就看不见gcc命令了。这时候我们可以使用-a选项强制重新构建包。

另外借助go tool cgo工具,我们可以生成绑定的代码:

1
go tool cgo main.go

上面命令会在当前目录下面生_obj目录,该目录存放着一系列生成的文件:

  • _cgo_gotypes.go:定义 C 函数和类型的 Go 封装。
  • _cgo_main.c:主要用于初始化 C 和 Go 的运行时。
  • _cgo_export.c:包含 Go 导出到 C 的接口实现。

#cgo指令

在序言中我们可以使用#cgo指令来设置在构建 C 编译器参数 CFLAGS 和链接器参数 LDFLAGS

1
2
3
4
5
// #cgo CFLAGS: -g -Wall -I./include
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"

上面 CFLAGS-g 选项用于开启debug symbols-Wall 用于开启all warning-I 用于设置头文件目录,-DPNG_DEBUG=1 用于设置宏PNG_DEBUG值为1。

在设置 LDFLAGS 时候,可以使用 ${SRCDIR} 来替换源码的路径,比如:

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

将会拓展为

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

CGO编译时候,总是隐式包含-I${SRCDIR}这个链接选项,并且优先级是高于系统include目录,或者-I指定的目录。这意味着如果foo/bar.h这个文件既存在于代码目录中,也存在系统目录中,#include <foo/bar.h>始终优先使用本地代码目录的版本。

此外#cgo指令还支持条件选择,只有满足特定系统或者CPU架构时候编译或者连接选项才会生效:

1
2
// #cgo amd64 386 CFLAGS: -DX86=1 // amd64 368 平台才设置该编译选项
// #cgo !amd64 CFLAGS: -DX86=1 // 非amd64平台才设置编译该编译选项

当然我们也可以使用#cgo pkg-config指令来获取设置CFLAGS和LDFLAGS参数。#cgo pkg-config指令依赖pkg-config命令,pkg-config可以帮助我们编译时候插入正确的编译器参数,而不必硬编码,例如我们可以使用 gcc -o test test.c $(pkg-config --libs --cflags glib-2.0) 来找到glib库。你可以查看这篇文章来获取有关 pkg-config 更多信息。pkg-config在CGO中使用方法如下:

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

数据类型

Go 语言中引用 C 语言类型

float

1
type C.float

double

1
type C.double

struct

1
type C.struct_<name_of_C_Struct>

union

1
C.union_<name_of_C_Union>

enum

1
C.enum_<name_of_C_Enum>

void*

C语言中的void *对应Go语言中的unsafe.Pointer

1
2
cs := C.CString("Hello from stdio")
C.free(unsafe.Pointer(cs)) // free函数接收void *类型参数

C-Go 类型转换

基本数值类型转换

Go 和 C 的基本数值类型转换对照表如下:

C语言类型 CGO类型 Go类型
char C.char byte
singed char C.schar int8
unsigned char C.uchar uint8/byte
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

C 中的整形 int 在标准中是没有定义具体字长的,但一般默认认为是 4 字节,对应 CGO 类型中 C.int 则明确定义了字长是 4 ,但 golang 中的 int 字长则是 8 ,因此对应的 Go 类型不是 int 而是 int32 。为了避免误用,C 代码最好使用 C99 标准的数值类型(引入<stdint.h>头),对应的转换关系如下:

C语言类型 CGO类型 Go类型
int8_t C.int8_t int8
uint8_t C.uint8_t uint8
int16_t C.int16_t int16
uint16_t C.uint16_t uint16
int32_t C.int32_t int32
uint32_t C.uint32_t uint32
int64_t C.int64_t int64
uint64_t C.uint64_t uint64

Go 类型字符串转换成C 类型

由于C语言和Go语言中字符串底层内存模型不一样,且 Go 是gc型语言,Go类型字符串需要转换成C类型字符串,才能作为参数传递给C函数,反过来也是一样。

1
2
3
4
5
6
7
8
9
10
// 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)
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)
func C.CBytes([]byte) unsafe.Pointer

需要注意得是 Go 类型字符串转换成 C 类型字符串之后,以及 Go类型字节切片 转换成C类型字节数组时候,需要手动进行回收:

1
2
3
4
5
cs := C.CString("hello, world") // C.CString底层使用到C语言malloc进行内存分配,所以需要调用C语言free函数进行释放
defer C.free(unsafe.Pointer(cs)) // C语言中字符串本质是char类型数组,free的参数是指向char数组的指针,所以需要使用unsafe.Pointer获取cs指针

cBytes := C.CBytes(goBytes) // go字节切片转换成C类型字节数字
defer C.free(cBytes) // 释放内存

C 类型字符串转换成Go 类型

1
2
3
4
5
6
7
8
// 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 (in bytes) to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

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
24
package main

/*
#include <stdint.h>

static void fill_255(char* buf, int32_t len) {
int32_t i;
for (i = 0; i < len; i++) {
buf[i] = 255;
}
}
*/
import "C"
import (
"fmt"
"unsafe"
)

func main() {
b := make([]byte, 5)
fmt.Println(b) // [0 0 0 0 0]
C.fill_255((*C.char)(unsafe.Pointer(&b[0])), C.int32_t(len(b)))
fmt.Println(b) // [255 255 255 255 255]
}

函数

Go 语言调用 C 语言函数

只要我们在序言中设置好#include之后,我们就可以在Go 语言可以直接调用标准库里面的函数,或者其他库里面的函数:

1
2
3
4
5
6
7
8
package main

//#include <stdio.h> // 在序言中引入标准io库
import "C"

func main() {
C.puts(C.CString("Hello, world\n"))
}

我们也可以调用序言中定义的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
/*
#cgo CFLAGS: -g -Wall
#include <stdio.h>
#include <stdlib.h>
int greet(const char *name, int year, char *out) {
int n;
n = sprintf(out, "Greetings, %s from %d! We come in peace :)", name, year);
return n;
}
*/
import "C"
import (
"unsafe"
"fmt"
)

func main() {
name := C.CString("Gopher")
defer C.free(unsafe.Pointer(name))

year := C.int(2022)

ptr := C.malloc(C.sizeof_char * 1024)
defer C.free(unsafe.Pointer(ptr))

size := C.greet(name, year, (*C.char)(ptr))

b := C.GoBytes(ptr, size)
fmt.Println(string(b))
}

当然我们也可以调用外部C文件中定义的函数:

greeter.h文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
#ifndef _GREETER_H
#define _GREETER_H

struct Greetee {
const char *name;
int year;
};

int greet(const char *name, int year, char *out);
int greet2(struct Greetee *g, char *out);
#endif

greeter.c文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "greeter.h"
#include <stdio.h>

int greet(const char *name, int year, char *out) {
int n;
n = sprintf(out, "Greetings, %s from %d! We come in peace :)", name, year);
return n;
}

int greet2(struct Greetee *g, char *out) {
int n;
n = sprintf(out, "Greetings, %s from %d! We come in peace :)", g->name, g->year);
return n;
}

go文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

// #cgo CFLAGS: -g -Wall
// #include <stdlib.h>
// #include "greeter.h"
import "C"
import (
"fmt"
"unsafe"
)

func main() {
name := C.CString("Gopher")
defer C.free(unsafe.Pointer(name))

year := C.int(2022)

ptr := C.malloc(C.sizeof_char * 1024)
defer C.free(unsafe.Pointer(ptr))

size := C.greet(name, year, (*C.char)(ptr))

b := C.GoBytes(ptr, size)
fmt.Println(string(b))

g := C.struct_Greetee{
name: name,
year: year,
}
ptr := C.malloc(C.sizeof_char * 1024)
defer C.free(unsafe.Pointer(ptr))
size := C.greet2(&g, (*C.char)(ptr))
b := C.GoBytes(ptr, size)
fmt.Println(string(b))
}

Go语言也只调用C对象文件中的函数,但是相应头文件一定要指定。上面例子中我们可以使用gcc -c greeter.c生成对象文件greeter.o,然后删除调用greeter.c文件,接着在main.go文件中添加以下LDFLAGS参数,也是OK的:

1
// #cgo LDFLAGS: ./greeter.o

如果调用C函数时,如果有两个返回值,那么第二个返回值对应的是<errno.h>标准库errno宏,它用于记录错误状态:

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

static int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
*/
import "C"
import "fmt"

func main() {
v0, err0 := C.div(2, 1)
fmt.Println(v0, err0)

v1, err1 := C.div(1, 0)
fmt.Println(v1, err1)
}

上面代码将会输出:

1
2
2 <nil>
0 invalid argument

C 语言调用 Go 语言函数

Go语言中函数要能被C语言调用,需要使用 export 指令进行导出。

greet.go文件内容如下:

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

import "C"
import "fmt"

//export greet
func greet() {
fmt.Printf("hello, world")
}

func main(){}

我们使用go build -buildmode=c-archive greet.go,将greet.go构建成C存档文件,此时会生成greet.agreet.h文件。需要注意的是go文件中一定要导入C伪包,以及存在main包。

对应的C文件是main.c,内容如下:

1
2
3
4
5
6
#include <stdio.h>
#include "greet.h"

void main() {
greet();
}

最后使用gcc -pthread main.c greet.a -o main构建C二进制应用,-pthread选项是必须的,因为Go runtime使用到线程。我们也可以使用go build -buildmode=c-shared将Go代码编译成C共享文件,然后在C语言中调用。

进一步阅读