go和其它语言互调要考虑如下几个核心问题
- 线程与栈:
- Go自己的函数使用的是goroutine,并使用特殊的栈(运行时可能会扩张)
- C语言使用的是线程,并使用普通线程栈来运行,幸运的是m中的g0使用的就是这个栈。
- 系统调用处理
- Go运行时为了保证始终是GOMAXPROCS个P在运行,
- 会在系统调用之前先调用runtime.entersyscall,会将P的M剥离并将它设置为PSyscall。告知系统此时其它的P有机会运行。
- 会在系统调用之后再runtime.exitsyscall,会查看当前仍然有可用的P,则让它继续运行,否则这个goroutine就要被挂起了
- 为了不让cgo代码影响Go的调度,Go运行时将C函数像处理系统调用一样隔离开来,要使用entersyscall、exitsyscall。
- Go运行时为了保证始终是GOMAXPROCS个P在运行,
- 垃圾回收
- Go语言中存在GC
- C语言中没有GC,需要依赖开发手动释放
- 函数调用约定:调用约定规定了参数是使用寄存器,还是栈,入栈的话是从右往左,还是从左往右,堆栈是由被调用者清理,还是调用者清理
- 内存模型
Go语言的实现中,包括如下两个关键点
- CGO桩代码生成
- 负责C类型和Go类型之间的转换
- 命名空间处理以及特殊的调用方式处理
- 运行时支持
- 负责处理好C的运行环境,类似于给C代码一个非分段的栈空间并让它脱离与调度系统的交互
- go调用c的核心函数是runtime.cgocall
- c调用go的核心函数是runtime.crosscall2
C 语言 x86架构 常用的三种调用约定:
- cdecl,入参从右往左依次入栈,由调用者清理堆栈
- stdcall,入参从右往左依次入栈,由被调自己清理堆栈
- fastcall,使用 ecx、edx 传递前两个参数,剩下的参数从右向左依次入栈,且由被调自己清理堆栈
C语言x86_64架构中
- 函数前 6 个参数通过寄存器 rdi、rsi、rdx、rcx、r8、r9 传递,超出的参数从右向左依次入栈
- 调用方清理栈
Go1.16调用约定
- 栈底到栈顶先储存返回参数,然后储存输入参数,
- 压栈顺序按参数顺序从右到左。
- main 函数分配的栈内存由 main 函数自己销毁
Go1.17之后
- 使用 AX,BX,CX,DI,SI,R8,R9,R10,R11 传递前 9 个参数,剩余 2 个参数按从右到左的顺序依次压栈
- 主调负责释放参数占用的栈空间
Go中使用的C编译器其实是plan9的C编译器,和gcc等会有一些区别。
例如Golang采用垃圾回收机制,C语言采用手动释放内存机制。
如果在 CGO 处理的跨语言函数调用时涉及到了指针的传递,则可能会出现 Go 语言和 C 语言共享某一段内存的场景
- 在C语言中,只要没有显示释放,内存默认是一直可用的
- 但在go语言中,可能会出现因为函数栈的动态伸缩而导致内存地址变化
在Go语言访问C中内存的时候,一般不存在问题。但在C访问Go内存的时候,可能会出现因为Go内存的动态伸缩而导致访问不安全。
对于C临时访问传入的Go内存 1、避免指针传递,全部使用值传递。不过会带来一些额外的性能开销。 2、CGO 规定在调用的 C 语言函数返回前,cgo 保证传入的 Go 语言内存在此期间不会发生移动。 但需要开发者注意:
- 保证在取得 Go 内存后需要马上传入 C 语言函数。因为在调用CGO之前还是可能变化的
- 在需要长时间运行的 C 语言函数需要谨慎处理参数,因为运行期间协程栈无法扩缩
如果C需要长期访问某个对象,可以以将 Go 语言内存对象在 Go 语言空间映射为一个 int 类型的 id,然后通过此 id 来间接访问和控制 Go 语言对象。 这样对象发生移动后,仍然可以通过id找到对应的值,而不会出现非法访问。
Go 语言的 new 函数分配,是由 Go 语言运行时统一管理的内存。所以默认 cgocheck 为1,检查go导出的C函数中不能返回 Go 内存。 如果需要可以将该选项调整成2或者0。
- 直接使用C语言的源代码
- 使用C语言编译出来的静态链接库
- 使用C语言编译出来的动态链接库