Skip to main content

Rust 调用 Go 代码

· 8 min read

事情的起因是这样的。最近有个 rust 的程序, 想在 rust 代码中读取 minio (一种开源且兼容 AWS s3 api 的对象存储)中的文件, 但是无奈 rust 的 api 很不完善 且似乎现在也没啥维护(有一年没有更新了)。而另外一边,minio 的 go api 开发十分活跃且是最优先支持的。 因此我就想能不能让 rust 调用 go 的 minio api 来实现 minio 中的对象读取。

虽然网络上面似乎也没太多相关的教程,但实际上也很容易做到。Rust 是一门面向底层开发的语言,其提供来很好的与 C 语言的互操作能力。 因此可以将 Go 代码中的相关函数导出为 C 语言的头文件声明,然后让 rust 像调用 C 一样间接地调用 Go 代码。

Step 1: Go 代码

为例能够将 Go 代码导出为 C 语言的头文件,我们需要添加import "C" 和对应函数前面添加export xxx的注释。 如:

api.go
package main

import "C"
import (
"fmt"
)

//export ReadMinioFile
func ReadMinioFile(path *C.char)
fmt.Println("Hello " + C.GoString(path));
}

func main() {

}

这里还需要注意:1. 包名称必须为 main; 2. 必须有一个 main 函数(里面内容可以为空)。

为了进一步标准化,我们最好还需要在 Go 代码的同一目录下初始化一个 go.mod 文件,并指定包名。

# remember to change to your package name
go mod init github.com/misa-md/md-tools/src/ans/minio

然后就可以将 Go 代码编译为一个静态或者动态库,并生成对应的 .h 头文件了。例如静态库:

$ go build --buildmode=c-archive -o libapi.a
$ ls
api.go go.mod go.sum libapi.a libapi.h

关于这部分导出 go 函数为 C 头文件的更多细节,可以查看网上的其他教程或文档 (如:https://medium.com/learning-the-go-programming-language/calling-go-functions-from-other-languages-4c7d8bcc69bf )。

Step2: Rust call C

第二部分,就是让 rust 调用 C。这部分也有很多相关的文档可以参考:

由于我们需要调用的函数的实现并非在 rust 端,所以我们用 rust 的 extern 关键字来声明这些外部函数(这些声明称为 FFI bindings)。 这里有两种方式:第一种是手动根据 C 的头文件来手写这些函数声明 (主要是注意 C 和 Rust 的类型对应), 这种方式对于简单的几个函数的情况可以适用,但函数多了很很费时费力且容易出错;第二种是使用 bindgen 工具生成 Rust 的 FFI bindings。

bindgen 工具有两种形式,一种是 library API, 另一种是 executable command-line API。 前者可以用于在 build.rs 文件中自动生成 FFI bindings,后者用于命令行下生成 FFI bindings。 例如采用后者的方式:

bindgen minio/libapi.h -o api_bindings.rs

然后就可以利用文件 libminio_api.rs 来实现对 Go 代码对调用了。
这种方式虽然简单,但是还是需要手动执行命令,我们希望可以自动化完成。幸运的是,cargo 提供了 build.rs 文件,可以让我们将代码生成步骤放进 build.rs 文件中。下面将介绍这种方式

Step3: 代码生成自动化

我们打开 cargo 工程目录 build.rs 文件,向里面添加如下内容:

build.rs
extern crate bindgen;

use std::env;
use std::path::PathBuf;
use std::process::Command;

fn main() {
println!("cargo:rerun-if-changed=src/ans/minio/api.go");

// run `go build --buildmode=c-archive -o /path/to/save/libapi.a`
let lib_out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
let mut cmd = Command::new("go");
cmd.current_dir("src/ans/minio")
.envs(env::vars())
.args(&["build", "--buildmode=c-archive", "-o", format!("{}/{}", lib_out_path.display(), "libapi.a").as_str()]);

let status = match cmd.status() {
Ok(status) => status,
Err(e) => panic!(format!("failed to execute command: {:?}\nerror: {}", cmd, e)),
};
assert!(status.success());

// see https://github.com/golang/go/issues/11258 if there is linking error
println!("cargo:rustc-link-search={}", lib_out_path.display());
println!("cargo:rustc-link-lib=static=api");

// Configure and generate bindings.
let bindings = bindgen::Builder::default()
.header(format!("{}/{}", lib_out_path.display(), "libapi.h").as_str())
.generate()
.expect("unable to generate bindings");

// Write the generated bindings to an output file.
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings.write_to_file(out_path.join("api_bindings.rs"))
.expect("Couldn't write bindings!");
}

在 build.rs 的 main 函数中:

  • 通过 cargo:rerun-if-changed 告诉 cargo, 当 go 代码改变时,build.rs 会重新编译并执行。
  • 随后当代码块,即是利用std::process::Command来执行 go build 命令(并指定在哪个目录下执行命令),以生成静态库 libapi.a 和对应当 .h 头文件。
    其中生成当静态库和头文件位于变量 lib_out_path 指定的 out 目录 (位于构建目录下的一个out目录中),这样生成当代码就不会和程序代码混在一块了。
  • 中间的两个 println! 宏:cargo:rustc-link-search 指定链接时,库 libapi.a 的搜索路径;cargo:rustc-link-lib 指定链接库的类型(statis/dylib) 和名称。
  • bindgen: 最后采用 bindgen 从 .h 头文件生成 rust 的 FFI bindings。生成的 FFI bindings 文件 api_bindings.rs 也同样放在 out 目录下。

这里,最后一步是将代码放置到构建目录下的out目录,对应 rust 程序使用可能会不方便引入。 因此,我们在 rust 工程的代码目录中,建立一个文件 minio_api.rs,将 FFI bindings 包含进来:

minio_api.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

include!(concat!(env!("OUT_DIR"), "/api_bindings.rs"));

至此,我们将 step1 的 go build 步骤和第二步的步骤都放到了 build.rs 文件中,实现的代码生成的自动化,且能够依据文件变化自动更新。

One more thing

我们在 Go 代码中,可能会调用一些 C 的函数,如 C.free 函数 (即 C 的 free 函数),需要引入一些头文件,如 stdlib,h

api.go
package main

/*
#include <stdlib.h>
*/
import "C"

//export ReleaseMinioFile
func ReleaseMinioFile(data *C.char) {
defer C.free(unsafe.Pointer(data)) release memory
}

func main() {
}

这样会导致一个小问题,后面执行 bindgen 时,会将 stdlib.h 里面的各种函数和类型声明也转化为 FFI bindings, 放到生成的 rust 中,导致生成的 rust 文件非常冗长 (有3000多行,如果没有加入 stdlib.h 则只有300行左右)。
如果我们仅仅使用标准库头文件中的少数的几个函数,可以不引入头文件,仅声明一下即可: 如:

api.go
package main

/*
void free (void* ptr);
*/
import "C"
...