Go 语言
标准库、反射和 cgo
cgo 原理和使用开销

cgo 原理和使用开销

Go 语言有很强的 C 语言背景,除了语法具有继承外,其设计者及设计目标都与 C 语言有着千丝万缕的联系。 在 Go 语言与 C 语言的互操作方面,Go 更是提供了强大的支持,尤其是在 Go 中使用 C 语言,甚至可以直接在 Go 源码里编写 C 代码。这里的关键技术就是 cgo。

Go 调用 C 代码的原理

我们来看一个例子:

main.go
package main
 
// #include <stdio.h>
// #include <stdlib.h>
 
// void print(char* str) {
//     printf("%s\n", str);
// }
import "C"
 
import "unsafe"
 
func main() {
    s := "Hello, cgo!"
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.print(cs) // Hello, cgo!
}

与常规的 Go 代码相比,上述代码有几处特殊的地方:

  • C 代码直接出现在 Go 源文件中,但是都是以注释的形式存在
  • 紧邻注释了的 C 代码之后(中间没有空行),我们导入了一个名为 C 的包
  • 在 main 函数中通过 C 这个包调用了 C 代码中的函数 print

这里的 C 不是包名,而是一种类似名字空间的概念,也可以理解为伪包名。C 语言所有语法元素均在该伪包下面。 访问 C 语言元素时都要在其前面加上 C. 前缀。比如代码中的 C.CStringC.free

编译这个带 C 代码的 Go 源文件的方法如下:

go build -x -v main main.go

我们能看到实际编译过程中,go build 调用了名为 cgo 的工具,cgo 会识别和读取 Go 源文件中的 C 代码, 并将其提取后交给外部的 C 编译器(clang 或 gcc)编译,最后与 Go 源码编译后的目标文件链接成一个可执行程序。

在 Go 中使用 C 语言的类型

原生类型

1、数值类型

在 Go 中可以用如下方式访问 C 原生的数值类型:

C.char
C.schar (signed char)
C.uchar (unsigned char)
C.short
C.ushort (unsigned short)
C.int
C.uint (unsigned int)
C.long
C.ulong (unsigned long)
C.longlong (long long)
C.ulonglong (unsigned long long)
C.float
C.double

2、指针类型

原生数值类型的指针类型可按 Go 语法在类型前面加上 * 来访问,比如 var p *C.int。 但 void* 比较特殊,在 Go 中用 unsafe.Pointer 来表示。

3、字符串类型

C 语言中并不存在原生的字符串类型,在 C 中用带结尾 '\0' 的字符数组来表示字符串; 我们来看一个例子:

s := "Hello, cgo!\n"
cs := C.CString(s)
c.print(cs)
C.free(unsafe.Pointer(cs))

这个转换相当于在 C 的堆上分配了一块新内存空间,这样转型后所得到的 C 字符串 cs 并不能由 Go 的 GC 管理。 只能使用后手动释放,例子中通过 C.free 函数释放了这块内存。

4、数组类型

Go 仅提供了 C.GoBytes 来将 C 中的 char 类型数组转换为 Go 中的 []byte 切片类型:

// char cArray[] = {'a', 'b', 'c', 'd', 'e'};
import "C"
 
func main() {
    goArray := C.GoBytes(unsafe.Pointer(&C.cArray[0]), C.int(len(C.cArray)))
    fmt.Printf("%c\n", goArray) // [a b c d e]
}

自定义类型

1、枚举(enum)

在 Go 中可以用 C.enum_枚举名 来访问 C 语言中的枚举类型。

// enum color {
//     RED,
//     GREEN,
//     BLUE
// };
import "C"
 
func main() {
    var e, f, g C.enum_color = C.RED, C.GREEN, C.BLUE
    fmt.Println(e, f, g) // 0 1 2
}

2、结构体(struct)

和访问枚举的方式类似,可以用 C.struct_结构体名 来访问 C 语言中的结构体类型。

// struct point {
//     int x;
//     int y;
// };
import "C"
 
func main() {
    var p C.struct_point
    p.x = 10
    p.y = 20
    fmt.Println(p.x, p.y) // 10 20
}

3、联合(union)

联合类型在 Go 中可以用 C.union_联合名 来访问。

// union value {
//     int i;
//     float f;
// };
import "C"
 
func main() {
    var v C.union_value
    v.i = 10
    fmt.Println(v.i) // 10
    v.f = 3.14
    fmt.Println(v.f) // 3.14
}

4、别名类型(typedef)

在 Go 中可以用 C.别名 来访问 C 语言中的别名类型。

// typedef int myint;
import "C"
 
func main() {
    var i C.myint = 10
    fmt.Println(i) // 10
}

获取 C 类型大小

在 Go 中可以用 C.sizeof_类型 来获取 C 语言中的类型大小。

// #include <stdio.h>
import "C"
 
func main() {
    println(C.sizeof_char) // 1
    println(C.sizeof_int) // 4
    println(C.sizeof_long) // 8
    println(C.sizeof_float) // 4
    println(C.sizeof_double) // 8
}

在 Go 中链接外部 C 库

上面的例子都是演示了在 Go 中是如何访问 C 的类型、变量和函数,一般就是加上 C 前缀即可。

这里我们展示如何将 C 的代码以共享库的形式通过给 Go 源码。Go 提供了指示符 #cgo 来指定 C 编译器和链接器的选项。

hello.c
#include <stdio.h>
 
void hello() {
    printf("Hello, cgo!\n");
}
hello.h
void hello();

用下面命令将上面的 C 代码编译为一个动态共享库:

gcc -shared -o libhello.so hello.c
main.go
package main
 
// #cgo CFLAGS: -I${SRCDIR}
// #cgo LDFLAGS: -L${SRCDIR} -lhello
// #include "hello.h"
import "C"
 
func main() {
    C.hello()
}

编译 go 文件:

go build main.go
 
otool -L main
main:
    libhello.so (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1)