Go語言 C調用Go

2018-07-25 16:25 更新

cgo不僅僅支持從Go調用C,它還同樣支持從C中調用Go的函數(shù),雖然這種情況相對前者較少使用。

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

使用export標記可以將Go函數(shù)導出提供給C調用:

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

下面讓我們看看它是如何實現(xiàn)的。假定上面的函數(shù)GoF是在Go語言的一個包p內的,為了能夠讓gcc編譯的C代碼調用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,因為gcc沒有包的概念。由gcc編譯的C函數(shù)可以調用這個GoF函數(shù)。

GoF調用crosscall2(_cgoexp_GoF, frame, framesize)。crosscall2是用匯編代碼實現(xiàn)的,它是一個兩參數(shù)的適配器,作用是從gcc函數(shù)調用6c函數(shù)(6c和gcc使用的調用協(xié)議還是有些區(qū)別的)。crosscall2實現(xiàn)了從一個ABI的gcc函數(shù)調用,到6c的函數(shù)調用ABI。所以上面代碼中實際上相當于調用_cgoexp_GoF(frame,framesize)。注意此時是仍然運行在mg的g0棧并且不受GOMAXPROCS限制的。因此,這個代碼不能直接調用任意的Go代碼并且不能分配內存或者用盡m->g0的棧。

_cgoexp_GoF調用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也是一個用匯編實現(xiàn)的函數(shù)。它從m->g0的棧切換回原來的goroutine的棧,并在這個棧中調用runtime.cgocallbackg(p.GoF, frame, framesize)。

這中間會涉及到一些保存棧寄存器之類的細節(jié)操作比較復雜。因為這個過程相當于我們接管了m->curg的執(zhí)行,但是卻并沒有完全恢復到之前的運行環(huán)境(只是借m->curg這個goroutine運行Go代碼),所以我們需要保存當前環(huán)境到以便之后再次返回到m->g0棧。

好了,runtime.cgocallbackg現(xiàn)在是運行在一個真實的goroutine棧中(不是m->g0棧)。不過現(xiàn)在我們只是切換到了goroutine棧,此刻還是處于syscall狀態(tài)的。因此這個函數(shù)會先調用runtime.exitsyscall,接著才是執(zhí)行Go代碼。當它調用runtime.exitsyscall,這會阻塞這條goroutine直到滿足$GOMAXPROCS限制條件。一旦從exitsyscall返回,則可以安全地執(zhí)行像調用內存分配或者是調用Go的回調函數(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重獲控制權之后,它切換回m->g0棧,從棧中恢復之前的m->g0.sched.sp值,然后返回到_cgoexp_GoF。_cgoexp_GoF立即返回到crosscall2,它會恢復被調者為gcc保存的寄存器并返回到GoF,最后返回到C的調用函數(shù)中。

小結

無論是Go調用C,還是C調用Go,其需要解決的核心問題其實都是提供一個C/Go的運行環(huán)境來執(zhí)行相應的代碼。Go的代碼執(zhí)行環(huán)境就是goroutine以及Go的runtime,而C的執(zhí)行環(huán)境需要一個不使用分段的棧,并且執(zhí)行C代碼的goroutine需要暫時地脫離調度器的管理。要達到這些要求,運行時提供的支持就是切換棧,以及runtime.entersyscall。

在Go中調用C函數(shù)時,runtime.cgocall中調用entersyscall脫離調度器管理。runtime.asmcgocall切換到m的g0棧,于是得到C的運行環(huán)境。

在C中調用Go函數(shù)時,crosscall2解決gcc編譯到6c編譯之間的調用協(xié)議問題。cgocallback切換回goroutine棧。runtime.cgocallbackg中調用exitsyscall恢復Go的運行環(huán)境。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號