From f97219b07bc80978e0e5e245dd4d0b9580a6fd11 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Sun, 5 Aug 2018 08:12:02 +0800 Subject: [PATCH] =?UTF-8?q?ch2:=20=E8=A7=84=E8=8C=83=E5=8C=96=E5=86=85?= =?UTF-8?q?=E9=83=A8=E6=AE=B5=E8=90=BD=E7=9A=84=E7=BC=96=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ch2-cgo/ch2-01-hello-cgo.md | 14 +++++++------- ch2-cgo/ch2-02-basic.md | 6 +++--- ch2-cgo/ch2-03-cgo-types.md | 22 +++++++--------------- ch2-cgo/ch2-04-func.md | 16 ++++------------ ch2-cgo/ch2-05-internal.md | 6 +++--- ch2-cgo/ch2-06-qsort.md | 8 ++++---- ch2-cgo/ch2-07-memory.md | 8 ++++---- ch2-cgo/ch2-08-class.md | 22 +++++++++++----------- ch2-cgo/ch2-09-static-shared-lib.md | 10 +++++----- ch2-cgo/ch2-10-link.md | 10 +++++----- 10 files changed, 53 insertions(+), 69 deletions(-) diff --git a/ch2-cgo/ch2-01-hello-cgo.md b/ch2-cgo/ch2-01-hello-cgo.md index b1589c5..d6e5ac1 100644 --- a/ch2-cgo/ch2-01-hello-cgo.md +++ b/ch2-cgo/ch2-01-hello-cgo.md @@ -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只执行?* diff --git a/ch2-cgo/ch2-02-basic.md b/ch2-cgo/ch2-02-basic.md index 0484975..5a22007 100644 --- a/ch2-cgo/ch2-02-basic.md +++ b/ch2-cgo/ch2-02-basic.md @@ -2,7 +2,7 @@ 要使用CGO特性,需要安装C/C++构建工具链,在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 条件编译特性则可以简单做到。 diff --git a/ch2-cgo/ch2-03-cgo-types.md b/ch2-cgo/ch2-03-cgo-types.md index 795ee38..2aab3c7 100644 --- a/ch2-cgo/ch2-03-cgo-types.md +++ b/ch2-cgo/ch2-03-cgo-types.md @@ -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`访问)。但是,在``中通过使用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内存对象。 - - 关于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语言中,数组或数组对应到切片都不再是指针类型,因为我们也就无法直接实现不同类型到切片之间的转换。 diff --git a/ch2-cgo/ch2-04-func.md b/ch2-cgo/ch2-04-func.md index a3298ab..a941045 100644 --- a/ch2-cgo/ch2-04-func.md +++ b/ch2-cgo/ch2-04-func.md @@ -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语言内存空间的指针。 - - diff --git a/ch2-cgo/ch2-05-internal.md b/ch2-cgo/ch2-05-internal.md index 1d81697..c0ffaf4 100644 --- a/ch2-cgo/ch2-05-internal.md +++ b/ch2-cgo/ch2-05-internal.md @@ -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`: diff --git a/ch2-cgo/ch2-06-qsort.md b/ch2-cgo/ch2-06-qsort.md index 6d0caca..e64d319 100644 --- a/ch2-cgo/ch2-06-qsort.md +++ b/ch2-cgo/ch2-06-qsort.md @@ -2,7 +2,7 @@ qsort快速排序函数是C语言的高阶函数,支持用于自定义排序比较函数,可以对任意类型的数组进行排序。本节我们尝试基于C语言的qsort函数封装一个Go语言版本的qsort函数。 -## 认识qsort函数 +## 2.6.1 认识qsort函数 qsort快速排序函数有``标准库提供,函数的声明如下: @@ -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的排序函数。 diff --git a/ch2-cgo/ch2-07-memory.md b/ch2-cgo/ch2-07-memory.md index c847fd5..2fcca31 100644 --- a/ch2-cgo/ch2-07-memory.md +++ b/ch2-cgo/ch2-07-memory.md @@ -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语言分配的,如果是则会抛出运行时异常。 diff --git a/ch2-cgo/ch2-08-class.md b/ch2-cgo/ch2-08-class.md index 5700fa4..3c3b7d4 100644 --- a/ch2-cgo/ch2-08-class.md +++ b/ch2-cgo/ch2-08-class.md @@ -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类型,就可以有自己的方法: diff --git a/ch2-cgo/ch2-09-static-shared-lib.md b/ch2-cgo/ch2-09-static-shared-lib.md index a6c16f1..657a575 100644 --- a/ch2-cgo/ch2-09-static-shared-lib.md +++ b/ch2-cgo/ch2-09-static-shared-lib.md @@ -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动态库的构建说明: diff --git a/ch2-cgo/ch2-10-link.md b/ch2-cgo/ch2-10-link.md index e7fa1fc..997deaa 100644 --- a/ch2-cgo/ch2-10-link.md +++ b/ch2-cgo/ch2-10-link.md @@ -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函数将在同一个全局的名字空间,因此需要小心避免重名的问题。