《深入 CGO 编程》 学习笔记

最简单的CGO程序 hello cgo

1
2
3
4
5
import "C"

func main() {
    println("hello cgo")
}

代码通过 import "C" 语句启用 CGO 特性,主函数只是通过 Go 内置的 println 函数输出字符串,其中并没有任何和 CGO 相关的代码。虽然没有调用 CGO 的相关函数,但是 go build 命令会在编译和链接阶段启动 gcc 编译器,这已经是一个完整的 CGO 程序了。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*
#include <stdio.h>
*/
import "C"

func main() {
	C.puts(C.CString("hello cgo"))

	// output:
	// $ go run hello-cgo.go
	// hello cgo
}

上面代码有个缺陷:

  • 没有在程序退出前,释放 C.CString 创建的 C 语言字符串, 这样会导致内存泄漏 (Note: 至于为什么要释放在后面说明)

使用自己的 C 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*
#include <stdio.h>

void SayHello(char* str) {
    puts(str)
}
*/
import "C"

func main() {
    C.SayHello(C.CString("hello cgo"))
}

使用 C 源文件调用 C 函数

我们也可以将 SayHello 函数放到当前目录下的一个C语言源文件中(后缀名必须是.c)

  • 创建 hello.c 文件,实现 SayHello 函数
  • 然后在 Go 源文件的 CGO 部分声明 SayHello 函数进行调用
1
2
3
4
5
6
// hello.c
#include <stdio.h>

void SayHello(char* str) {
    puts(str);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// hello.go
package main

/*
#include <stdio.h>

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

func main() {
	C.SayHello(C.CString("hello cgo"))
}

当 SayHello 函数在 C 源文件(hello.c) 中实现以后,我们可以将该 C 源文件打包为静态或动态库文件供 Go 程序调用

  • Note: 如果以静态库或动态库的方式引用 SayHello 函数的话,需要将对应的 c 源文件移除当前目录,因为: CGO 在构建程序时,会自动自动构建当前目录下的 c 源文件,从而导致 C 函数名冲突。关于静态库等细节后面再仔细学习。

C 代码的模块化

在编程过程中,抽象和模块化是将复杂问题简化的通用手段。当代码语句变多时,我们可以将相似的代码封装到一个个函数中;当程序中的函数变多时,我们将函数拆分到不同的文件或模块中。而模块化编程的核心是面向程序接口编程(这里的接口并不是Go语言的interface,而是API的概念)。

在前面的例子中,我们抽象一个名为 hello 的模块,模块的全部接口函数都在 hello.h 中定义:

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

上面代码中,只有一个SayHello函数的声明。但是作为hello模块的用户来说,就可以放心地使用SayHello函数,而无需关心函数的具体实现。作为SayHello函数的实现者来说,函数的实现只要满足头文件中函数的声明的规范即可。

下面是 SayHello 函数的 C 语言实现,对应:hello.c 文件

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

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

在 hello.c 的文件开头,实现者通过 #include “hello.h” 语句包含 SayHello 函数声明的签名,这样可以保证函数的实现满足模块的外的公开接口

用 Go 重新实现 C 函数

其实CGO不仅仅用于Go语言中调用C语言函数,还可以用于导出Go语言函数给C语言函数调用。在前面的例子中,我们已经抽象一个名为hello的模块,模块的全部接口函数都在hello.h头文件定义:

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

现在我们创建一个hello.go文件来用Go语言重新实现C语言接口的SayHello函数:

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

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

然后通过 CGO 的 //export SayHello 指令将Go语言实现的函数SayHello导出为C语言函数。需要注意的是,这里其实有两个版本的SayHello函数:一个Go语言环境的;另一个是C语言环境的。cgo生成的C语言版本SayHello函数最终会通过桥接代码调用Go语言版本的SayHello函数。

通过面向C语言接口的编程技术,我们不仅仅解放了函数的实现者,同时也简化的函数的使用者。现在我们可以将SayHello当作一个标准库的函数使用(和puts函数的使用方式类似):

1
2
3
4
5
6
7
//#include "hello.h"
import "C"
import "fmt"

func main() {
	C.SayHello(C.CString("hello cgo")) // C.SayHello 调用的是 hello.h 中定义的 SayHello 函数
}

面向 C 接口的 Go 编程

在开始的例子中,我们的全部CGO代码都在一个Go文件中。然后,通过面向C接口编程的技术将SayHello分别拆分到不同的C文件,而main依然是Go文件。再然后,是用Go函数重新实现了C语言接口的SayHello函数。但是对于目前的例子来说只有一个函数,要拆分到三个不同的文件确实有些繁琐了。

正所谓合久必分、分久必合,我们现在尝试将例子中的几个文件重新合并到一个Go文件。下面是合并后的成果:

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

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

func main() {
    C.SayHello(C.CString("hello, world!")) // C.SayHello 调用的是 Go 导出的接口函数
}

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

上面代码中 Go 的导出函数 SayHello 参数类型还是使用的 Go 中 C 的原生数据类型。 但是如果可以直接用 Go 的 string 类型则是最直接的。在 Go 1.10 中 CGO 增加了一个 GoString 预定义的 C 语言类型,用来表示 Go 语言字符串。下面是改进后的代码:

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

package main

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

func main() {
    C.SayHello("Hello, World") // C.SayHello 调用的是 Go 导出的接口函数
}

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

Note: 上面代码虽然看起来都是 Go 语言代码,但是执行的时候还是从 Go 的 main 函数开始执行,到 CGO 自动生成的 C 语言版本的 SayHello 桥接函数,最后又回到了Go语言环境的SayHello函数。这个代码包含了CGO编程的精华。

思考题: main函数和SayHello函数是否在同一个Goroutine只执行?

Click here to checkout the Repo

See Also

Thanks to the authors 🙂

返回目录