Go語言 cgo關(guān)鍵技術(shù)

2018-07-25 16:21 更新

上一節(jié)我們看了一些預(yù)備知識,解答了前面的一點疑惑。這一節(jié)我們將接著從宏觀上分析cgo實現(xiàn)中使用到的一些關(guān)鍵技術(shù)。而對于其中一些細(xì)節(jié)部分將留到下一節(jié)具體分析。

整個cgo的實現(xiàn)依賴于幾個部分,依賴于cgo命令生成樁文件,依賴于6c和6g對Go這一端的代碼進(jìn)行編譯,依賴gcc對C那一端編譯成動態(tài)鏈接庫,同時,還依賴于運行時庫實現(xiàn)Go和C互操作的一些支持。

cgo命令會生成一些樁文件,這些樁文件是給6c和6g命令使用的,它們是Go和C調(diào)用之間的橋梁。原始的C文件會使用gcc編譯成動態(tài)鏈接庫的形式使用。

cgo命令

gc編譯器在編譯源文件時,如果識別出go源文件中的

import "C"

字段,就會先調(diào)用cgo命令。cgo提取出相應(yīng)的C函數(shù)接口部分,生成樁文件。比如我們寫一個go文件test.go,內(nèi)容如下:

package main

/*
#include "stdio.h"

void test(int n) {
  char dummy[10240];

  printf("in c test func iterator %d\n", n);
  if(n <= 0) {
    return;
  }
  dummy[n] = '\a';
  test(n-1);
}
#cgo CFLAGS: -g
*/
import "C"

func main() {
    C.test(C.int(2))
}

對它執(zhí)行cgo命令:

go tool cgo test.go

在當(dāng)前目錄下會生成一個_obj的文件夾,文件夾里會包含下列文件:

.
├── _cgo_.o
├── _cgo_defun.c
├── _cgo_export.c
├── _cgo_export.h
├── _cgo_flags
├── _cgo_gotypes.go
├── _cgo_main.c
├── test.cgo1.go
└── test.cgo2.c

樁文件

cgo生成了很多文件,其中大多數(shù)作用都是包裝現(xiàn)有的函數(shù),或者進(jìn)行聲明。比如在test.cgo2.c中,它生成了一個函數(shù)來包裝test函數(shù):

void
_cgo_1b9ecf7f7656_Cfunc_test(void *v)
{
    struct {
        int p0;
        char __pad4[4];
    } __attribute__((__packed__)) *a = v;
    test(a->p0);
}

在_cgo_defun.c中是封裝另一個函數(shù)來調(diào)用它:

void
·_Cfunc_test(struct{uint8 x[8];}p)
{
    runtime·cgocall(_cgo_1b9ecf7f7656_Cfunc_test, &p);
}

test.cgo1.go文件中包含一個main函數(shù),它調(diào)用封裝后的函數(shù):

func main() {
    _Cfunc_test(_Ctype_int(2))
}

cgo做這些封裝原因來自兩方面,一方面是Go運行時調(diào)用cgo代碼時要做特殊處理,比如runtime.cgocall。另一方面是由于Go和C使用的命名空間不一樣,需要加一層轉(zhuǎn)換,像·_Cfunc_test中的·字符是Go使用的命令空間區(qū)分,而在C這邊使用的是_cgo_1b9ecf7f7656_Cfunc_test。

cgo會識別任意的C.xxx關(guān)鍵字,使用gcc來找到xxx的定義。C中的算術(shù)類型會被轉(zhuǎn)換為精確大小的Go的算術(shù)類型。C的結(jié)構(gòu)體會被轉(zhuǎn)換為Go結(jié)構(gòu)體,對其中每個域進(jìn)行轉(zhuǎn)換。無法表示的域?qū)胋yte數(shù)組代替。C的union會被轉(zhuǎn)換成一個結(jié)構(gòu)體,這個結(jié)構(gòu)體中包含第一個union成員,然后可能還會有一些填充。C的數(shù)組被轉(zhuǎn)換成Go的數(shù)組,C指針轉(zhuǎn)換為Go指針。C的函數(shù)指針會被轉(zhuǎn)換為Go中的uinptr。C中的void指針轉(zhuǎn)換為Go的unsafe.Pointer。所有出現(xiàn)的C.xxx類型會被轉(zhuǎn)換為_C_xxx。

如果xxx是數(shù)據(jù),那么cgo會讓C.xxx引用那個C變量(先做上面的轉(zhuǎn)換)。為此,cgo必須引入一個Go變量指向C變量,鏈接器會生成初始化指針的代碼。例如,gmp庫中:

mpz_t zero;

cgo會引入一個變量引用C.zero:

var _C_zero *C.mpz_t

然后將所有引用C.zero的實例替換為(*_C_zero)。

cgo轉(zhuǎn)換中最重要的部分是函數(shù)。如果xxx是一個C函數(shù),那么cgo會重寫C.xxx為一個新的函數(shù)_C_xxx,這個函數(shù)會在一個標(biāo)準(zhǔn)pthread中調(diào)用C的xxx。這個新的函數(shù)還負(fù)責(zé)進(jìn)行參數(shù)轉(zhuǎn)換,轉(zhuǎn)換輸入?yún)?shù),調(diào)用xxx,然后轉(zhuǎn)換返回值。

參數(shù)轉(zhuǎn)換和返回值轉(zhuǎn)換與前面的規(guī)則是一致的,除了數(shù)組。數(shù)組在C中是隱式地轉(zhuǎn)換為指針的,而在Go中要顯式地將數(shù)組轉(zhuǎn)換為指針。

處理垃圾回收是個大問題。如果是Go中引用了C的指針,不再使用時進(jìn)行釋放,這個很容易。麻煩的是C中使用了Go的指針,但是Go的垃圾回收并不知道,這樣就會很麻煩。

運行時庫部分

運行時庫會對cgo調(diào)用做一些處理,就像前面說過的,執(zhí)行C函數(shù)之前會運行runtime.entersyscall,而C函數(shù)執(zhí)行完返回后會調(diào)用runtime.exitsyscall。讓cgo的運行仿佛是在另一個pthread中執(zhí)行的,然后函數(shù)執(zhí)行完畢后將返回值轉(zhuǎn)換成Go的值。

比較難處理的情況是,在cgo調(diào)用的C函數(shù)中,發(fā)生了C回調(diào)Go函數(shù)的情況,這時處理起來會比較復(fù)雜。因為此時是沒有Go運行環(huán)境的,所以必須再進(jìn)行一次特殊處理,回到Go的goroutine中調(diào)用相應(yīng)的Go函數(shù)代碼,完成之后繼續(xù)回到C的運行環(huán)境??瓷先ビ悬c復(fù)雜,但是cgo對于在C中調(diào)用Go函數(shù)也是支持的。

從宏觀上來講cgo的關(guān)鍵技術(shù)就是這些,由cgo命令生成一些樁代碼,負(fù)責(zé)C類型和Go類型之間的轉(zhuǎn)換,命名空間處理以及特殊的調(diào)用方式處理。而運行時庫部分則負(fù)責(zé)處理好C的運行環(huán)境,類似于給C代碼一個非分段的??臻g并讓它脫離與調(diào)度系統(tǒng)的交互。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號