Go语言高级编程

324课时
1.4K学过
8分

2. 第2章 CGO编程

引言

2.1 快速入门

2.1.1 最简CGO程序

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

2.1.3 使用自己的C函数

2.1.4 C代码的模块化

2.1.5 用Go重新实现C函数

2.1.6 面向C接口的Go编程

2.2 CGO基础

2.2.1 import "C"语句

2.2.2 #cgo语句

2.2.3 build tag 条件编译

2.3 类型转换

2.3.1 数值类型

2.3.2 Go 字符串和切片

2.3.3 结构体、联合、枚举类型

2.3.4 数组、字符串和切片

2.3.5 指针间的转换

2.3.6 数值和指针的转换

2.3.7 切片间的转换

2.4 函数调用

2.4.1 Go调用C函数

2.4.2 C函数的返回值

2.4.3 void函数的返回值

2.4.4 C调用Go导出函数

2.5 内部机制

2.5.1 CGO生成的中间文件

2.5.2 Go调用C函数

2.5.3 C调用Go函数

2.6 实战: 封装qsort

2.6.1 认识qsort函数

2.6.2 将qsort函数从Go包导出

2.6.3 改进:闭包函数作为比较函数

2.6.4 改进:消除用户对unsafe包的依赖

2.7 CGO内存模型

2.7.1 Go访问C内存

2.7.2 C临时访问传入的Go内存

2.7.3 C长期持有Go指针对象

2.7.4 导出C函数不能返回Go内存

2.8 C++ 类包装

2.8.1 C++ 类到 Go 语言对象

2.8.1.1 准备一个 C++ 类

2.8.1.2 用纯C函数接口封装 C++ 类

2.8.1.3 将纯C接口函数转为Go函数

2.8.1.4 包装为Go对象

2.8.2 Go 语言对象到 C++ 类

2.8.2.1 构造一个Go对象

2.8.2.2 导出C接口

2.8.2.3 封装C++对象

2.8.2.4 封装C++对象改进

2.8.3 彻底解放C++的this指针

2.9 静态库和动态库

2.9.1 使用C静态库

2.9.2 使用C动态库

2.9.3 导出C静态库

2.9.4 导出C动态库

2.9.5 导出非main包的函数

2.10 编译和链接参数

2.10.1 编译参数:CFLAGS/CPPFLAGS/CXXFLAGS

2.10.2 链接参数:LDFLAGS

2.10.3 pkg-config

2.10.4 go get 链

2.10.5 多个非main包中导出C函数

课程评价 (0)

请对课程作出评价:
0/300

学员评价

暂无精选评价
20分钟

3.8.4 获取g结构体对应的接口对象

枚举和暴力穷举虽然够直接,但是对于正在开发中的未发布的Go版本支持并不好,我们无法提前知晓开发中的某个版本的goid成员的偏移量。

如果是在runtime包内部,我们可以通过unsafe.OffsetOf(g.goid)直接获取成员的偏移量。也可以通过反射获取g结构体的类型,然后通过类型查询某个成员的偏移量。因为g结构体是一个内部类型,Go代码无法从外部包获取g结构体的类型信息。但是在Go汇编语言中,我们是可以看到全部的符号的,因此理论上我们也可以获取g结构体的类型信息。

在任意的类型被定义之后,Go语言都会为该类型生成对应的类型信息。比如g结构体会生成一个type·runtime·g标识符表示g结构体的值类型信息,同时还有一个type·*runtime·g标识符表示指针类型的信息。如果g结构体带有方法,那么同时还会生成go.itab.runtime.ggo.itab.*runtime.g类型信息,用于表示带方法的类型信息。

如果我们能够拿到表示g结构体类型的type·runtime·g和g指针,那么就可以构造g对象的接口。下面是改进的getg函数,返回g指针对象的接口:

// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
	// get runtime.g
	MOVQ (TLS), AX
	// get runtime.g type
	MOVQ $type·runtime·g(SB), BX

	// convert (*g) to interface{}
	MOVQ AX, 8(SP)
	MOVQ BX, 0(SP)
	CALL runtime·convT2E(SB)
	MOVQ 16(SP), AX
	MOVQ 24(SP), BX

	// return interface{}
	MOVQ AX, ret+0(FP)
	MOVQ BX, ret+8(FP)
	RET

其中AX寄存器对应g指针,BX寄存器对应g结构体的类型。然后通过runtime·convT2E函数将类型转为接口。因为我们使用的不是g结构体指针类型,因此返回的接口表示的g结构体值类型。理论上我们也可以构造g指针类型的接口,但是因为Go汇编语言的限制,我们无法使用type·*runtime·g标识符。

基于g返回的接口,就可以容易获取goid了:

func GetGoid() int64 {
	g := getg()
	gid := reflect.ValueOf(g).FieldByName("goid").Int()
	return goid
}

上述代码通过反射直接获取goid,理论上只要反射的接口和goid成员的名字不发生变化,代码都可以正常运行。经过实际测试,以上的代码可以在Go1.8、Go1.9和Go1.10版本中正确运行。乐观推测,如果g结构体类型的名字不发生变化,Go语言反射的机制也不发生变化,那么未来Go语言版本应该也是可以运行的。

反射虽然具备一定的灵活性,但是反射的性能一直是被大家诟病的地方。一个改进的思路是通过反射获取goid的偏移量,然后通过g指针和偏移量获取goid,这样反射只需要在初始化阶段执行一次。

下面是g_goid_offset变量的初始化代码:

var g_goid_offset uintptr = func() uintptr {
	g := GetGroutine()
	if f, ok := reflect.TypeOf(g).FieldByName("goid"); ok {
		return f.Offset
	}
	panic("can not find g.goid field")
}()

有了正确的goid偏移量之后,采用前面讲过的方式获取goid:

func GetGroutineId() int64 {
	g := getg()
	p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
	return *p
}

至此我们获取goid的实现思路已经足够完善了,不过汇编的代码依然有严重的安全隐患。

虽然getg函数是用NOSPLIT标志声明的禁止栈分裂的函数类型,但是getg内部又调用了更为复杂的runtime·convT2E函数。runtime·convT2E函数如果遇到栈空间不足,可能触发栈分裂的操作。而栈分裂时,GC将要挪动栈上所有函数的参数和返回值和局部变量中的栈指针。但是我们的getg函数并没有提供局部变量的指针信息。

下面是改进后的getg函数的完整实现:

// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
	NO_LOCAL_POINTERS

	MOVQ $0, ret_type+0(FP)
	MOVQ $0, ret_data+8(FP)
	GO_RESULTS_INITIALIZED

	// get runtime.g
	MOVQ (TLS), AX

	// get runtime.g type
	MOVQ $type·runtime·g(SB), BX

	// convert (*g) to interface{}
	MOVQ AX, 8(SP)
	MOVQ BX, 0(SP)
	CALL runtime·convT2E(SB)
	MOVQ 16(SP), AX
	MOVQ 24(SP), BX

	// return interface{}
	MOVQ AX, ret_type+0(FP)
	MOVQ BX, ret_data+8(FP)
	RET

其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化,初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂时,GC能够正确处理返回值和局部变量中的指针。