1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 04:22:22 +00:00

ch2: 规范化内部段落的编号

This commit is contained in:
chai2010 2018-08-05 08:12:02 +08:00
parent 23ba7def78
commit f97219b07b
10 changed files with 53 additions and 69 deletions

View File

@ -2,7 +2,7 @@
本节我们将通过由浅入深的一系列小例子来快速掌握CGO的基本用法。
## 最简CGO程序
## 2.1.1 最简CGO程序
真实的CGO程序一般都比较复杂。不过我们可以反其道而行之一个最简的CGO程序该是什么样的呢要构造一个最简CGO程序首先要去掉一些复杂的CGO特性同时要展示CGO程序和纯Go程序的差别来。下面是我们构建的最简CGO程序
@ -16,7 +16,7 @@ func main() {
代码通过`import "C"`语句启用CGO特性主函数只是通过Go内置的println函数输出字符串其中并没有任何和CGO相关的代码。虽然没有调用CGO的相关函数但是go build命令会在编译和链接阶段启动gcc编译器这已经是一个完整的CGO程序了。
## 基于C标准库函数输出字符串
## 2.1.2 基于C标准库函数输出字符串
第一章那个CGO程序还不够简单我们现在来看看更简单的版本
@ -37,7 +37,7 @@ func main() {
没有释放使用`C.CString`创建的C语言字符串会导致内存泄露。但是对于这个小程序来说这样是没有问题的因为程序退出后操作系统会自动回收程序的所有资源。
## 使用自己的C函数
## 2.1.3 使用自己的C函数
前面我们使用了标准库中已有的函数。现在我们先自定义一个叫`SayHello`的C函数来实现打印然后从Go语言环境中调用这个`SayHello`函数:
@ -87,7 +87,7 @@ func main() {
既然`SayHello`函数已经放到独立的C文件中了我们自然可以将对应的C文件编译打包为静态库或动态库文件供使用。如果是以静态库或动态库方式引用`SayHello`函数的话需要将对应的C源文件移出当前目录CGO构建程序会自动构建当前目录下的C源文件从而导致C函数名冲突。关于静态库等细节将在稍后章节讲解。
## C代码的模块化
## 2.1.4 C代码的模块化
在编程过程中抽象和模块化是将复杂问题简化的通用手段。当代码语句变多时我们可以将相似的代码封装到一个个函数中当程序中的函数变多时我们将函数拆分到不同的文件或模块中。而模块化编程的核心是面向程序接口编程这里的接口并不是Go语言的interface而是API的概念
@ -133,7 +133,7 @@ void SayHello(const char* s) {
在采用面向C语言API接口编程之后我们彻底解放了模块实现者的语言枷锁实现者可以用任何编程语言实现模块只要最终满足公开的API约定即可。我们可以用C语言实现SayHello函数也可以使用更复杂的C++语言来实现SayHello函数当然我们也可以用汇编语言甚至Go语言来重新实现SayHello函数。
## 用Go重新实现C函数
## 2.1.5 用Go重新实现C函数
其实CGO不仅仅用于Go语言中调用C语言函数还可以用于导出Go语言函数给C语言函数调用。在前面的例子中我们已经抽象一个名为hello的模块模块的全部接口函数都在hello.h头文件定义
@ -171,7 +171,7 @@ func main() {
一切似乎都回到了开始的CGO代码但是代码内涵更丰富了。
## 面向C接口的Go编程
## 2.1.6 面向C接口的Go编程
在开始的例子中我们的全部CGO代码都在一个Go文件中。然后通过面向C接口编程的技术将SayHello分别拆分到不同的C文件而main依然是Go文件。再然后是用Go函数重新实现了C语言接口的SayHello函数。但是对于目前的例子来说只有一个函数要拆分到三个不同的文件确实有些繁琐了。
@ -223,4 +223,4 @@ func SayHello(s string) {
虽然看起来全部是Go语言代码但是执行的时候是先从Go语言的`main`函数到CGO自动生成的C语言版本`SayHello`桥接函数最后又回到了Go语言环境的`SayHello`函数。这个代码包含了CGO编程的精华读者需要深入理解。
思考题: main函数和SayHello函数是否在同一个Goroutine只执行
*思考题: main函数和SayHello函数是否在同一个Goroutine只执行*

View File

@ -2,7 +2,7 @@
要使用CGO特性需要安装CC++构建工具链在macOS和Linux下是要安装GCC在windows下是需要安装MinGW工具。同时需要保证环境变量`CGO_ENABLED`被设置为1这表示CGO是被启用的状态。在本地构建时`CGO_ENABLED`默认是启用的当交叉构建时CGO默认是禁止的。比如要交叉构建ARM环境运行的Go程序需要手工设置好C/C++交叉构建的工具链,同时开启`CGO_ENABLED`环境变量。然后通过`import "C"`语句启用CGO特性。
## `import "C"`语句
## 2.2.1 `import "C"`语句
如果在Go代码中出现了`import "C"`语句则表示使用了CGO特性紧跟在这行语句前面的注释是一种特殊语法里面包含的是正常的C语言代码。当确保CGO启用的情况下还可以在当前目录中包含C/C++对应的源文件。
@ -70,7 +70,7 @@ func main() {
<!-- 测试代码;需要确实是否有问题 -->
## `#cgo`语句
## 2.2.2 `#cgo`语句
`import "C"`语句前的注释中可以通过`#cgo`语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。
@ -138,7 +138,7 @@ func main() {
这样我们就可以用C语言中常用的技术来处理不同平台之间的差异代码。
## build tag 条件编译
## 2.2.3 build tag 条件编译
build tag 是在Go或cgo环境下的C/C++文件开头的一种特殊的注释。条件编译类似于前面通过`#cgo`指令针对不同平台定义的宏,只有在对应平台的宏被定义之后才会构建对应的代码。但是通过`#cgo`指令定义宏有个限制它只能是基于Go语言支持的windows、darwin和linux等已经支持的操作系统。如果我们希望定义一个DEBUG标志的宏`#cgo`指令就无能为力了。而Go语言提供的build tag 条件编译特性则可以简单做到。

View File

@ -2,7 +2,7 @@
最初CGO是为了达到方便从Go语言函数调用C语言函数以复用C语言资源这一目的而出现的(因为C语言还会涉及回调函数自然也会涉及到从C语言函数调用Go语言函数)。现在它已经演变为C语言和Go语言双向通讯的桥梁。要想利用好CGO特性自然需要了解此二语言类型之间的转换规则这是本节要讨论的问题。
## 数值类型
## 2.3.1 数值类型
在Go语言中访问C语言的符号时一般是通过虚拟的“C”包访问比如`C.int`对应C语言的`int`类型。有些C语言的类型是由多个关键字组成但通过虚拟的“C”包访问C语言类型时名称部分不能有空格字符比如`unsigned int`不能直接通过`C.unsigned int`访问。因此CGO为C语言的基础数值类型都提供了相应转换规则比如`C.uint`对应C语言的`unsigned int`
@ -59,7 +59,7 @@ uint64_t | C.uint64_t | uint64
前文说过如果C语言的类型是由多个关键字组成则无法通过虚拟的“C”包直接访问(比如C语言的`unsigned short`不能直接通过`C.unsigned short`访问)。但是,在`<stdint.h>`中通过使用C语言的`typedef`关键字将`unsigned short`重新定义为`uint16_t`这样一个单词的类型后,我们就可以通过`C.uint16_t`访问原来的`unsigned short`类型了。对于比较复杂的C语言类型推荐使用`typedef`关键字提供一个规则的类型命名这样更利于在CGO中访问。
## Go 字符串和切片
## 2.3.2 Go 字符串和切片
在CGO生成的`_cgo_export.h`头文件中还会为Go语言的字符串、切片、字典、接口和管道等特有的数据类型生成对应的C语言类型
@ -107,7 +107,7 @@ const char *_GoStringPtr(_GoString_ s);
更严谨的做法是为C语言函数接口定义严格的头文件然后基于稳定的头文件实现代码。
## 结构体、联合、枚举类型
## 2.3.3 结构体、联合、枚举类型
C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。在Go语言中我们可以通过`C.struct_xxx`来访问C语言中定义的`struct xxx`结构体类型。结构体的内存布局按照C语言的通用对齐规则在32位Go语言环境C语言结构体也按照32位对齐规则在64位Go语言环境按照64位的对齐规则。对于指定了特殊对齐规则的结构体无法在CGO中访问。
@ -259,7 +259,7 @@ func main() {
在C语言中枚举类型底层对应`int`类型,支持负数类型的值。我们可以通过`C.ONE``C.TWO`等直接访问定义的枚举值。
## 数组、字符串和切片
## 2.3.4 数组、字符串和切片
在C语言中数组名其实对应于一个指针指向特定类型特定长度的一段内存但是这个指针不能被修改当把数组名传递给一个函数时实际上传递的是数组第一个元素的地址。为了讨论方便我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。
@ -356,17 +356,9 @@ typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
在C语言中可以通过`GoString``GoSlice`来访问Go语言的字符串和切片。如果是Go语言中数组类型可以将数组转为切片后再行转换。如果字符串或切片对应的底层内存空间由Go语言的运行时管理那么在C语言中不能长时间保存Go内存对象。
<!--
```go
// 例子
// 字符串和切片
// 作为参数或返回值 和 C 函数通信
```
-->
关于CGO内存模型的细节在稍后章节中会详细讨论。
## 指针间的转换
## 2.3.5 指针间的转换
在C语言中不同类型的指针是可以显式或隐式转换的如果是隐式只是会在编译时给出一些警告信息。但是Go语言对于不同类型的转换非常严格任何C语言中可能出现的警告信息在Go语言中都可能是错误指针是C语言的灵魂指针间的自由转换也是cgo代码中经常要解决的第一个重要的问题。
@ -390,7 +382,7 @@ p = (*X)(unsafe.Pointer(q)) // *Y => *X
任何类型的指针都可以通过强制转换为`unsafe.Pointer`指针类型去掉原有的类型信息,然后再重新赋予新的指针类型而达到指针间的转换的目的。
## 数值和指针的转换
## 2.3.6 数值和指针的转换
不同类型指针间的转换看似复杂但是在cgo中已经算是比较简单的了。在C语言中经常遇到用普通数值表示指针的场景也就是说如何实现数值和指针的转换也是cgo需要面对的一个问题。
@ -402,7 +394,7 @@ p = (*X)(unsafe.Pointer(q)) // *Y => *X
转换分为几个阶段在每个阶段实现一个小目标首先是int32到uintptr类型然后是uintptr到`unsafe.Pointr`指针类型,最后是`unsafe.Pointr`指针类型到`*C.char`类型。
## 切片间的转换
## 2.3.7 切片间的转换
在C语言中数组也一种指针因此两个不同类型数组之间到转换和指针间转换基本类似。但是在Go语言中数组或数组对应到切片都不再是指针类型因为我们也就无法直接实现不同类型到切片之间的转换。

View File

@ -2,7 +2,7 @@
函数是C语言编程的核心通过CGO技术我们不仅仅可以在Go语言中调用C语言函数也可以将Go语言函数导出为C语言函数。
## Go调用C函数
## 2.4.1 Go调用C函数
对于一个启用CGO特性的程序CGO会构造一个虚拟的C包。通过这个虚拟的C包可以调用C语言函数。
@ -21,7 +21,7 @@ func main() {
以上的CGO代码首先定义了一个当前文件内可见的add函数然后通过`C.add`
## C函数的返回值
## 2.4.2 C函数的返回值
对于有返回值的C函数我们可以正常获取返回值。
@ -99,7 +99,7 @@ func C.div(a, b C.int) (C.int, [error])
第二个返回值是可忽略的error接口类型底层对应 `syscall.Errno` 错误类型。
## void函数的返回值
## 2.4.3 void函数的返回值
C语言函数还有一种没有返回值类型的函数用void表示返回值类型。一般情况下我们无法获取void类型函数的返回值因为没有返回值可以获取。前面的例子中提到cgo对errno做了特殊处理可以通过第二个返回值来获取C语言的错误状态。对于void类型函数这个特性依然有效。
@ -160,7 +160,7 @@ func main() {
以上有效特性虽然看似有些无聊但是通过这些例子我们可以精确掌握CGO代码的边界可以从更深层次的设计的角度来思考产生这些奇怪特性的原因。
## C调用Go导出函数
## 2.4.4 C调用Go导出函数
CGO还有一个强大的特性将Go函数导出为C语言函数。这样的话我们可以定义好C语言接口然后通过Go语言实现。在本章的第一节快速入门部分我们已经展示过Go语言导出C语言函数的例子。
@ -189,11 +189,3 @@ void foo() {
当导出C语言接口时需要保证函数的参数和返回值类型都是C语言友好的类型同时返回值不得直接或间接包含Go语言内存空间的指针。
<!--
## C语言函数指针
在C语言中我们可以直接通过函数的名字获取函数的地址
cgo 辅助函数
-->

View File

@ -2,7 +2,7 @@
对于刚刚接触CGO用户来说CGO的很多特性类似魔法。CGO特性主要是通过一个叫cgo的命令行工具来辅助输出Go和C之间的桥接代码。本节我们尝试从生成的代码分析Go语言和C语言函数直接相互调用的流程。
## CGO生成的中间文件
## 2.5.1 CGO生成的中间文件
要了解CGO技术的底层秘密首先需要了解CGO生成了哪些中间文件。我们可以在构建一个cgo包时增加一个`-work`输出中间生成文件所在的目录并且在构建完成时保留中间文件。如果是比较简单的cgo代码我们也可以直接通过手工调用`go tool cgo`命令来查看生成的中间文件。
@ -12,7 +12,7 @@
包中有4个Go文件其中nocgo开头的文件中没有`import "C"`指令其它的2个文件则包含了cgo代码。cgo命令会为每个包含了cgo代码的Go文件创建2个中间文件比如 main.go 会分别创建 main.cgo1.go 和 main.cgo2.c 两个中间文件。然后会为整个包创建一个 `_cgo_gotypes.go` Go文件其中包含Go语言部分辅助代码。此外还会创建一个 `_cgo_export.h``_cgo_export.c` 文件对应Go语言导出到C语言的类型和函数。
## Go调用C函数
## 2.5.2 Go调用C函数
Go调用C函数是CGO最常见的应用场景我们将从最简单的例子入手分析Go调用C函数的详细流程。
@ -136,7 +136,7 @@ void _cgo_506f45f9fa85_Cfunc_sum(void *v) {
其中`runtime.cgocall`函数是实现Go语言到C语言函数跨界调用的关键。更详细的细节可以参考 https://golang.org/src/cmd/cgo/doc.go 内部的代码注释和 `runtime.cgocall` 函数的实现。
## C调用Go函数
## 2.5.3 C调用Go函数
在简单分析了Go调用C函数的流程后我们现在来分析C反向调用Go函数的流程。同样我们现构造一个Go语言版本的sum函数文件名同样为`main.go`

View File

@ -2,7 +2,7 @@
qsort快速排序函数是C语言的高阶函数支持用于自定义排序比较函数可以对任意类型的数组进行排序。本节我们尝试基于C语言的qsort函数封装一个Go语言版本的qsort函数。
## 认识qsort函数
## 2.6.1 认识qsort函数
qsort快速排序函数有`<stdlib.h>`标准库提供,函数的声明如下:
@ -45,7 +45,7 @@ int main() {
其中`DIM(values)`宏用于计算数组元素的个数,`sizeof(values[0])`用于计算数组元素的大小。
cmp是用于排序时比较两个元素大小的回调函数。为了避免对全局名字空间的污染我们将cmp回调函数定义为仅当前文件内可访问的静态函数。
## 将qsort函数从Go包导出
## 2.6.2 将qsort函数从Go包导出
为了方便Go语言的非CGO用户使用qsort函数我们需要将C语言的qsort函数包装为一个外部可以访问的Go函数。
@ -141,7 +141,7 @@ func main() {
消除用户对CGO代码的直接依赖。
## 改进:闭包函数作为比较函数
## 2.6.3 改进:闭包函数作为比较函数
在改进之前我们先回顾下Go语言sort包自带的排序函数的接口
@ -237,7 +237,7 @@ func main() {
现在排序不再需要通过CGO实现C语言版本的比较函数了可以传入Go语言闭包函数作为比较函数。
但是导入的排序函数依然依赖unsafe包这是违背Go语言编程习惯的。
## 改进消除用户对unsafe包的依赖
## 2.6.4 改进消除用户对unsafe包的依赖
前一个版本的qsort.Sort包装函数已经比最初的C语言版本的qsort易用很多但是依然保留了很多C语言底层数据结构的细节。
现在我们将继续改进包装函数尝试消除对unsafe包的依赖并实现一个类似标准库中sort.Slice的排序函数。

View File

@ -2,7 +2,7 @@
CGO是架接Go语言和C语言的桥梁它使二者在二进制接口层面实现了互通但是我们要注意因两种语言的内存模型的差异而可能引起的问题。如果在CGO处理的跨语言函数调用时涉及到了指针的传递则可能会出现Go语言和C语言共享某一段内存的场景。我们知道C语言的内存在分配之后就是稳定的但是Go语言因为函数栈的动态伸缩可能导致栈中内存地址的移动(这是Go和C内存模型的最大差异)。如果C语言持有的是移动之前的Go指针那么以旧指针访问Go对象时会导致程序崩溃。
## Go访问C内存
## 2.7.1 Go访问C内存
C语言空间的内存是稳定的只要不是被人为提前释放那么在Go语言空间可以放心大胆地使用。在Go语言访问C语言内存是最简单的情形我们在之前的例子中已经见过多次。
@ -42,7 +42,7 @@ func main() {
因为C语言内存空间是稳定的基于C语言内存构造的切片也是绝对稳定的不会因为Go语言栈的变化而被移动。
## C临时访问传入的Go内存
## 2.7.2 C临时访问传入的Go内存
cgo之所以存在的一大因素是为了方便在Go语言中接纳吸收过去几十年来使用C/C++语言软件构建的大量的软件资源。C/C++很多库都是需要通过指针直接处理传入的内存数据的因此cgo中也有很多需要将Go内存传入C语言函数的应用场景。
@ -116,7 +116,7 @@ pb := (*int16)(unsafe.Pointer(tmp))
因为tmp并不是指针类型在它获取到Go对象地址之后x对象可能会被移动但是因为不是指针类型所以不会被Go语言运行时更新成新内存的地址。在非指针类型的tmp保持Go对象的地址和在C语言环境保持Go对象的地址的效果是一样的如果原始的Go对象内存发生了移动Go语言运行时并不会同步更新它们。
## C长期持有Go指针对象
## 2.7.3 C长期持有Go指针对象
作为一个Go程序员在使用CGO时潜意识会认为总是Go调用C函数。其实CGO中C语言函数也可以回调Go语言实现的函数。特别是我们可以用Go语言写一个动态库导出C语言规范的接口给其它用户调用。当C语言函数调用Go语言函数的时候C语言函数就成了程序的调用方Go语言函数返回的Go对象内存的生命周期也就自然超出了Go语言运行时的管理。简言之我们不能在C语言函数中直接使用Go语言对象的内存。
@ -226,7 +226,7 @@ func main() {
在printString函数中我们通过NewGoString创建一个对应的Go字符串对象返回的其实是一个id不能直接使用。我们借助PrintGoString函数将id解析为Go语言字符串后打印。该字符串在C语言函数中完全跨越了Go语言的内存管理在PrintGoString调用前即使发生了栈伸缩导致的Go字符串地址发生变化也依然可以正常工作因为该字符串对应的id是稳定的在Go语言空间通过id解码得到的字符串也就是有效的。
## 导出C函数不能返回Go内存
## 2.7.4 导出C函数不能返回Go内存
在Go语言中Go是从一个固定的虚拟地址空间分配内存。而C语言分配的内存则不能使用Go语言保留的虚拟内存空间。在CGO环境Go语言运行时默认会检查导出返回的内存是否是由Go语言分配的如果是则会抛出运行时异常。

View File

@ -2,11 +2,11 @@
CGO是C语言和Go语言之间的桥梁原则上无法直接支持C++的类。CGO不支持C++语法的根本原因是C++至今为止还没有一个二进制接口规范(ABI)。一个C++类的构造函数在编译为目标文件时如何生成链接符号名称、方法在不同平台甚至是C++的不同版本之间都是不一样的。但是C++是兼容C语言所以我们可以通过增加一组C语言函数接口作为C++类和CGO之间的桥梁这样就可以间接地实现C++和Go之间的互联。当然因为CGO只支持C语言中值类型的数据类型所以我们是无法直接使用C++的引用参数等特性的。
## C++ 类到 Go 语言对象
## 2.8.1 C++ 类到 Go 语言对象
实现C++类到Go语言对象的包装需要经过以下几个步骤首先是用纯C函数接口包装该C++类其次是通过CGO将纯C函数接口映射到Go函数最后是做一个Go包装对象将C++类到方法用Go对象的方法实现。
### 准备一个 C++ 类
### 2.8.1.1 准备一个 C++ 类
为了演示简单,我们基于`std::string`做一个最简单的缓存类MyBuffer。除了构造函数和析构函数之外只有两个成员函数分别是返回底层的数据指针和缓存的大小。因为是二进制缓存所以我们可以在里面中放置任意数据。
@ -48,7 +48,7 @@ int main() {
为了方便向C语言接口过渡在此处我们故意没有定义C++的拷贝构造函数。我们必须以new和delete来分配和释放缓存对象而不能以值风格的方式来使用。
### 用纯C函数接口封装 C++ 类
### 2.8.1.2 用纯C函数接口封装 C++ 类
如果要将上面的C++类用C语言函数接口封装我们可以从使用方式入手。我们可以将new和delete映射为C语言函数将对象的方法也映射为C语言函数。
@ -114,7 +114,7 @@ int MyBuffer_Size(MyBuffer_T* p) {
将C++类包装为纯C接口之后下一步的工作就是将C函数转为Go函数。
### 将纯C接口函数转为Go函数
### 2.8.1.3 将纯C接口函数转为Go函数
将纯C函数包装为对应的Go函数的过程比较简单。需要注意的是因为我们的包中包含C++11的语法因此需要通过`#cgo CXXFLAGS: -std=c++11`打开C++11的选项。
@ -154,7 +154,7 @@ func cgo_MyBuffer_Size(p *cgo_MyBuffer_T) C.int {
为了处理简单在包装纯C函数到Go函数时除了cgo_MyBuffer_T类型外对输入参数和返回值的基础类型我们依然是用的C语言的类型。
### 包装为Go对象
### 2.8.1.4 包装为Go对象
在将纯C接口包装为Go函数之后我们就可以很容易地基于包装的Go函数构造出Go对象来。因为cgo_MyBuffer_T是从C语言空间导入的类型它无法定义自己的方法因此我们构造了一个新的MyBuffer类型里面的成员持有cgo_MyBuffer_T指向的C语言缓存对象。
@ -208,11 +208,11 @@ func main() {
例子中我们创建了一个1024字节大小的缓存然后通过copy函数向缓存填充了一个字符串。为了方便C语言字符串函数处理我们在填充字符串的默认用'\0'表示字符串结束。最后我们直接获取缓存的底层数据指针用C语言的puts函数打印缓存的内容。
## Go 语言对象到 C++ 类
## 2.8.2 Go 语言对象到 C++ 类
要实现Go语言对象到C++类的包装需要经过以下几个步骤首先是将Go对象映射为一个id然后基于id导出对应的C接口函数最后是基于C接口函数包装为C++对象。
### 构造一个Go对象
### 2.8.2.1 构造一个Go对象
为了便于演示我们用Go语言构建了一个Person对象每个Person可以有名字和年龄信息
@ -243,7 +243,7 @@ func (p *Person) Get() (name string, age int) {
Person对象如果想要在C/C++中访问需要通过cgo导出C接口来访问。
### 导出C接口
### 2.8.2.2 导出C接口
我们前面仿照C++对象到C接口的过程也抽象一组C接口描述Person对象。创建一个`person_capi.h`文件对应C接口规范文件
@ -315,7 +315,7 @@ func person_get_age(h C.person_handle_t) C.int {
在创建Go对象后我们通过NewObjectId将Go对应映射为id。然后将id强制转义为person_handle_t类型返回。其它的接口函数则是根据person_handle_t所表示的id让根据id解析出对应的Go对象。
### 封装C++对象
### 2.8.2.3 封装C++对象
有了C接口之后封装C++对象就比较简单了。常见的做法是新建一个Person类里面包含一个person_handle_t类型的成员对应真实的Go对象然后在Person类的构造函数中通过C接口创建Go对象在析构函数中通过C接口释放Go对象。下面是采用这种技术的实现
@ -367,7 +367,7 @@ int main() {
}
```
### 封装C++对象改进
### 2.8.2.4 封装C++对象改进
在前面的封装C++对象的实现中每次通过new创建一个Person实例需要进行两次内存分配一次是针对C++版本的Person再一次是针对Go语言版本的Person。其实C++版本的Person内部只有一个person_handle_t类型的id用于映射Go对象。我们完全可以将person_handle_t直接当中C++对象来使用。
@ -402,7 +402,7 @@ struct Person {
到此我们就达到了将Go对象导出为C接口然后基于C接口再包装为C++对象以便于使用的目的。
## 彻底解放C++的this指针
## 2.8.3 彻底解放C++的this指针
熟悉Go语言的用法会发现Go语言中方法是绑定到类型的。比如我们基于int定义一个新的Int类型就可以有自己的方法

View File

@ -2,7 +2,7 @@
CGO在使用C/C++资源的时候一般有三种形式:直接使用源码;链接静态库;链接动态库。直接使用源码就是在`import "C"`之前的注释部分包含C代码或者在当前包中包含C/C++源文件。链接静态库和动态库的方式比较类似都是通过在LDFLAGS选项指定要链接的库方式链接。本节我们主要关注在CGO中如何使用静态库和动态库相关的问题。
## 使用C静态库
## 2.9.1 使用C静态库
如果CGO中引入的C/C++资源有代码而且代码规模也比较小直接使用源码是最理想的方式但很多时候我们并没有源代码或者从C/C++源代码开始构建的过程异常复杂这种时候使用C静态库也是一个不错的选择。静态库因为是静态链接最终的目标程序并不会产生额外的运行时依赖也不会出现动态库特有的跨运行时资源管理的错误。不过静态库对链接阶段会有一定要求静态库一般包含了全部的代码里面会有大量的符号如果不同静态库之间出现了符号冲突则会导致链接的失败。
@ -73,7 +73,7 @@ func main() {
在Linux环境有一个pkg-config命令可以查询要使用某个静态库或动态库时的编译和链接参数。我们可以在#cgo命令中直接使用pkg-config命令来生成编译和链接参数。而且还可以通过PKG_CONFIG环境变量订制pkg-config命令。因为不同的操作系统对pkg-config命令的支持不尽相同通过该方式很难兼容不同的操作系统下的构建参数。不过对于Linux等特定的系统pkg-config命令确实可以简化构建参数的管理。关于pkg-config的使用细节在此我们不深入展开大家可以自行参考相关文档。
## 使用C动态库
## 2.9.2 使用C动态库
动态库出现的初衷是对于相同的库多个进程可以共享同一个以节省内存和磁盘资源。但是在磁盘和内存已经白菜价的今天这两个作用已经显得微不足道了那么除此之外动态库还有哪些存在的价值呢从库开发角度来说动态库可以隔离不同动态库之间的关系减少链接时出现符号冲突的风险。而且对于windows等平台动态库是跨越VC和GCC不同编译器平台的唯一的可行方式。
@ -137,7 +137,7 @@ $ dlltool -dllname number.dll --def number.def --output-lib libnumber.a
需要注意的是在运行时需要将动态库放到系统能够找到的位置。对于windows来说可以将动态库和可执行程序放到同一个目录或者将动态库所在的目录绝对路径添加到PATH环境变量中。对于macOS来说需要设置DYLD_LIBRARY_PATH环境变量。而对于Linux系统来说需要设置LD_LIBRARY_PATH环境变量。
## 导出C静态库
## 2.9.3 导出C静态库
CGO不仅可以使用C静态库也可以将Go实现的函数导出为C静态库。我们现在用Go实现前面的number库的模加法函数。
@ -208,7 +208,7 @@ $ ./a.out
使用CGO创建静态库的过程非常简单。
## 导出C动态库
## 2.9.4 导出C动态库
CGO导出动态库的过程和静态库类似只是将构建模式改为`c-shared`,输出文件名改为`number.so`而已:
@ -223,7 +223,7 @@ $ gcc -o a.out _test_main.c number.so
$ ./a.out
```
## 导出非main包的函数
## 2.9.5 导出非main包的函数
通过`go help buildmode`命令可以查看C静态库和C动态库的构建说明

View File

@ -3,19 +3,19 @@
编译和链接参数是每一个C/C++程序员需要经常面对的问题。构建每一个C/C++应用均需要经过编译和链接两个步骤CGO也是如此。
本节我们将简要讨论CGO中经常用到的编译和链接参数的用法。
## 编译参数CFLAGS/CPPFLAGS/CXXFLAGS
## 2.10.1 编译参数CFLAGS/CPPFLAGS/CXXFLAGS
编译参数主要是头文件的检索路径预定义的宏等参数。理论上来说C和C++是完全独立的两个编程语言,它们可以有着自己独立的编译参数。
但是因为C++语言对C语言做了深度兼容甚至可以将C++理解为C语言的超集因此C和C++语言之间又会共享很多编译参数。
因此CGO提供了CFLAGS/CPPFLAGS/CXXFLAGS三种参数其中CFLAGS对应C语言编译参数(以`.c`后缀名)、
CPPFLAGS对应C/C++ 代码编译参数(*.c,*.cc,*.cpp,*.cxx)、CXXFLAGS对应纯C++编译参数(*.cc,*.cpp,*.cxx)。
## 链接参数LDFLAGS
## 2.10.2 链接参数LDFLAGS
链接参数主要包含要链接库的检索目录和要链接库的名字。因为历史遗留问题,链接库不支持相对路径,我们必须为链接库指定绝对路径。
cgo 中的 ${SRCDIR} 为当前目录的绝对路径。经过编译后的C和C++目标文件格式是一样的因此LDFLAGS对应C/C++共同的链接参数。
## pkg-config
## 2.10.3 pkg-config
为不同C/C++库提供编译和链接参数是一项非常繁琐的工作因此cgo提供了对应`pkg-config`工具的支持。
我们可以通过`#cgo pkg-config xxx`命令来生成xxx库需要的编译和链接参数其底层通过调用
@ -71,7 +71,7 @@ $ PKG_CONFIG=./py3-config go build -buildmode=c-shared -o gopkg.so main.go
具体的细节可以参考Go实现Python模块章节。
## go get 链
## 2.10.4 go get 链
在使用`go get`获取Go语言包的同时会获取包依赖的包。比如A包依赖B包B包依赖C包C包依赖D包
`pkgA -> pkgB -> pkgC -> pkgD -> ...`。再go get获取A包之后会依次线获取BCD包。
@ -103,7 +103,7 @@ $ PKG_CONFIG=./py3-config go build -buildmode=c-shared -o gopkg.so main.go
因此在编译`z_libwebp_src_dec_alpha.c`文件时会编译libweb原生的代码。
其中的依赖是相对目录,对于不同的平台支持可以保持最大的一致性。
## 多个非main包中导出C函数
## 2.10.5 多个非main包中导出C函数
官方文档说明导出的Go函数要放main包但是真实情况是其它包的Go导出函数也是有效的。
因为导出后的Go函数就可以当作C函数使用所以必须有效。但是不同包导出的Go函数将在同一个全局的名字空间因此需要小心避免重名的问题。