橋接 Rust 和原生 Go
你好,大家好,我是 yuchanns!
最近我做了一些有趣的事情,想和你分享:介紹 OpenDAL 作爲 Go 語言的原生綁定。
TLDR; 我將向你展示一種可行的方法,利用 purego 和 libffi 的魔力,從 Rust 和 C 組件構建原生 Go 綁定。
什麼是 OpenDAL?
Apache OpenDAL[1] 是一個 Rust 庫,提供了統一的數據訪問層。它爲訪問各種存儲服務(如 S3、Google Drive 和 Dropbox)提供了一致的 API。
OpenDAL 有一個願景,幫助用戶以任何語言、任何方法和任何集成自由訪問數據。這一願景推動社區構建了許多其他語言綁定。
我們已經發布了 Java、NodeJS 和 Python 的綁定。但是我們還沒有 Go 的綁定。
CGo 不是 Go
那麼問題是什麼?
@Suyan[2] 告訴我,Go 綁定因涉及構建和使用 CGo 而變得複雜而停滯不前。
讓我們快速回顧 9ef494d[3],這是在我們更新了 Go 綁定以完全支持原生功能之前所做的提交。
由於 Go 綁定是建立在 C 綁定之上的,讓我們首先構建一個 C 綁定工件:
cd bindings/c
make build
然後我們需要添加一個名爲 opendal_c.pc
的文件,其中包含以下內容:
libdir=/path/to/opendal/target/debug/
includedir=/path/to/opendal/bindings/c/include/
Name: opendal_c
Description: opendal c binding
Version:
Libs: -L${libdir} -lopendal_c
Cflags: -I${includedir}
在這之後,我們可以使用以下命令構建 Go 綁定:
export PKG_CONFIG_PATH=/dir/of/opendal_c.pc
cd bindings/go
go build -tags dynamic .
最後,我們使用以下命令運行測試
expose LD_LIBRARY_PATH=/path/to/opendal/bindings/c/target/debug/
go test -tags dynamic .
如你所見,在我們與 OpenDAL 集成之前,需要進行 4 項繁瑣的手動操作,這與 Go 的包管理方法相悖。
無法做出這樣的權衡是不值得的。以這種方式無法在 Go 原生社區中推廣 OpenDAL。沒有人對其進行維護感興趣,即使構建了,用戶實際上也無法 go get
綁定!畢竟,他們已經說過:
CGo 不是 Go。
調用 Rust 而不使用 CGo
我開始思考:難道沒有一種方法可以直接從 Go 中純粹地調用 Rust 嗎?
答案是肯定的。我進行了大量的在線搜索,然後從這篇文章 RustGo: Calling Rust from Go with near-zero overhead[4] 中得到了一個有趣的想法。
簡而言之,這個想法是將受限制的 Rust 代碼鏈接起來,並在 Go 的粘合層中調用它。
這太棒了,如果你喜歡的話可以去看一下。主要問題是它依賴於一些彙編語言的粘合劑,對我來說太複雜了。想象一下,你必須爲每個方法寫大量的彙編代碼,並在每個平臺上都進行適配,那簡直是瘋狂。
TEXT ·ScalarBaseMult(SB), 0, $16384-16
MOVQ dst+0(FP), DI
MOVQ in+8(FP), SI
MOVQ SP, BX
ADDQ $16384, SP
ANDQ $~15, SP
MOVQ ·_scalar_base_mult(SB), AX
CALL AX
MOVQ BX, SP
RET
忘了吧。
第二個想法是 purego[5]。它是由 Ebitengine 創建的,旨在支持 Linux、macOS、Windows、FreeBSD 和包括 amd64 和 arm64 在內的架構。
它聲稱我們可以在不使用 CGo 的情況下從 Go 調用 C 函數,這意味着交叉編譯非常簡單,我們的用戶只需一個指令 go get
就可以輕鬆獲取綁定。
把這個例子快速回顧一下:
package main
import (
"fmt"
"runtime"
"github.com/ebitengine/purego"
)
func getSystemLibrary() string {
switch runtime.GOOS {
case "darwin":
return "/usr/lib/libSystem.B.dylib"
case "linux":
return "libc.so.6"
default:
panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS))
}
}
func main() {
libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
panic(err)
}
var puts func(string)
purego.RegisterLibFunc(&puts, libc, "puts")
puts("Calling C from Go without Cgo!")
}
嗯,雖然不是直接的,但仍然是純粹的。我們所需要的只是基於我們的 C 綁定工件的共享對象( *.so
)。
我現在迫不及待地想要實現它了!
結構體的麻煩
很快我遇到了問題。
現實世界告訴我們,purego 不支持結構作爲返回值,而我需要調用的首個 C 函數是 opendal_operator_new
,它返回一個結構 opendal_operator
。
我反覆查看了示例,包括一個稍微複雜的窗口演示。結果顯示,結構值僅受 Darwin 平臺支持,並作爲實驗性功能。
看起來我們又陷入了僵局。
突然,一個略微不太理智的想法閃現在我的腦海中。
衆所周知,高級語言的編譯器生成遵循特定調用約定的代碼,以便程序可以通過接口橋調用外部函數。
那就是 libffi[6] 的目標。
libffi 已經移植到許多平臺,並覆蓋了我們所需的所有內容。
把 libffi 用 purego 封裝起來,然後通過所謂的 purego-libffi 調用我們的 C 綁定,怎麼樣?
是的,這是可能的。而且已經有人做到了。
嗨社區,讓我向你們介紹 JupiterRider/ffi[7]。
橋接 Rust 和 Go 世界
我在不到半天的時間內建立了一個 概念演示 [8]。後來我向 OpenDAL 社區提交了一個議題 [9] 。
使用 purego + libbfi 的神奇組合,我們可以根據 C 綁定函數的簽名輕鬆地勾勒調用方法。以 opendal_operator_new
爲例:
// C-binding signature
struct opendal_result_operator_new opendal_operator_new(const char *scheme,
const struct opendal_operator_options *options);
struct opendal_operator_options *opendal_operator_options_new(void);
typedef struct opendal_operator_options {
struct HashMap_String__String *inner;
} opendal_operator_options;
typedef struct opendal_result_operator_new {
struct opendal_operator *op;
struct opendal_error *error;
} opendal_result_operator_new;
該函數返回一個名爲 opendal_result_operator_new
的結構。因此,我們可以使用 ffi.Type
構造一個表示它的 Go 變量
var (
typeResultOperatorNew = ffi.Type{
Type: ffi.Struct,
Elements: &[]*ffi.Type{
&ffi.TypePointer,
&ffi.TypePointer,
nil,
}[0],
}
)
你可能會注意到 C 結構內有兩個字段,但我們不會爲它們構造類型化變量,因爲它們只是指針。
我們打算把這個結構應用於下面這個函數:
func NewOperator(name string, opts *OperatorOptions) (*Operator, error) {
var cif ffi.Cif
if status := ffi.PrepCif(&cif, ffi.DefaultAbi, 2, &TypeResultOperatorNew, &ffi.TypePointer, &ffi.TypePointer); status != ffi.OK {
return nil, errors.New(status.String())
}
sym, _ := purego.Dlsym(libopendal, "opendal_operator_new")
fn := func(name string, opts OperatorOptions) (*ResultOperatorNew, error) {
byteName, err := unix.BytePtrFromString(name)
if err != nil {
return nil, err
}
var result ResultOperatorNew
ffi.Call(&cif, sym, unsafe.Pointer(&result), unsafe.Pointer(&byteName), unsafe.Pointer(&opts))
return &result, nil
}
result, _ := fn(name, *opts)
return result.op, nil
}
type ResultOperatorNew struct {
op *Operator
error *Error
}
type Operator struct {
ptr uintptr
}
儘管我們忽略了一些錯誤處理,但大致上應該是這樣。
只要我們正確指定變量定義了相應的 C 結構,我們將使用類似的 Go 結構獲取返回值 ResultOperatorNew
。
現在我們可以與 CGO_ENABLED=0
一起使用。
這就是全部。這樣就建立了 Rust 和原生 Go 之間的橋樑。其餘的工作仍然是直截了當和簡單的。
很快,我們將擁有一個完全功能的 Go 綁定,以利用 OpenDAL 的強大功能。敬請關注!
分發和基準測試
事情還沒有做完。
就我而言,分發共享對象對於維護者來說仍然是一個重大挑戰。雖然我們可以將共享對象嵌入到 Go 文件中,但 C 綁定的構件對於 Go 庫來說太大了。默認功能包含 15 個服務,在發佈後達到 12.4M!
我們設法將默認服務減少到僅一個,並使用 zstd 進行壓縮。現在每個服務的大小已經縮小到 400K〜2M。
除此之外,我們創建了一個倉庫來提供這些預構建的 Go 綁定服務。
請記住,apache/opendal-go-services[10] 是可選的,用戶可以根據自己的條件和特性構建自己的構件。
此外,我創建了一個基準測試 [11] 來滿足一些好奇心。它比較了原生 Go ( github.com/apache/opendal/bindings/go
) 和 CGo ( pkg: opendal.apache.org/go
) 在讀寫方面使用內存服務的性能。
benchstat old.txt new.txt
goos: linux
goarch: arm64
pkg: github.com/apache/opendal/bindings/go
│ new.txt │
│ sec/op │
Write4KiB-10 2.844µ ± ∞ ¹
Write256KiB-10 10.09µ ± ∞ ¹
Write4MiB-10 99.16µ ± ∞ ¹
Write16MiB-10 658.2µ ± ∞ ¹
Read4KiB-10 6.387µ ± ∞ ¹
Read256KiB-10 82.70µ ± ∞ ¹
Read4MiB-10 1.228m ± ∞ ¹
Read16MiB-10 3.617m ± ∞ ¹
geomean 90.23µ
¹ need >= 6 samples for confidence interval at level 0.95
pkg: opendal.apache.org/go
│ old.txt │
│ sec/op │
Write4KiB-10 4.240µ ± ∞ ¹
Write256KiB-10 10.11µ ± ∞ ¹
Write4MiB-10 89.58µ ± ∞ ¹
Write16MiB-10 646.2µ ± ∞ ¹
Read4KiB-10 20.94µ ± ∞ ¹
Read256KiB-10 132.7µ ± ∞ ¹
Read4MiB-10 1.847m ± ∞ ¹
Read16MiB-10 6.305m ± ∞ ¹
geomean 129.7µ
¹ need >= 6 samples for confidence interval at level 0.95
哇,我必須說這真是個驚喜!
接下來是什麼
我已經爲 Go 綁定創建了一個跟蹤問題 [12],你可以隨意可以選擇一個關注點。我們很快將發佈 opendal-go 的第一個版本。即使現在,你也可以嘗試一下 go get
!
go get github.com/apache/opendal/bindings/go
go get github.com/apache/opendal-go-services/memory
libffi 的純 Go 封裝現在只支持 Linux 和 BSD,但 @Xuanwo[13] 和我已經與作者 @JupiterRider[14] 討論了對 Windows 和 macOS 的支持 [15]。
這就是開源的美妙之處!通過與上游和下游密切合作,我們可以共同推動事情發生!
引用鏈接
[1]
Apache OpenDAL: https://opendal.apache.org/
[2]
@Suyan: https://github.com/suyanhanx
[3]
9ef494d: https://github.com/apache/opendal/commit/9ef494d6df2e9a13c4e5b9b03bcb36ec30c0a7c0
[4]
RustGo: Calling Rust from Go with near-zero overhead: https://words.filippo.io/rustgo/
[5]
purego: https://github.com/ebitengine/purego
[6]
libffi: https://github.com/libffi/libffi
[7]
JupiterRider/ffi: https://github.com/JupiterRider/ffi
[8]
概念演示: https://github.com/yuchanns/opendal
[9]
議題: https://github.com/apache/opendal/issues/4848
[10]
apache/opendal-go-services: https://github.com/apache/opendal-go-services
[11]
基準測試: https://github.com/apache/opendal/blob/main/bindings/go/tests/behavior_tests/benchmark_test.go
[12]
跟蹤問題: https://github.com/apache/opendal/issues/4892
[13]
@Xuanwo: https://github.com/Xuanwo
[14]
@JupiterRider: https://github.com/JupiterRider
[15]
對 Windows 和 macOS 的支持: https://github.com/JupiterRider/ffi/issues/3
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/kFDzKo_i7B3HFZkTlDP4Zw