3

Golang | CGO => 基础

 2 years ago
source link: https://ijayer.github.io/post/tech/code/golang/20171023-cgo-note/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

《深入 CGO 编程》 学习笔记

最简单的CGO程序 hello cgo

import "C"

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

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

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

/*
#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 函数

/*
#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 函数进行调用
// hello.c
#include <stdio.h>

void SayHello(char* str) {
    puts(str);
}
// 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 中定义:

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

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

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

#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头文件定义:

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

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

// 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函数的使用方式类似):

//#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文件。下面是合并后的成果:

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 语言字符串。下面是改进后的代码:

// +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 🙂

返回目录


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK