用go语言实现一个sum函数,给C语言调用。
package main
//int sum(int a, int b);
import "C"
//export sum
func sum(a, b C.int) C.int {
return a + b
}
func main() {
}
将 Go 代码编译为一个 C 静态库,生成一个 sum.a 静态库和 sum.h 头文件。其中 sum.h 头文件将包含 sum 函数的声明,静态库中将包含 sum 函数的实现。
# go build -buildmode=c-archive -o sum.a main.go
分析 cgo 生成的中间文件
# go tool cgo main.go
生成的文件包括
- _cgo_export.c:包含C语言版本的 sum 函数的实现
- _cgo_export.h:文件的内容和生成 C 静态库时产生的 sum.h 头文件是同一个文件
- _cgo_gotypes.go
- main.cgo1.go
- main.cgo2.c
C语言版本的SUM函数位于_cgo_export.c中。
int sum(int a, int b)
{
__SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
typedef struct {
int p0;
int p1;
int r0;
} __attribute__((__packed__)) _cgo_argtype;
static _cgo_argtype _cgo_zero;
_cgo_argtype _cgo_a = _cgo_zero;
_cgo_a.p0 = a;
_cgo_a.p1 = b;
_cgo_tsan_release();
crosscall2(_cgoexp_e81351f2a93e_sum, &_cgo_a, 12, _cgo_ctxt);
_cgo_tsan_acquire();
_cgo_release_context(_cgo_ctxt);
return _cgo_a.r0;
}
sum函数负责工作如下
- 将 sum 函数的参数和返回值打包到一个结构体中
- 通过 runtime/cgo.crosscall2 函数将结构体传给 _cgoexp_8313eaf44386_sum 函数执行
在 crosscall2 的参数中,fn 是中间代理函数的指针,a 是对应调用参数和返回值的结构体指针
//file:runtime/cgo/asm_amd64.s
TEXT crosscall2(SB),NOSPLIT,$0-0
PUSH_REGS_HOST_TO_ABI0()
// Make room for arguments to cgocallback.
ADJSP $0x18
#ifndef GOOS_windows
MOVQ DI, 0x0(SP) /* fn */
MOVQ SI, 0x8(SP) /* arg */
// Skip n in DX.
MOVQ CX, 0x10(SP) /* ctxt */
#else
MOVQ CX, 0x0(SP) /* fn */
MOVQ DX, 0x8(SP) /* arg */
// Skip n in R8.
MOVQ R9, 0x10(SP) /* ctxt */
#endif
CALL runtime·cgocallback(SB)
ADJSP $-0x18
POP_REGS_HOST_TO_ABI0()
RET
在 crosscall2 中,调用 runtime·cgocallback。 runtime·cgocallback也是一个用汇编实现的函数。
//file:runtime/asm_amd64.s
TEXT ·cgocallback(SB),NOSPLIT,$24-24
......
MOVQ $runtime·cgocallbackg(SB), AX
.....
cgocallback
- 从m->g0的栈切换goroutine的栈,
- 并在这个栈中调用runtime.cgocallbackg(p.GoF, frame, framesize)
- 在runtime.cgocallback重获控制权之后,它切换回m->g0栈,从栈中恢复之前的m->g0.sched.sp值,
- 最后返回到crosscall2
重点看runtime.cgocallbackg。 runtime.cgocallbackg现在是运行在一个真实的goroutine栈中(不是m->g0栈)。现在我们只是切换到了goroutine栈,此刻还是处于syscall状态的。 因此这个函数会先调用runtime.exitsyscall,接着才是执行Go代码。当它调用runtime.exitsyscall,这会阻塞这条goroutine直到满足$GOMAXPROCS限制条件。
//file:runtime/cgocall.go
// Call from C back to Go. fn must point to an ABIInternal Go entry-point.
func cgocallbackg(fn, frame unsafe.Pointer, ctxt uintptr) {
gp := getg()
// entersyscall saves the caller's SP to allow the GC to trace the Go
// stack. However, since we're returning to an earlier stack frame and
// need to pair with the entersyscall() call made by cgocall, we must
// save syscall* and let reentersyscall restore them.
savedsp := unsafe.Pointer(gp.syscallsp)
savedpc := gp.syscallpc
exitsyscall() // coming out of cgo call
//调用用户函数
cgocallbackg1(fn, frame, ctxt) // will call unlockOSThread
// going back to cgo call
reentersyscall(savedpc, uintptr(savedsp))
gp.m.syscall = syscall
gp.m.syscallsp = sp
}
cgocallbackg1调用 reflectcall,正式进入到用户定义的 Go 函数。
//file:runtime/cgocall.go
func cgocallbackg1(fn, frame unsafe.Pointer, ctxt uintptr) {
......
// Invoke callback. This function is generated by cmd/cgo and
// will unpack the argument frame and call the Go function.
var cb func(frame unsafe.Pointer)
cbFV := funcval{uintptr(fn)}
*(*unsafe.Pointer)(unsafe.Pointer(&cb)) = noescape(unsafe.Pointer(&cbFV))
cb(frame)
}
代理函数 _cgoexp_e81351f2a93e_sum 位于 _cgo_gotypes.go 文件中。
func _cgoexp_e81351f2a93e_sum(a *struct {
p0 _Ctype_int
p1 _Ctype_int
r0 _Ctype_int
}) {
a.r0 = sum(a.p0, a.p1)
}
通过代理函数调用到 main.go 函数中定义的 sum 函数。
无论是Go调用C,还是C调用Go,其需要解决的核心问题其实都是提供一个C/Go的运行环境来执行相应的代码。 Go的代码执行环境就是goroutine以及Go的runtime,而C的执行环境需要一个不使用分段的栈,并且执行C代码的goroutine需要暂时地脱离调度器的管理。 要达到这些要求,运行时提供的支持就是切换栈,以及runtime.entersyscall。
在Go中调用C函数时,runtime.cgocall中调用entersyscall脱离调度器管理。runtime.asmcgocall切换到m的g0栈,于是得到C的运行环境。 在C中调用Go函数时,crosscall2解决gcc编译到6c编译之间的调用协议问题。cgocallback切换回goroutine栈。 runtime.cgocallbackg中调用exitsyscall恢复Go的运行环境。
Linux下使用静态库,只需要在编译的时候,指定静态库的搜索路径(-L选项)、指定静态库名(不需要lib前缀和.a后缀,-l选项)。
#gcc main.c -L../StaticLibrary -lstaticdemo
-L:表示要连接的库所在目录 -l:指定链接时需要的动态库,编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a或.so来确定库的名称。
将go函数编译为静态库
#go build -buildmode=c-archive -o libsum.a main.go
将go函数编译为动态库(测试通过)
#go build -o libsum.dylib -buildmode=c-shared main.go
c加载动态链接库
#gcc main.c -L. -lsum