Skip to content

Latest commit

 

History

History
204 lines (165 loc) · 6.36 KB

index.md

File metadata and controls

204 lines (165 loc) · 6.36 KB

用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

调用入口 _cgo_export.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 函数执行

Go运行时 runtime/cgo.crosscall2

在 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)
}

代理函数 _cgo_types.go

代理函数 _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的运行环境。

C语言实际调用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

参考