Rustgo: 用Golang轻轻松松调用Rust

原文RUSTGO: CALLING RUST FROM GO WITH NEAR-ZERO OVERHEAD
作者:Filippo Valsorda
翻译:雁惊寒

摘要:本文介绍了在Go中调用Rust代码这个实验。你无需知道Rust或者编译器的内部原理,只需知道链接器有什么用即可。以下是译文

Go语言完美支持直接调用汇编程序。stdlib中的很多快速加密代码都是使用精心优化过的汇编语言编写的,速度是优化前的20倍以上。

但是,编写汇编代码很难,检查汇编代码更难。如果我们可以用更高级的语言编写这些热门函数就好了。

本文介绍了在Go中调用Rust代码这个实验。你无需知道Rust或者编译器的内部原理,只需知道链接器有什么用即可。

为什么是 Rust

坦率地讲:我对Rust并不熟悉,也并不觉得自己用Rust进行日常编程是被迫的。然而,我知道Rust是一个可调和可优化的语言,并且比汇编更易阅读。(事实上任何一个语言都比汇编更容易阅读!)

Go一直在努力寻找自己擅长的地方,但它只接受自己速度足够快这个特点。我很喜欢它这个特点。但对于我们今天要做的工作,我们需要有一种语言,它能在手动关闭了安全检查的情况下生成完全基于栈的函数。

因此,如果存在一种语言,我们能够像约束汇编一样约束它,并能像汇编一样进行优化,那它可能就是Rust。

最后,Rust安全性高,更新频繁,尤其存在着一个很不错的高性能Rust加密代码生态系统。

为什么不是 cgo

Go具备外部函数接口(Foreign Function Interface, FFI)机制,名叫cgocgo允许Go程序以最自然的方式调用C函数(但其实一点都不自然)。

通过使用C的应用程序二进制接口(Application Binary Interface, ABI)作为FFI的通用语言,我们可以在任何语言中调用其他任何语言:Rust可以编译成一个暴露C接口的库,然后cgo就可以使用它了。这很尴尬,但确实有效。

我们甚至可以使用reverse-cgo把Go编译到C库中,供其他任意一个语言调用,例如在这篇文章中描述的那样

但是,cgo为了实现这个功能做了很多事情:它为C的生存生成了一个完整的栈,这使得在Go回调中存在一定的延迟……这简直可以写一篇文章了。

因此,每一次cgo调用的性能成本对于我们这个例子来说实在太高了。

将它们链接在一起

所以我的想法是:如果我们可以让Rust代码像汇编一样受到约束,我们应该就能够想汇编一样使用它,直接调用它。也许还要用一点点胶水。

我们没有必要在中间表示层工作,因为Go编译器从Go 1.3版本开始就能在链接之前将代码和高级汇编转换为机器码了。

这是“外部链接”机制所决定的,该机制就是使用系统链接器将代码和高级汇编组合成一个Go程序。同时,这也是cgo的工作原理:它使用C编译器编译C,使用Go编译器编译Go,然后使用clanggcc将这两者链接在一起。我们甚至可以使用CGO_LDFLAGS将标记传递给链接器。

在cgo安全特性的底层能找到一个跨语言的函数调用。

如果我们可以弄清楚如何在不给编译器打补丁的情况下做到这一点就好了。首先,我们来搞清楚如何将Go程序与Rust文件链接到起来。

除了使用#cgo指令之外,我找不到一种使用go build命令链接到外部blob的体面方式。但是,调用cgo会使得用C编译来处理.s文件,而是Go编译器来处理

值得庆幸的是go/build只是一个前端工具,其他什么都没做! Go提供了一套低级工具可以用来编译链接程序,go build只是收集文件并调用这些工具。我们可以使用-x标志来跟踪编译链接过程。

我通过在cgo构建中增加-x -ldflags "-v -linkmode=external '-extldflags=-v'"来构建下面这个简单的 Makefile。

rustgo: rustgo.a  
        go tool link -o rustgo -extld clang -buildmode exe -buildid b01dca11ab1e -linkmode external -v rustgo.a

rustgo.a: hello.go hello.o  
        go tool compile -o rustgo.a -p main -buildid b01dca11ab1e -pack hello.go
        go tool pack r rustgo.a hello.o

hello.o: hello.s  
        go tool asm -I "$(shell go env GOROOT)/pkg/include" -D GOOS_darwin -D GOARCH_amd64 -o hello.o hello.s

这个Makefile文件将编译出一个由Go文件(hello.go)和Go汇编文件(hello.s)组成的简单主程序包。

现在,如果我们要链接一个Rust对象,我们首先要将其构建为一个静态库……

libhello.a: hello.rs  
        rustc -g -O --crate-type staticlib hello.rs

……然后告诉外部链接器将它们链接在一起。

rustgo: rustgo.a libhello.a  
        go tool link -o rustgo -extld clang -buildmode exe -buildid b01dca11ab1e -linkmode external -v -extldflags='-lhello -L"$(CURDIR)"' rustgo.a
$ make
go tool asm -I "/usr/local/Cellar/go/1.8.1_1/libexec/pkg/include" -D GOOS_darwin -D GOARCH_amd64 -o hello.o hello.s  
go tool compile -o rustgo.a -p main -buildid b01dca11ab1e -pack hello.go  
go tool pack r rustgo.a hello.o  
rustc --crate-type staticlib hello.rs  
note: link against the following native artifacts when linking against this static library

note: the order and any duplication can be significant on some platforms, and so may need to be preserved

note: library: System

note: library: c

note: library: m

go tool link -o rustgo -extld clang -buildmode exe -buildid b01dca11ab1e -linkmode external -v -extldflags="-lhello -L/Users/filippo/code/misc/rustgo" rustgo.a  
HEADER = -H1 -T0x1001000 -D0x0 -R0x1000  
searching for runtime.a in /usr/local/Cellar/go/1.8.1_1/libexec/pkg/darwin_amd64/runtime.a  
searching for runtime/cgo.a in /usr/local/Cellar/go/1.8.1_1/libexec/pkg/darwin_amd64/runtime/cgo.a  
 0.00 deadcode
 0.00 pclntab=166785 bytes, funcdata total 17079 bytes
 0.01 dodata
 0.01 symsize = 0
 0.01 symsize = 0
 0.01 reloc
 0.01 dwarf
 0.02 symsize = 0
 0.02 reloc
 0.02 asmb
 0.02 codeblk
 0.03 datblk
 0.03 sym
 0.03 headr
 0.06 host link: "clang" "-m64" "-gdwarf-2" "-Wl,-headerpad,1144" "-Wl,-no_pie" "-Wl,-pagezero_size,4000000" "-o" "rustgo" "-Qunused-arguments" "/var/folders/ry/v14gg02d0y9cb2w9809hf6ch0000gn/T/go-link-412633279/go.o" "/var/folders/ry/v14gg02d0y9cb2w9809hf6ch0000gn/T/go-link-412633279/000000.o" "-g" "-O2" "-lpthread" "-lhello" "-L/Users/filippo/code/misc/rustgo"
 0.34 cpu time
12641 symbols  
5764 liveness data  

跳转到Rust中

好了,链接成功了,下面我们需要在Go代​​码中以某种方式调用Rust函数了。

我们知道如何在Go中调用Go函数。在汇编中,调用函数是这样的:CALL hello(SB),其中SB是与所有的全局符号有关的虚拟寄存器。

如果想要在Go中调用一个汇编函数,就要在代码中加上func hello()(无需函数体),以便让编译器知道这个函数的存在。

我尝试了上述的所有的方法来调用外部(Rust)函数,但都提示找不到符号名称或函数体。

但是在某一天,cgo终于以某种方式成功调用了这个外部函数!怎么做到的呢?

几天之后,我偶然间发现了答案

//go:cgo_import_static _cgoPREFIX_Cfunc__Cmalloc
//go:linkname __cgofn__cgoPREFIX_Cfunc__Cmalloc _cgoPREFIX_Cfu
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值