C 語言竟可以調用 Go 語言函數,這是如何實現的?
大家好,我是飛哥!
今天和大家聊一個問題,一門語言是否可以在同一個進程內調用另外一門語言實現的函數?例如 C 語言是否可以調用 Golang 實現的函數?注意我說的是同進程內調用,跨進程的 IPC、PRC 之類的技術不算。
直接拋出這個問題的答案,同進程跨語言調用是可行的。在各種語言設計時,爲了複用其它語言生態中積累下來的大量的代碼資產,都會實現跨語言調用的機制。例如在 Golang 中,實現了 cgo 機制來允許 Golang 和 C / C++ 互相調用。在 Java 中,允許通過 JNI 機制來調用 C / C++。
本文就以 C 調用 Golang 爲例,來帶大家瞭解下跨語言調用的底層實現原理。
一、C 調用 Go 函數的例子
一個 C 調用 Go 的程序實現大致可以分爲下面三個步驟:
-
第一步:使用 Golang 定義和實現一個函數
-
第二步:將 Golang 代碼編譯成一個靜態 / 動態鏈接庫
-
第三步:在 C 語言中調用該靜態 / 動態鏈接庫
我們先來看一個最簡單的例子,看看 C 語言調用 Go 函數該如何使用的。
1.1 Go 函數定義和實現
我們先用 Golang 來定義和實現一個最簡單的加法函數。
package main
//int add(int a, int b);
import "C"
//export add
func add(a, b C.int) C.int {
return a + b
}
func main() {
}
上面的代碼中,雖然代碼不長,但有好幾個需要注意的地方:
-
import "C" 這行表示是使用 CGO 特性。有了這一行代碼,go build 命令會在編譯和鏈接階段啓動 gcc 編譯器。
-
//int add(int a, int b)
。這一行其實不是註釋,是正常的 C 語言的代碼,聲明瞭一個 add 函數。 -
add 函數實現上面的 export add。這是在將 add 函數導出,否則外部無法調用它。
-
add 函數中的參數類型,只能使用 C.int。這是因爲不同語言的數據類型是可能有細微差異的,必須使用標準的 cgo 數據類型纔可以正常通信。
1.2 將 Go 代碼編譯成庫
接下來需要將 Go 寫的函數編譯成一個靜態 / 動態庫。我們採用 go build 來編譯。
# go build -buildmode=c-shared -o libadd.dylib main.go
在上述命令中,
-
-buildmode=c-shared 指的是要把 go 代碼編譯成動態鏈接庫。如果想編譯成靜態鏈接庫,則使用 -buildmode=c-archive。
-
-o libadd.dylib 是指定編譯生成的動態庫名。由於我使用是 mac,所以動態鏈接庫的文件後綴是 dylib。
執行上述命令後,go 編譯器會將 go 源代碼編譯後生成一個頭文件 libadd.h,還有一個包含 add 函數二進制代碼的動態庫,這個函數滿足 C 語言調用約定的。庫的格式在 mac 下是 libadd.dylib(在 linux 下是 libadd.so)。
1.3 C 語言調用庫中函數
接着我們再寫一小段簡單的 C 語言代碼,來調用動態庫中的 add 函數。
#include <stdio.h>
#include "libadd.h"
int main(void)
{
int ret = add(2,3);
printf("C調用Go函數2+3= %d", ret);
return 0;
}
在這個 C 語言函數中,把 libadd.h 頭文件引用一下,就可以使用 add 函數了。
然後編譯和鏈接這個程序。注意使用 -L 選項指定要鏈接的庫的位置。-l 選項指定要鏈接的庫的名字,鏈接器會尋找以 libxxx 爲名的動態庫。
# gcc main.c -L. -ladd -o main
編譯完後,會生成一個可執行文件 main。執行該文件,發現 C 調用 Go 函數 add 成功!!
# ./main
C調用Go函數2+3=5
二、C 調用 Go 函數實現原理
只說技術如何使用不講原理,從來都不是咱們「開發內功修煉」的風格。在這一節中,我們來深入瞭解下 C 調用 Go 函數內部是如何實現的。
2.1 cgo 編譯工具
幸運的是,cgo 編譯工具不但可以勝任編譯工作,還把編譯過程的中間文件也能展示出來。這對大家理解其內部工作方式非常有幫助。
我們用 cgo 來生成一下中間編譯過程文件
# go tool cgo main.go
cgo 首先會爲每個包含 import "C"
指令的 go 源文件生成兩個中間文件。我們使用的文件名是 main.go,所以生成的文件名是 main.cgo1.go、main.cgo2.c。
接着對會整個 main 包生成一個 _cgo_gotypes.go,這裏麪包含了 Go 語言一些輔助代碼。
最後會生成包含導出的 C 語言 add 的入口函數以及其頭文件,_cgo_export.c 和 _cgo_export.h。
生成的文件都放在 _obj 目錄下。
# ll _obj
-rw-r--r-- 1 ... 2216 4 22 08:35 _cgo_.o
-rw-r--r-- 1 ... 1090 4 22 08:35 _cgo_export.c
-rw-r--r-- 1 ... 1652 4 22 08:35 _cgo_export.h
-rw-r--r-- 1 ... 13 4 22 08:35 _cgo_flags
-rw-r--r-- 1 ... 1013 4 22 08:35 _cgo_gotypes.go
-rw-r--r-- 1 ... 653 4 22 08:35 _cgo_main.c
-rw-r--r-- 1 ... 324 4 22 08:35 main.cgo1.go
-rw-r--r-- 1 ... 2028 4 22 08:35 main.cgo2.c
2.2 函數調用入口 _cgo_export.c
在 C 語言代碼中調用 add 函數時,最先進入的是位於 _cgo_export.c 中的調用入口。該入口函數定義如下:
int add(int a, int b)
{
__SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
typedef struct {
int p0;
int p1;
int r0;
} __attribute__((__packed__)) _cgo_argtype;
static _cgo_argtype _cgo_zero;
_cgo_argtype _cgo_a = _cgo_zero;
_cgo_a.p0 = a;
_cgo_a.p1 = b;
...
crosscall2(_cgoexp_ec46b88da812_add, &_cgo_a, 12, _cgo_ctxt);
...
return _cgo_a.r0;
}
在這個函數源碼中主要包含兩塊功能:
第一是對 Go 語言運行時的初始化,這是由 _cgo_wait_runtime_init_done 函數完成的。因爲 Go 函數還是需要由 Go 運行時來執行,所以確保 Go 運行時已經初始化是必要的。
第二是調用 runtime 的 crosscall2 函數,把調用轉交給 Go runtime 來處理。
在調用 runtime 的 crosscall2 之前,先定義了一個包含所有輸入參數、輸出參數的 _cgo_argtype,並將 C 語言輸入的兩個參數打包進來。這樣訪問 _cgo_a 就可以獲取到所有的輸入和輸出參數。
另外也告訴 runtime 該函數的在 Go 語言實現的入口函數名是 _cgoexp_ec46b88da812_add。等 runtime 中的邏輯執行完了後要回調這個函數來進一步執行指。
2.3 Go runtime cgo 執行
在上一小節我們看到了 Go runtime 的入口是 crosscall2 函數。這是一個純彙編寫的函數,其源碼位於 runtime/cgo/asm_amd64.s 文件中。
//file:runtime/cgo/asm_amd64.s
TEXT crosscall2(SB),NOSPLIT,$0-0
PUSH_REGS_HOST_TO_ABI0()
// Make room for arguments to cgocallback.
ADJSP $0x18
MOVQ CX, 0x0(SP) /* fn */
MOVQ DX, 0x8(SP) /* arg */
// Skip n in R8.
MOVQ R9, 0x10(SP) /* ctxt */
CALL runtime·cgocallback(SB)
ADJSP $-0x18
POP_REGS_HOST_TO_ABI0()
RET
在計算機體系結構和編程中,ABI 定義了函數調用的約定,包括寄存器使用、參數傳遞等。不同的語言採用的 ABI 調用習慣是不一樣的。
這裏因爲要從 C 語言進入到 Go 語言中運行,所以在 crosscall2 的開頭和結尾處有 PUSH_REGS_HOST_TO_ABI0、POP_REGS_HOST_TO_ABI0 兩個函數。PUSH_REGS_HOST_TO_ABI0 函數作用是保存調用方使用的寄存器。等調用結束後再通過 POP_REGS_HOST_TO_ABI0 恢復這些寄存器的值。
接着通過 ADJSP 指令將棧擴張一些,把輸入參數入棧,然後調用 runtime·cgocallback 函數來進一步在 runtime 中運行。
//file:runtime/asm_amd64.s
TEXT ·cgocallback(SB),NOSPLIT,$24-24
......
havem:
// 保存線程棧
MOVQ m_g0(BX), SI
...
// 切換到協程棧
MOVQ m_curg(BX), SI
...
// 調用 runtime.cgocallbackg
MOVQ $runtime·cgocallbackg(SB), AX
...
cgocallback 函數很長,我對它進行了提煉和精簡。這裏面主要是進行了棧的切換。因爲 C 語言是使用線程來運行的,而 Go 是使用協程來執行。所以這裏就需要保存線程棧,並切換到協程棧,然後才能夠進入到 Go 函數中繼續執行。執行完棧切換後,交由 runtime·cgocallbackg 中進一步處理。
//file:runtime/cgocall.go
func cgocallbackg(fn, frame unsafe.Pointer, ctxt uintptr) {
gp := getg()
lockOSThread()
...
cgocallbackg1(fn, frame, ctxt)
...
}
在 cgocallbackg 函數中,已經是由協程來執行處理了。但這時候還要調用 lockOSThread 來告訴 Go 的調度器,要把當前的協程綁定在當前線程上運行,不要把它交給其它線程。
接着調用 cgocallbackg1,在這個函數中調用 reflectcall,正式進入到用戶定義的 Go 函數。
func cgocallbackg1(fn, frame unsafe.Pointer, ctxt uintptr) {
gp := getg()
...
var cb func(frame unsafe.Pointer)
cbFV := funcval{uintptr(fn)}
*(*unsafe.Pointer)(unsafe.Pointer(&cb)) = noescape(unsafe.Pointer(&cbFV))
cb(frame)
...
}
2.4 Go 語言函數
從 runtime 出來後首先進入到的函數並不是我們寫的 Go 函數代碼,而是由 cgo 生成的一個樁代碼。在樁代碼中,是爲了將參數轉換成 Go 正常的參數列表。
這個樁代碼位於 _cgo_gotypes.go 文件中。
//go:cgo_export_dynamic add
//go:linkname _cgoexp_ec46b88da812_add _cgoexp_ec46b88da812_add
//go:cgo_export_static _cgoexp_ec46b88da812_add
func _cgoexp_ec46b88da812_add(a *struct {
p0 _Ctype_int
p1 _Ctype_int
r0 _Ctype_int
}) {
a.r0 = add(a.p0, a.p1)
}
通過代理函數調用到 main.go 函數中定義的 add 函數。
//export add
func add(a, b C.int) C.int {
return a + b
}
經過漫長的路徑,一個 C 函數調用 Go 函數的執行流總算是打通了。
三、總結
我們來總結一下 C 語言調用 Go 語言函數的底層執行過程。
總體上來看,跨語言的調用是由三部分代碼來配合運行的。分別是用戶代碼、cgo 生成的樁代碼、Go 語言運行時。
-
在 C 語言代碼中調用 add 函數時,最先進入的是位於 _cgo_export.c 中的樁代碼。
-
接着進入 Go 語言運行時,包括 crosscall2、·cgocallback、cgocallbackg、cgocallbackg1 等函數。在這些運行時函數中保存了調用方的寄存器、將線程棧切換到了協程棧、把協程鎖定到當前線程上運行。
-
Go 語言運行時執行完後,通過 _cgo_gotypes.go 中的樁代碼 _cgoexp_ec46b88da812_add 函數後真正進入到 Go 函數中運行。
我們在很早的一篇函數調用太多了會有性能問題嗎? 文章中曾經分析過 C 語言內部的函數開銷。每個 C 語言函數大概只需要 8 個指令,平均耗時 0.43 納秒。
通過今天的文章我們可以看到跨語言的函數調用的執行過程是非常複雜的,要比語言內部的函數調用要複雜的多。所以在性能上開銷也是要大於普通函數調用。但由於仍然是屬於進程內部的調用,不像 RPC 一樣需要進行內核協議棧處理、協議序列化 / 反序列化。所以還是比 RPC 調用性能要好的。也就是說在性能開銷上,語言內函數調用 < 跨語言函數調用 < RPC 調用。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/zvI3SJ2zItHzx-7w1HWu0Q