C语言作为一个通用语言,很多库会选择提供一个C兼容的API,然后用其他不同的编程语言实现。

Go语言通过自带的一个叫CGO的工具来支持C语言函数调用,同时我们可以用Go语言导出C动态库接口给其它语言使用。

要使用CGO特性,需要安装C/C++构建工具链,在macOS和Linux下是要安装GCC,在windows下是需要安装MinGW工具。同时需要保证环境变量CGO_ENABLED被设置为1,这表示CGO是被启用的状态。在本地构建时CGO_ENABLED默认是启用的,当交叉构建时CGO默认是禁止的。比如要交叉构建ARM环境运行的Go程序,需要手工设置好C/C++交叉构建的工具链,同时开启CGO_ENABLED环境变量。然后通过import "C"语句启用CGO特性。

调用C标准库函数

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

/**
* 调用 C 语言<stdio.h>头文件
*
//#include <stdio.h>
这里不能有空行
import (
"C"
)
*/

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

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

通过import "C"语句启用CGO特性,同时包含C语言的<stdio.h>头文件。然后通过CGO包的C.CString函数将Go语言字符串转为C语言字符串,最后调用CGO包的C.puts函数向标准输出窗口打印转换后的C字符串。

调用C自定义函数

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

/*
#include <stdio.h>

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

func main(){
C.SayHello(C.CString("hello, world\n"))
}

定义一个叫SayHello的C函数来实现打印,然后从Go语言环境中调用这个SayHello函数

也可以将SayHello的C函数放在当前目录的C语言源文件中(后缀名必须是.c)。因为是编写在独立的C文件中,为了允许外部引用,所以需要去掉函数的static修饰符。

hello.c文件

1
2
3
4
5
6
7
// hello.c

#include <stdio.h>

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

hello.go

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

注意 调用 不是用 go run hello.go 而是 go run "your/package"go build "your/package"才可以。

1
go run .

SayHello函数已经放到独立的C文件中了,我们自然可以将对应的C文件编译打包为静态库或动态库文件供使用。

用Go实现C函数

hello.h

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

hello.go 用Go语言重新实现C语言接口的SayHello函数:

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

通过CGO的//export SayHello指令将Go语言实现的函数SayHello导出为C语言函数。为了适配CGO导出的C语言函数,我们禁止了在函数的声明语句中的const修饰符。需要注意的是,这里其实有两个版本的SayHello函数:一个Go语言环境的;另一个是C语言环境的。cgo生成的C语言版本SayHello函数最终会通过桥接代码调用Go语言版本的SayHello函数。

main.go

1
2
3
4
5
6
7
8
package main

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

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

调用

1
go run .

面向C接口的Go编程

我们现在尝试将例子中的几个文件重新合并到一个Go文件

main.go

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

调用

1
go run main.go

现在版本的CGO代码中C语言代码的比例已经很少了,但是我们依然可以进一步以Go语言的思维来提炼我们的CGO代码。通过分析可以发现SayHello函数的参数如果可以直接使用Go字符串是最直接的。在Go1.10中CGO新增加了一个_GoString_预定义的C语言类型,用来表示Go语言字符串。下面是改进后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
}

虽然看起来全部是Go语言代码,但是执行的时候是先从Go语言的main函数,到CGO自动生成的C语言版本SayHello桥接函数,最后又回到了Go语言环境的SayHello函数。

原文

CGO编程-快速入门