CGO: Go與C互操作技術(一):Go調C基本原理


GO調C基本原理

CGO是實現Go與C互操作的方式,包括Go調C和C調Go兩個過程。其中Go調C的過程比較簡單。對於一個在C中定義的函數add3,在Go中調用時需要顯式的使用C.add3調用。其中C是在程序中引入的一個偽包。示例代碼如下所示:

image.png

圖一:CGO使用示例代碼

            代碼中的import “C”即為在Go中使用的偽包。這個包並不真實存在,也不會被Go的compile組件見到,它會在編譯前被CGO工具捕捉到,並做一些代碼的改寫和樁文件的生成。

下面是CGO工具產生的目錄樹(可以使用go tool cgo在本地目錄生成這些樁文件):

image.png

圖二:CGO產生的樁文件目錄樹

            其中main.cgo1.go為主要文件,是用戶代碼main.go被cgo改寫之后的文件:

image.png

圖三:CGO改寫之后的用戶代碼

這個文件才是Go的compile組件真正看到的用戶代碼。可以看到原來文件中的import “C”被去掉,而用戶寫的C.int被改寫為_Ctype_int,C.add3被改寫為_Cfunc_add3。關於這個特性有兩個點需要注意。一是在有import “C”的文件中,用戶的注釋信息全部丟失,使用的一些progma也不例外。二是在testing套件中import “C”不允許使用,表現為testing不支持CGO。但並不是沒有辦法在testing中使用CGO,可以利用上面的特性,在另外一個獨立的Go文件中定義C函數,並使用import “C”;但是在使用testing的Go文件中直接使用_Cfunc_add3函數即可。_Cfunc_add3用戶雖然沒有顯示定義,但是CGO自動產生了這一函數的定義。上面一系列的//line編譯制導語句用做關聯生成的Go與原來的用戶代碼的行號信息。

再次回到_Cfunc_add3函數,並不是C中的add3函數,是CGO產生的一個Go函數。它的定義在CGO產生的樁文件_cgo_gotypes.go中:

image.png

圖四:_Cfunc_add3的定義

            _Cfunc_add3的參數傳遞與正常的函數有些不同,其參數並不在棧上,而是在堆上。函數中的_Cgo_use,其實是runtime.cgoUse,用來告訴編譯器要把p0, p1, p2逃逸到堆上去,這樣才能較為安全的把參數傳遞到C的程序中去。

image.png

圖五:參數逃逸到堆中

函數中的__cgo_79f22807c129_Cfunc_add3是一個變量,記錄了一個C函數的地址(注意,這並不是實際要調用add3函數),是一個真正定義在C程序中的函數。在Go中,通過編譯制導語句//go:cgo_import_static在鏈接時拿到C中函數__cgo_79f22807c129_Cfunc_add3的地址,然后通過編譯制導語句//go:linkname把這個函數地址與Go中的byte型變量__cgofn_cgo_79f22807c129_Cfunc_add3的地址對齊在一起。之后再利用一個新的變量__cgo_79f22807c129_Cfunc_add3記錄這個byte型變量的地址。從而可以實現在Go中拿到C中函數的地址。做完,這些之后把C的函數地址和參數地址傳給cgocall函數,進行Go與C之間call ABI操作。當然,cgocall里面會做一些調度相關的准備動作,后面有詳細說明。

__cgo_79f22807c129_Cfunc_add3如上文所述,是定義在main.cgo2.c中的一個函數,其定義如下:

image.png

圖六:__cgo_79f22807c129_Cfunc_add3的定義

         在這個函數的定義中,並沒有顯式的參數拷貝;而是利用類型強轉,在C中直接操作Go傳遞過來的參數地址。在這個函數中真正調用了用戶定義的add3函數。

            cgocall即_Cfunc_add3中的_cgo_runtime_cgocall函數,是runtime中的一個從Go調C的關鍵函數。這個函數里面做了一些調度相關的安排。之所以有這樣的設計,是因為Go調入C之后,程序的運行不受Go的runtime的管控。一個正常的Go函數是需要runtime的管控的,即函數的運行時間過長會導致goroutine的搶占,以及GC的執行會導致所有的goroutine被拉齊。C程序的執行,限制了Go的runtime的調度行為。為此,Go的runtime會在進入到C程序之后,會標記這個運行C的線程排除在runtime的調度之后,以減少這個線程對Go的調度的影響。此外,由於正常的Go程序運行在一個2K的棧上,而C程序需要一個無窮大的棧。這樣的設計會導致在Go的棧上執行C函數會導致棧的溢出,因此在進去C函數之前需要把當前線程的棧從2K的棧切換到線程本身的系統棧上。棧切換發生在asmcgocall中,而線程的狀態標記發生在cgocall中。其具體原理示意圖如下所示:

image.png 

image.png

image.png

圖八:Go調C原理圖

【版權聲明】本文為華為雲社區用戶原創內容,轉載時必須標注文章的來源(華為雲社區),文章鏈接,文章作者等基本信息,否則作者和本社區有權追究責任。如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: huaweicloud.bbs@huawei.com進行舉報,並提供相關證據,一經查實,本社區將立刻刪除涉嫌侵權內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM