文章系列
【GO】Golang/C++混合編程 - SWIG
【GO】Golang/C++混合編程 - 初識(shí)
【GO】Golang/C++混合編程 - 入門(mén)
【GO】Golang/C++混合編程 - 基礎(chǔ)
【GO】Golang/C++混合編程 - 進(jìn)階一
【GO】Golang/C++混合編程 - 進(jìn)階二
【GO】Golang/C++混合編程 - 實(shí)戰(zhàn)
Golang/C++混合編程
編譯過(guò)程
GO 調(diào)用 C
對(duì)于比較簡(jiǎn)單的 CGO 代碼我們可以直接通過(guò)手動(dòng)調(diào)用
go tool cgo
命令來(lái)查看生成的中間文件。
// 例:
package main
//int sum(int a, int b) { return a+b; }
import "C"
func main() {
println(C.sum(1, 1))
}
go tool cgo main.go
# 生成的中間文件目錄
$ ls _obj | awk '{print $NF}'
_cgo_.o
_cgo_export.c
_cgo_export.h
_cgo_flags
_cgo_gotypes.go
_cgo_main.c
main.cgo1.go
main.cgo2.c
# 其中_cgo_.o爱态、_cgo_flags和_cgo_main.c文件和我們的代碼沒(méi)有直接的邏輯關(guān)聯(lián),可以暫時(shí)忽略
// main.cgo1.go,它是`main.go`文件展開(kāi)虛擬 C 包相關(guān)函數(shù)和變量后的 GO 代碼
package main
//int sum(int a, int b) { return a+b; }
import _ "unsafe"
func main() {
println((_Cfunc_sum)(1, 1))
}
// 其中`C.sum(1, 1)`函數(shù)調(diào)用被替換成了`(_Cfunc_sum)(1, 1)`, 每一個(gè)`C.xxx`形式的函數(shù)都會(huì)被替換為`_Cfunc_xxx`格式的純 GO 函數(shù),其中前綴`_Cfunc_`表示這是一個(gè)C函數(shù),對(duì)應(yīng)一個(gè)私有的 GO 橋接函數(shù)
// _cgo_gotypes.go
//go:cgo_unsafe_args
func _Cfunc_sum(p0 _Ctype_int, p1 _Ctype_int) (r1 _Ctype_int) {
_cgo_runtime_cgocall(_cgo_506f45f9fa85_Cfunc_sum, uintptr(unsafe.Pointer(&p0)))
if _Cgo_always_false {
_Cgo_use(p0)
_Cgo_use(p1)
}
return
}
// `_Cfunc_sum`函數(shù)在 CGO 生成的
// 其參數(shù)和返回值`_Ctype_int`類型對(duì)應(yīng)`C.int`類型秦忿,命名的規(guī)則和`_Cfunc_xxx`類似默赂,不同的前綴用于區(qū)分函數(shù)和類型
//
// 其中`_cgo_runtime_cgocall`對(duì)應(yīng)`runtime.cgocall`函數(shù)沛鸵,函數(shù)的聲明如下:
// func runtime.cgocall(fn, arg unsafe.Pointer) int32
// 第一個(gè)參數(shù)是 C 語(yǔ)言函數(shù)的地址,第二個(gè)參數(shù)是存放 C 語(yǔ)言函數(shù)對(duì)應(yīng)的參數(shù)結(jié)構(gòu)體的地址
// main.cgo2.c
//
// 被傳入C語(yǔ)言函數(shù)`_cgo_506f45f9fa85_Cfunc_sum`也是 CGO 生成的中間函數(shù)
void _cgo_506f45f9fa85_Cfunc_sum(void *v) {
struct {
int p0;
int p1;
int r;
char __pad12[4];
} __attribute__((__packed__)) *a = v;
char *stktop = _cgo_topofstack();
__typeof__(a->r) r;
_cgo_tsan_acquire();
r = sum(a->p0, a->p1);
_cgo_tsan_release();
a = (void*)((char*)a + (_cgo_topofstack() - stktop));
a->r = r;
}
// 這個(gè)函數(shù)參數(shù)只有一個(gè)void范型的指針缆八,函數(shù)沒(méi)有返回值曲掰。真實(shí)的sum函數(shù)的函數(shù)參數(shù)和返回值均通過(guò)唯一的參數(shù)指針類實(shí)現(xiàn)
//
// _cgo_506f45f9fa85_Cfunc_sum函數(shù)的指針指向的結(jié)構(gòu)為:
// struct {
// int p0;
// int p1;
// int r;
// char __pad12[4];
// } __attribute__((__packed__)) *a = v;
// 其中p0成員對(duì)應(yīng)sum的第一個(gè)參數(shù),p1成員對(duì)應(yīng)sum的第二個(gè)參數(shù)奈辰,r成員栏妖,__pad12用于填充結(jié)構(gòu)體保證對(duì)齊CPU機(jī)器字的整倍數(shù)
// 然后從參數(shù)指向的結(jié)構(gòu)體獲取調(diào)用參數(shù)后開(kāi)始調(diào)用真實(shí)的C語(yǔ)言版sum函數(shù),并且將返回值保持到結(jié)構(gòu)體內(nèi)返回值對(duì)應(yīng)的成員
因?yàn)?GO 語(yǔ)言和 C 語(yǔ)言有著不同的內(nèi)存模型和函數(shù)調(diào)用規(guī)范奖恰。其中
_cgo_topofstack
函數(shù)相關(guān)的代碼用于 C 函數(shù)調(diào)用后恢復(fù)調(diào)用棧吊趾。_cgo_tsan_acquire
和_cgo_tsan_release
則是用于掃描 CGO 相關(guān)的函數(shù)則是對(duì) CGO 相關(guān)函數(shù)的指針做相關(guān)檢查宛裕,C.sum
的整個(gè)調(diào)用流程圖如下:
C 調(diào)用 GO
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() {}
// 同上一講,export 關(guān)鍵字论泛,指明 sum 函數(shù)是導(dǎo)出的揩尸,可以被 C 代碼調(diào)用
// go build -buildmode=c-archive -o sum.a sum.go -> 編譯為 C 靜態(tài)庫(kù)
// 此時(shí)會(huì)生成一個(gè)`sum.a`靜態(tài)庫(kù)和`sum.h`頭文件
go tool cgo main.go
# 生成的中間文件目錄
$ ls _obj | awk '{print $NF}'
_cgo_export.c
_cgo_export.h
_cgo_gotypes.go
main.cgo1.go
main.cgo2.c
# 其中僅包含了需要關(guān)心的文件
其中_cgo_export.h文件的內(nèi)容和生成C靜態(tài)庫(kù)時(shí)產(chǎn)生的sum.h頭文件是同一個(gè)文件,里面同樣包含sum函數(shù)的聲明
// _cgo_export.c
int sum(int p0, int p1)
{
__SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
struct {
int p0;
int p1;
int r0;
char __pad0[4];
} __attribute__((__packed__)) a;
a.p0 = p0;
a.p1 = p1;
_cgo_tsan_release();
crosscall2(_cgoexp_8313eaf44386_sum, &a, 16, _cgo_ctxt);
_cgo_tsan_acquire();
_cgo_release_context(_cgo_ctxt);
return a.r0;
}
// sum 函數(shù)的內(nèi)容采用和前面類似的技術(shù)屁奏,將 sum 函數(shù)的參數(shù)和返回值打包到一個(gè)結(jié)構(gòu)體中岩榆,然后通過(guò)`runtime/cgo.crosscall2`函數(shù)將結(jié)構(gòu)體傳給`_cgoexp_8313eaf44386_sum`函數(shù)執(zhí)行
//
// `runtime/cgo.crosscall2`函數(shù)采用匯編語(yǔ)言實(shí)現(xiàn),它對(duì)應(yīng)的函數(shù)聲明如下
// func runtime/cgo.crosscall2(
// fn func(a unsafe.Pointer, n int32, ctxt uintptr),
// a unsafe.Pointer, n int32,
// ctxt uintptr,
// )
// fn是中間代理函數(shù)的指針了袁,a是對(duì)應(yīng)調(diào)用參數(shù)和返回值的結(jié)構(gòu)體指針
// 中間的`_cgoexp_8313eaf44386_sum`代理函數(shù)在`_cgo_gotypes.go`文件
// _cgo_gotypes.go
func _cgoexp_8313eaf44386_sum(a unsafe.Pointer, n int32, ctxt uintptr) {
fn := _cgoexpwrap_8313eaf44386_sum
_cgo_runtime_cgocallback(**(**unsafe.Pointer)(unsafe.Pointer(&fn)), a, uintptr(n), ctxt);
}
func _cgoexpwrap_8313eaf44386_sum(p0 _Ctype_int, p1 _Ctype_int) (r0 _Ctype_int) {
return sum(p0, p1)
}
// 內(nèi)部將 sum 的包裝函數(shù)`_cgoexpwrap_8313eaf44386_sum`作為函數(shù)指針朗恳,然后由`_cgo_runtime_cgocallback`函數(shù)完成 C 語(yǔ)言到 GO 函數(shù)的回調(diào)工作
// `_cgo_runtime_cgocallback`函數(shù)對(duì)應(yīng)`runtime.cgocallback`函數(shù),函數(shù)的類型如下:
// func runtime.cgocallback(fn, frame unsafe.Pointer, framesize, ctxt uintptr)
其調(diào)用流程载绿,大致如下:
內(nèi)存模型
CGO 是架接 GO 語(yǔ)言和 C 語(yǔ)言的橋梁粥诫,它使二者在二進(jìn)制接口層面實(shí)現(xiàn)了互通,但是我們要注意因兩種語(yǔ)言的內(nèi)存模型的差異而可能引起的問(wèn)題崭庸。如果在 CGO 處理的跨語(yǔ)言函數(shù)調(diào)用時(shí)涉及到了指針的傳遞怀浆,則可能會(huì)出現(xiàn) GO 語(yǔ)言和 C 語(yǔ)言共享某一段內(nèi)存的場(chǎng)景。我們知道 C 語(yǔ)言的內(nèi)存在分配之后就是穩(wěn)定的怕享,但是 GO 語(yǔ)言因?yàn)楹瘮?shù)棧的動(dòng)態(tài)伸縮可能導(dǎo)致棧中內(nèi)存地址的移動(dòng)执赡。如果 C 語(yǔ)言持有的是移動(dòng)之前的 GO 指針,那么以舊指針訪問(wèn) GO 對(duì)象時(shí)會(huì)導(dǎo)致程序崩潰函筋。
GO 訪問(wèn) C 內(nèi)存
C語(yǔ)言空間的內(nèi)存是穩(wěn)定的沙合,只要不是被人為提前釋放,那么在Go語(yǔ)言空間可以放心大膽地使用跌帐。
package main
/*
#include <stdlib.h>
void* makeslice(size_t memsize) {
return malloc(memsize);
}
*/
import "C"
import "unsafe"
func makeByteSlize(n int) []byte {
p := C.makeslice(C.size_t(n))
return ((*[1 << 31]byte)(p))[0:n:n]
}
func freeByteSlice(p []byte) {
C.free(unsafe.Pointer(&p[0]))
}
func main() {
s := makeByteSlize(1<<32+1)
s[len(s)-1] = 255
print(s[len(s)-1])
freeByteSlice(s)
}
// 我們通過(guò)`makeByteSlize`來(lái)創(chuàng)建大于4G內(nèi)存大小的切片首懈,從而繞過(guò)了 GO 語(yǔ)言實(shí)現(xiàn)的限制(需要代碼驗(yàn)證)。而`freeByteSlice`輔助函數(shù)則用于釋放從 C 語(yǔ)言函數(shù)創(chuàng)建的切片谨敛。
C 訪問(wèn) GO 內(nèi)存
參考值傳遞究履,每次都對(duì)內(nèi)存進(jìn)行拷貝:
package main
/*
void printString(const char* s) {
printf("%s", s);
}
*/
import "C"
func printString(s string) {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
C.printString(cs)
}
func main() {
s := "hello"
printString(s)
}
在 CGO 調(diào)用的 C 語(yǔ)言函數(shù)返回前,CGO 保證傳入的 Go 語(yǔ)言內(nèi)存在此期間不會(huì)發(fā)生移動(dòng)脸狸,C 語(yǔ)言函數(shù)可以大膽地使用 GO 語(yǔ)言的內(nèi)存
package main
/*
#include<stdio.h>
void printString(const char* s, int n) {
int i;
for(i = 0; i < n; i++) {
putchar(s[i]);
}
putchar('\n');
}
*/
import "C"
func printString(s string) {
p := (*reflect.StringHeader)(unsafe.Pointer(&s))
C.printString((*C.char)(unsafe.Pointer(p.Data)), C.int(len(s)))
}
func main() {
s := "hello"
printString(s)
}
我們通過(guò)
reflect.StringHeader
結(jié)構(gòu)體來(lái)獲取字符串的指針和長(zhǎng)度最仑,然后直接將指針和長(zhǎng)度傳遞給 C 語(yǔ)言函數(shù)。
但此時(shí)存在幾個(gè)可能發(fā)生的問(wèn)題炊甲,比如字符串的長(zhǎng)度超過(guò) C 語(yǔ)言函數(shù)的參數(shù)限制泥彤,或者字符串的長(zhǎng)度大于 C 語(yǔ)言函數(shù)的棧空間卿啡,那么 C 語(yǔ)言函數(shù)就會(huì)發(fā)生棧溢出吟吝;另外,如果 C 語(yǔ)言函數(shù)持有這塊內(nèi)存過(guò)久牵囤,這會(huì)導(dǎo)致 GO 語(yǔ)言內(nèi)協(xié)程注冊(cè)的棧內(nèi)存無(wú)法被回收爸黄,從而導(dǎo)致這個(gè)協(xié)程阻塞,并影響 GO 語(yǔ)言 GC 的效率揭鳞。
C 長(zhǎng)期持有 GO 指針對(duì)象
當(dāng) C 語(yǔ)言調(diào)用 GO 函數(shù)時(shí)炕贵,若存在返回值情況,那么此時(shí) GO 對(duì)象內(nèi)存的生命周期就超出了 GO 語(yǔ)言的管理范圍野崇,因?yàn)?C 語(yǔ)言可能正在使用這塊內(nèi)存称开,此時(shí)如果 GO 語(yǔ)言 GC 回收了這塊內(nèi)存,那么 C 語(yǔ)言就會(huì)訪問(wèn)到一塊無(wú)效的內(nèi)存乓梨,導(dǎo)致程序崩潰鳖轰。所以,我們不能在 C 語(yǔ)言中直接使用 GO 語(yǔ)言對(duì)象的內(nèi)存扶镀。
但是這種情況是存在的蕴侣,比如 C 語(yǔ)言需要持有 GO 語(yǔ)言對(duì)象的內(nèi)存,并在后續(xù)的調(diào)用中使用臭觉,那么這種情況下昆雀,我們需要將內(nèi)存拷貝到 C 語(yǔ)言中,或者借鑒內(nèi)存管理的思路蝠筑,讓 C 語(yǔ)言和 GO 語(yǔ)言共同管理這塊內(nèi)存(GO 語(yǔ)言提供方法給 C 語(yǔ)言使用)狞膘。
為了減少拷貝帶來(lái)的性能開(kāi)銷,我們主要使用方法二什乙,其例子如下:
package main
import "sync"
type ObjectId int32
var refs struct {
sync.Mutex
objs map[ObjectId]interface{}
next ObjectId
}
func init() {
refs.Lock()
defer refs.Unlock()
refs.objs = make(map[ObjectId]interface{})
refs.next = 1000
}
func NewObjectId(obj interface{}) ObjectId {
refs.Lock()
defer refs.Unlock()
id := refs.next
refs.next++
refs.objs[id] = obj
return id
}
func (id ObjectId) IsNil() bool {
return id == 0
}
func (id ObjectId) Get() interface{} {
refs.Lock()
defer refs.Unlock()
return refs.objs[id]
}
func (id *ObjectId) Free() interface{} {
refs.Lock()
defer refs.Unlock()
obj := refs.objs[*id]
delete(refs.objs, *id)
*id = 0
return obj
}
上述代碼可以看到挽封,我們通過(guò)一個(gè)map來(lái)管理Go語(yǔ)言對(duì)象和id對(duì)象的映射關(guān)系。其中NewObjectId用于創(chuàng)建一個(gè)和對(duì)象綁定的id臣镣,而id對(duì)象的方法可用于解碼出原始的Go對(duì)象辅愿,也可以用于結(jié)束id和原始Go對(duì)象的綁定。
下面一組函數(shù)以C接口規(guī)范導(dǎo)出退疫,可以被C語(yǔ)言函數(shù)調(diào)用:
package main
/*
extern char* NewGoString(char* );
extern void FreeGoString(char* );
extern void PrintGoString(char* );
static void printString(const char* s) {
char* gs = NewGoString(s);
PrintGoString(gs);
FreeGoString(gs);
}
*/
import "C"
//export NewGoString
func NewGoString(s *C.char) *C.char {
gs := C.GoString(s)
id := NewObjectId(gs)
return (*C.char)(unsafe.Pointer(uintptr(id)))
}
//export FreeGoString
func FreeGoString(p *C.char) {
id := ObjectId(uintptr(unsafe.Pointer(p)))
id.Free()
}
//export PrintGoString
func PrintGoString(s *C.char) {
id := ObjectId(uintptr(unsafe.Pointer(p)))
gs := id.Get().(string)
print(gs)
}
func main() {
C.printString("hello")
}
在
printString
函數(shù)中渠缕,我們通過(guò)NewGoString
創(chuàng)建一個(gè)對(duì)應(yīng)的 GO 字符串對(duì)象,返回的其實(shí)是一個(gè) id褒繁,不能直接使用亦鳞。我們借助PrintGoString
函數(shù)將 id 解析為 GO 語(yǔ)言字符串后打印。該字符串在 C 語(yǔ)言函數(shù)中完全跨越了 GO 語(yǔ)言的內(nèi)存管理棒坏,在PrintGoString
調(diào)用前即使發(fā)生了棧伸縮導(dǎo)致的 GO 字符串地址發(fā)生變化也依然可以正常工作燕差,因?yàn)樵撟址畬?duì)應(yīng)的 id 是穩(wěn)定的,在 GO 語(yǔ)言空間通過(guò) id 解碼得到的字符串也就是有效的坝冕。
導(dǎo)出 C 函數(shù)不能返回 GO 內(nèi)存
在 GO 語(yǔ)言中徒探,GO 是從一個(gè)固定的虛擬地址空間分配內(nèi)存。而 C 語(yǔ)言分配的內(nèi)存則不能使用 GO 語(yǔ)言保留的虛擬內(nèi)存空間喂窟。在CGO 環(huán)境测暗,GO 語(yǔ)言運(yùn)行時(shí)默認(rèn)會(huì)檢查導(dǎo)出返回的內(nèi)存是否是由 GO 語(yǔ)言分配的央串,如果是則會(huì)拋出運(yùn)行時(shí)異常。
/*
extern int* getGoPtr();
static void Main() {
int* p = getGoPtr();
*p = 42;
}
*/
import "C"
func main() {
C.Main()
}
//export getGoPtr
func getGoPtr() *C.int {
return new(C.int)
}
其中
getGoPtr
返回的雖然是 C 語(yǔ)言類型的指針碗啄,但是內(nèi)存本身是從 GO 語(yǔ)言的new
函數(shù)分配质和,也就是由 GO 語(yǔ)言運(yùn)行時(shí)統(tǒng)一管理的內(nèi)存。然后我們?cè)?C 語(yǔ)言的Main
函數(shù)中調(diào)用了getGoPtr
函數(shù)稚字,此時(shí)默認(rèn)將發(fā)送運(yùn)行時(shí)異常
$ go run main.go
panic: runtime error: cgo result has Go pointer
goroutine 1 [running]:
main._cgoexpwrap_cfb3840e3af2_getGoPtr.func1(0xc420051dc0)
command-line-arguments/_obj/_cgo_gotypes.go:60 +0x3a
main._cgoexpwrap_cfb3840e3af2_getGoPtr(0xc420016078)
command-line-arguments/_obj/_cgo_gotypes.go:62 +0x67
main._Cfunc_Main()
command-line-arguments/_obj/_cgo_gotypes.go:43 +0x41
main.main()
/Users/chai/go/src/github.com/chai2010 \
/advanced-go-programming-book/examples/ch2-xx \
/return-go-ptr/main.go:17 +0x20
exit status 2
異常說(shuō)明 CGO 函數(shù)返回的結(jié)果中含有 GO 語(yǔ)言分配的指針饲宿。指針的檢查操作發(fā)生在 C 語(yǔ)言版的
getGoPtr
函數(shù)中,它是由 CGO 生成的橋接 C 語(yǔ)言和 GO 語(yǔ)言的函數(shù)胆描。
下面是cgo生成的C語(yǔ)言版本getGoPtr函數(shù)的具體細(xì)節(jié)(在cgo生成的_cgo_export.c文件定義):
int* getGoPtr()
{
__SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
struct {
int* r0;
} __attribute__((__packed__)) a;
_cgo_tsan_release();
crosscall2(_cgoexp_95d42b8e6230_getGoPtr, &a, 8, _cgo_ctxt);
_cgo_tsan_acquire();
_cgo_release_context(_cgo_ctxt);
return a.r0;
}
其中
_cgo_tsan_acquire
是從LLVM
項(xiàng)目移植過(guò)來(lái)的內(nèi)存指針掃描函數(shù)瘫想,它會(huì)檢查 CGO 函數(shù)返回的結(jié)果是否包含 GO 指針。
需要說(shuō)明的是昌讲,CGO 默認(rèn)對(duì)返回結(jié)果的指針的檢查是有代價(jià)的国夜,特別是 CGO 函數(shù)返回的結(jié)果是一個(gè)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)時(shí)將花費(fèi)更多的時(shí)間。如果已經(jīng)確保了 CGO 函數(shù)返回的結(jié)果是安全的話短绸,可以通過(guò)設(shè)置環(huán)境變量GODEBUG=cgocheck=0
來(lái)關(guān)閉指針檢查行為支竹。
其中,0:關(guān)閉鸠按,1:默認(rèn)礼搁,2:更嚴(yán)格的檢查。