C调用Go
2021.07

9.4 C调用Go

cgo不仅仅支持从Go调用C,它还同样支持从C中调用Go的函数,虽然这种情况相对前者较少使用。

//export GoF
func GoF(arg1, arg2 int, arg3 string) int64 {
}

使用export标记可以将Go函数导出提供给C调用:

extern int64 GoF(int arg1, int arg2, GoString arg3);

下面让我们看看它是如何实现的。假定上面的函数GoF是在Go语言的一个包p内的,为了能够让gcc编译的C代码调用Go的函数p.GoF,cgo生成下面一个函数:

GoInt64 GoF(GoInt p0, GoInt p1, GoString p2)
{
    struct {
        GoInt p0;
        GoInt p1;
        GoString p2;
        GoInt64 r0;
    } __attribute__((packed)) a;
    a.p0 = p0;
    a.p1 = p1;
    a.p2 = p2;
    crosscall2(_cgoexp_95935062f5b1_GoF, &a, 40);
    return a.r0;
}

这个函数由cgo生成,提供给gcc编译。函数名不是p.GoF,因为gcc没有包的概念。由gcc编译的C函数可以调用这个GoF函数。

GoF调用crosscall2(cgoexpGoF, frame, framesize)。crosscall2是用汇编代码实现的,它是一个两参数的适配器,作用是从gcc函数调用6c函数(6c和gcc使用的调用协议还是有些区别的)。crosscall2实现了从一个ABI的gcc函数调用,到6c的函数调用ABI。所以上面代码中实际上相当于调用cgoexpGoF(frame,framesize)。注意此时是仍然运行在mg的g0栈并且不受GOMAXPROCS限制的。因此,这个代码不能直接调用任意的Go代码并且不能分配内存或者用尽m->g0的栈。

cgoexpGoF调用runtime.cgocallback(p.GoF, frame, framesize):

#pragma textflag 7
void
_cgoexp_95935062f5b1_GoF(void *a, int32 n)
{
    runtime·cgocallback(·GoF, a, n);
}

这个函数是由6c编译的,而不是gcc,因此可以引用到比如runtime.cgocallback和p.GoF这种名字。

runtime·cgocallback也是一个用汇编实现的函数。它从m->g0的栈切换回原来的goroutine的栈,并在这个栈中调用runtime.cgocallbackg(p.GoF, frame, framesize)。

这中间会涉及到一些保存栈寄存器之类的细节操作比较复杂。因为这个过程相当于我们接管了m->curg的执行,但是却并没有完全恢复到之前的运行环境(只是借m->curg这个goroutine运行Go代码),所以我们需要保存当前环境到以便之后再次返回到m->g0栈。

好了,runtime.cgocallbackg现在是运行在一个真实的goroutine栈中(不是m->g0栈)。不过现在我们只是切换到了goroutine栈,此刻还是处于syscall状态的。因此这个函数会先调用runtime.exitsyscall,接着才是执行Go代码。当它调用runtime.exitsyscall,这会阻塞这条goroutine直到满足$GOMAXPROCS限制条件。一旦从exitsyscall返回,则可以安全地执行像调用内存分配或者是调用Go的回调函数p.GoF。

void
runtime·cgocallbackg(FuncVal *fn, void *arg, uintptr argsize)
{
    runtime·exitsyscall();    // coming out of cgo call
    // Invoke callback.
    reflect·call(fn, arg, argsize);
    runtime·entersyscall();    // going back to cgo call
}

后面的过程就不用分析了,跟前面的过程是一个正好相反的过程。在runtime.cgocallback重获控制权之后,它切换回m->g0栈,从栈中恢复之前的m->g0.sched.sp值,然后返回到cgoexpGoF。cgoexpGoF立即返回到crosscall2,它会恢复被调者为gcc保存的寄存器并返回到GoF,最后返回到C的调用函数中。

小结

无论是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的运行环境。