专栏名称: 狗厂
目录
51好读  ›  专栏  ›  狗厂

深入CGO编程

狗厂  · 掘金  ·  · 2018-05-28 06:43

正文

树 杉

青云QingCloud应用平台研发工程师,开源的多云应用管理平台OpenPitrix开发者,Go 语言代码的贡献者,《Go 语言圣经》翻译者,《Go 语言高级编程》开源免费图书作者。2010年开始参与和组织 Go 语言早期文档翻译,2013年正式转向Go语言开发,CGO资深用户。

1. CGO的价值

2. 快速入门

3. 类型转换

4. 函数调用

5. CGO内部机制

6. 实践:包装 c.qsort

7. 内存模型

8. Go和 C++对象

背景

在2017年年底初步完成了《Go 语言高级编程》的第二章 CGO 编程部分。当时刚好 GopherChina2018 在招聘讲师,我就找谢孟军申请了 CGO 的分享主题。选择 CGO 作为分享主题的原因有二:一是国内外对 CGO 编程的分享主题比较少;二是我想借此机会重新将 CGO 编程的部分的内容彻底梳理一次。第一次分享 CGO 主题是在2011年深圳的珠三角技术沙龙,这是可能是最后一次分享 CGO 的主题,我希望将 CGO 的问题彻底画上一个句号。

CGO 的幻灯片和《Go 语言高级编程》的第二章CGO编程部分基本是一一对应的,因此对于一个分享来说内容就太多了。因为时间的关系,现场分享时只保留了基础和重要的快速入门、类型转换、函数、内存模型等部分。不过这个 ppt的内容已经开源,感兴趣的同学可以直接查看,也可以将《Go 语言高级编程》的 CGO 章节内容作为参考。

《Go语言高级编程》第二章 CGO 编程:

https://github.com/chai2010/advanced-go-programming-book

1.CGO的价值

1. 没有银弹,Go 也不是银弹,无法解决全部问题

2. 通过 CGO 可以继承 C/C++ 将近半个世纪的软件积累,站在巨人的肩膀之上

3. 通过可以用 CGO 可以用 Go 给其他系统写 C 接口的共享库

4. CGO 是 Go 给其他语言直接通讯的桥梁

CGO 是一个保底的后备技术。

可能的 CGO 场景:

  • 通过 OpenGL 或 OpenCL 使用显示卡的计算能力

  • 通过 OpenCV 来进行图像分析

  • 通过 Go 编写 Python 扩展

  • 通过 Go 编写移动应用

2.快速入门

其实 CGO 程序可以非常简单:只要包含一个 import“C” 语句 就表示已经启用CGO。当然这这种程序没有多少实际用途。

下是相对简单一点的 CGO 程序:通过 C 语言的输出函数puts输出一段信息。

 

在 import “C" 语句前面增加来<stdio.h> 语句,通过包含这个头文件,我们可以使用 C 语言的 C.puts 函数实现输出字符串的功能。然后输入 go run main.go 命令就 执行就可以执行该 CGO 程序了。当然,构建 CGO 程序的一个前提是要安装 GCC 编译器。

1.1 调用自定义的C函数

 

刚才是使用 puts 输出字符串,现在可以前进一步:通过自定义的函数实现输出。我们同样在 import "C " 语句之前的注释里面实现自定义的 Sayhello 函数。调用自己定义的函数实现某些功能,这是任何编程语言学习过程中非常重要的一个阶段。

1.2 C代码模块化

 

模块化是一种重要的编程方法。当程序中的一行语句太长的时候,我们希望将代码拆分为多行;当代码语句多到一定层度,我们就会将代码拆分为函数;如果一个文件中的函数太多,则希望将函数拆分到多个文件中重新组织。以上这些都是采用模块化的思路来简化代码的组织。

对于前面的 SayHello 函数,我们也可以采用模块化的测试来重新组织。首选创建一个 hello.h 头文件,里面包含 SayHello 函数的声明。然后将 SayHello 函数的实现放到 hello.c 文件中。在 CGO 代码中,就可以通过 #include "hello.h" 的方式直接引用 SayHello 函数。

1.3 Go 语言实现 C 模块

创建 hello.h 头文件是模块化编程的一个重要里程碑。对于 SayHello 函数的用户来说,我们只需要知道 SayHello 函数满足 C 语言函数的调用规约即可。至于SayHello 函数是采用 C 语言或 C++ 语言、甚至是其它任何语言实现的,对于SayHello 函数的用户并没有区别。因此,我们可以该 Go 语言重新实现SayHello 函数。

 

hello.h 头文件包含 SyaHello 函数声明,但是 hello.c 变成了 hello.go,函数本身从 C 语言实现改成了用 Go 语言实现 。Go 语言实现的函数和 C 语言版本的函数名字和参数类型几乎是完全一致的(Go 导出的 C 函数不支持 const 修饰),因此对于 SayHello 函数的用户来说并没有太多的差异。现在可以说我是采用 C 语言思维编程的 Go 语言码农。

1.4 手中无剑,心中有剑

在模块化的基础上,我们采用 Go 重新实现了 C 语言规格的 SayHello 函数。现在我们可以尝试打破模块化编程的思路:删除 hello.h 头文件,将全部的 CGO 代码统一到一个 Go 源文件中:

 

这时候虽然没有了头文件的函数声明,但是 SayHello 函数的声明在我们 Go 语言码农的心中。我们通过 extern 的方式在 CGO 中手工声明 SayHello 函数。然后在 main 函数中调用一个目前还不真是存在的 SayHello 函数进行字符串输出。这个例子其实90%以上是 Go 语言代码,但是编程的思维是 C 语言 。

1.5 忘掉心中之剑

前面的实现中,虽然手中无剑,但是心中有剑:在导出 SayHello 函数时,依然采用了 C 语言的字符串格式。为此,在输出 Go 语言字符串时,需要先转换为C 语言格式的字符串;然后在 Go 语言中输出 C 语言字符串时,有需要转回 Go语言字符串;最后还需要释放中间临时创建的 C 语言字符串。这是心中编程思维被 C 语言字符串固化的结果。

我们需要忘掉 C 语言原有的字符串结构:其实从 C 语言角度看来,Go 语言字符串也是一种特殊格式的字符串。

 

新的实现采用 Go 语言格式的字符串作为 SayHello 函数的参数,中间将不再有额外的字符串转换的开销。

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

3.类型转换

 

有些编程语言的教程中将“数据结构+算法”作为程序的定义。数据结构对应一切变量和变量对应的结构化数据,算法可以近似看作是函数的内在逻辑。因此如何解决不同类型变量之间的数据转换是第一个要解决的问题。

在 C 语言中,对不同类型之间的转换相当灵活,甚至普通整数和函数指针也可以自由直接转换。但是 Go 则对不同类型之间的转换有着非常严格的限制。指针是 C 语言的核心类型,因此 CGO 中围绕指针周边的类型转换也是第一个要解决的问题。

为此 Go 语言提供了一个 unsafe 包,用于提供不安全的类型转换。其实 unsafe包是一个非常安全的包,但是前提是你要彻底理解 unsafe 操作底层的含义。如果离开了 unsafe 包,CGO 编程将寸步难行!

CGO 编程中会涉及到 Go 指针和 C 指针之间的转换,还有数值类型和指针之间的转换。不同类型的指针转换,字符串和切片的转换,基本上主要布局在指针类型、数值类型,字符串和切片。

3.1 指针- unsafe包的灵魂

 

指针是 C 语言的灵魂,自然也是 unsafe 包的灵魂。unsafe.Pointer 对应 C 语言的 void 类型指针,是 GC 垃圾回收器要管理的对象; uintptr 则是数值化的指针,并不参与 GC 的管理。在 C 语言中 uintptr 和指针并无太大差异,但是在Go 语言中二者确实完全不同的类型。因为 Go 语言的指针可能会因为栈的伸缩而被移动,移动时 GC 会自动维护指针的变化,uintptr 类型的变量将无法实现指针被移动时的自动更新。

3.2 unsafe 包

 

 

unsafe 包的每个用法在 C/C++ 语言中都有对应的特性,熟悉 C 语言的用户应该比较容易理解。

3.3 Go 字符串和切片的结构

 

Go 语言和 C 语言是不同的语言,CGO 是二者连接的桥梁。CGO 也可以实现两者的数据共享,底层的基础正是扁平化的内存。因此有着扁平化内存结构的Go 字符串和 Go 字节切片将是 CGO 中需要频繁处理的类型。字符串和切片的结构在 reflect.StringHeader 和 reflect.SliceHeader 定义,CGO 中会生成对应的 C 语言结构体。

GoString 和 GoSlice 和头部结构是兼容的。这样可以保证字符串和切片是兼容的,如果一个字符串转成切片或者反向转过来,某种优化的时候就是一个指针加一个长度。

3.4  实践:int32 和 C.char 指针 相互转换

 

第一种是普通整数类型到指针类型的转换。中间需要数值化的指针类型 unitptr 和通用指针类型 unsafe.Pointer 作为转换中介。基于这个技术,就可以实现任何数值类型到任何指针类型的强制转换。

 

这个代码主要是刚才图的描述。

3.5  X 和 Y 的相互转换

 

然后是X*和Y*相互转换比较简单,通过 unsafe.Pointer 做中介就可以达成转换。

 

这个是P到Q的转换,然后可以转成X*和Y*。

3.6  [ ]X 和 [ ]Y 的相互转换

 

不同类型的切片之间的转换则比较复杂。因为切片是值类型,我们无法对两个不同的值类型对象做转换,因为两个类型在内存的大小可能并不相同。

 

转换不同的值类型的第一步是将值类型转为指针类型(因为任何类型的指针大小是相同的)。对于切片来说,通过 PX 和 PY 转换为两个指向不同切片类型的指针,切片指针的底层结构都是对应同一个 reflect.SliceHeader 类型。然后通过指针实现不同类型切片头部的复制,就是实现了 X 和 Y 切片的转换。

3. 7 实例:float64 数 排序优化

 

比如要对正规的 float64 数组就行排序。如果CPU没有浮点运算的指令,我们可以将 float64 切片作为 int64 切片来排序(可能会快一点)。具体的原理是:float64 是遵循 IEEE754 标准的浮点数,当浮点数有序时作为整数也是有序的(不考虑非数和正负0的问题)。

在 AMD64 平台,我们用前面的技术将 [ ]float64 转为 [ ]int,然后就可以用sort.Int 对浮点数进行排序了。

4.函数调用







请到「今天看啥」查看全文