CGO使用指南

文章目录
  1. 1. 序言
    1. 1.1. #cgo指令
  2. 2. 变量
    1. 2.1. Go 语言中引用 C 语言类型
      1. 2.1.1. char
      2. 2.1.2. short
      3. 2.1.3. int
      4. 2.1.4. long
      5. 2.1.5. longlong
      6. 2.1.6. float
      7. 2.1.7. double
      8. 2.1.8. struct
      9. 2.1.9. union
      10. 2.1.10. enum
      11. 2.1.11. Void*
      12. 2.1.12. C-Go 字符串类型转换
  3. 3. 函数
    1. 3.1. Go 语言调用 C 语言函数
      1. 3.1.1. C 语言调用 Go 语言函数
  4. 4. 进一步阅读

Go 提供一个名为C的伪包(pseudo-package)来与C 语言交互,这种Go语言与C语言交互的机制叫做CGO。当 Go 代码中加入import C语句来导入C这个不存在的包时候,会启动CGO特性。此后在Go 代码中我们可以使用C.前缀来引用C语言中的变量、类型,函数等。

序言

我们可以给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 build命令时候,我们可以使用-n选项查看所有执行的命令:

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

#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在CGO中使用方法如下:

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

变量

Go 语言中引用 C 语言类型

char

1
2
3
type C.char
type C.schar (signed char)
type C.uchar (unsigned char)

short

1
2
type C.short
type C.ushort (unsigned short)

int

1
2
type C.int
type C.uint (unsigned int)

long

1
2
type C.long
type C.ulong (unsigned long)

longlong

1
2
type C.longlong (long long)
type C.ulonglong (unsigned long long)

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))

C-Go 字符串类型转换

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

Go 类型转换成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

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
cs := C.CString("hello, world")
defer C.free(unsafe.Pointer(cs)) // C语言中字符串本质是char类型数组,free的参数是指向char数组的指针,所以需要使用unsafe.Pointer获取cs指针

函数

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语言中调用。

进一步阅读