cgo不僅僅支持從Go調(diào)用C,它還同樣支持從C中調(diào)用Go的函數(shù),雖然這種情況相對前者較少使用。
//export GoF
func GoF(arg1, arg2 int, arg3 string) int64 {
}
使用export標(biāo)記可以將Go函數(shù)導(dǎo)出提供給C調(diào)用:
extern int64 GoF(int arg1, int arg2, GoString arg3);
下面讓我們看看它是如何實(shí)現(xiàn)的。假定上面的函數(shù)GoF是在Go語言的一個包p內(nèi)的,為了能夠讓gcc編譯的C代碼調(diào)用Go的函數(shù)p.GoF,cgo生成下面一個函數(shù):
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;
}
這個函數(shù)由cgo生成,提供給gcc編譯。函數(shù)名不是p.GoF,因?yàn)間cc沒有包的概念。由gcc編譯的C函數(shù)可以調(diào)用這個GoF函數(shù)。
GoF調(diào)用crosscall2(_cgoexp_GoF, frame, framesize)。crosscall2是用匯編代碼實(shí)現(xiàn)的,它是一個兩參數(shù)的適配器,作用是從gcc函數(shù)調(diào)用6c函數(shù)(6c和gcc使用的調(diào)用協(xié)議還是有些區(qū)別的)。crosscall2實(shí)現(xiàn)了從一個ABI的gcc函數(shù)調(diào)用,到6c的函數(shù)調(diào)用ABI。所以上面代碼中實(shí)際上相當(dāng)于調(diào)用_cgoexp_GoF(frame,framesize)。注意此時是仍然運(yùn)行在mg的g0棧并且不受GOMAXPROCS限制的。因此,這個代碼不能直接調(diào)用任意的Go代碼并且不能分配內(nèi)存或者用盡m->g0的棧。
_cgoexp_GoF調(diào)用runtime.cgocallback(p.GoF, frame, framesize):
#pragma textflag 7
void
_cgoexp_95935062f5b1_GoF(void *a, int32 n)
{
runtime·cgocallback(·GoF, a, n);
}
這個函數(shù)是由6c編譯的,而不是gcc,因此可以引用到比如runtime.cgocallback和p.GoF這種名字。
runtime·cgocallback也是一個用匯編實(shí)現(xiàn)的函數(shù)。它從m->g0的棧切換回原來的goroutine的棧,并在這個棧中調(diào)用runtime.cgocallbackg(p.GoF, frame, framesize)。
這中間會涉及到一些保存棧寄存器之類的細(xì)節(jié)操作比較復(fù)雜。因?yàn)檫@個過程相當(dāng)于我們接管了m->curg的執(zhí)行,但是卻并沒有完全恢復(fù)到之前的運(yùn)行環(huán)境(只是借m->curg這個goroutine運(yùn)行Go代碼),所以我們需要保存當(dāng)前環(huán)境到以便之后再次返回到m->g0棧。
好了,runtime.cgocallbackg現(xiàn)在是運(yùn)行在一個真實(shí)的goroutine棧中(不是m->g0棧)。不過現(xiàn)在我們只是切換到了goroutine棧,此刻還是處于syscall狀態(tài)的。因此這個函數(shù)會先調(diào)用runtime.exitsyscall,接著才是執(zhí)行Go代碼。當(dāng)它調(diào)用runtime.exitsyscall,這會阻塞這條goroutine直到滿足$GOMAXPROCS限制條件。一旦從exitsyscall返回,則可以安全地執(zhí)行像調(diào)用內(nèi)存分配或者是調(diào)用Go的回調(diào)函數(shù)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重獲控制權(quán)之后,它切換回m->g0棧,從棧中恢復(fù)之前的m->g0.sched.sp值,然后返回到_cgoexp_GoF。_cgoexp_GoF立即返回到crosscall2,它會恢復(fù)被調(diào)者為gcc保存的寄存器并返回到GoF,最后返回到C的調(diào)用函數(shù)中。
無論是Go調(diào)用C,還是C調(diào)用Go,其需要解決的核心問題其實(shí)都是提供一個C/Go的運(yùn)行環(huán)境來執(zhí)行相應(yīng)的代碼。Go的代碼執(zhí)行環(huán)境就是goroutine以及Go的runtime,而C的執(zhí)行環(huán)境需要一個不使用分段的棧,并且執(zhí)行C代碼的goroutine需要暫時地脫離調(diào)度器的管理。要達(dá)到這些要求,運(yùn)行時提供的支持就是切換棧,以及runtime.entersyscall。
在Go中調(diào)用C函數(shù)時,runtime.cgocall中調(diào)用entersyscall脫離調(diào)度器管理。runtime.asmcgocall切換到m的g0棧,于是得到C的運(yùn)行環(huán)境。
在C中調(diào)用Go函數(shù)時,crosscall2解決gcc編譯到6c編譯之間的調(diào)用協(xié)議問題。cgocallback切換回goroutine棧。runtime.cgocallbackg中調(diào)用exitsyscall恢復(fù)Go的運(yùn)行環(huán)境。
更多建議: