前言
受到2022年“谷歌使用Rust重写Android系统且所有Rust代码的内存安全漏洞为零” [1] 的启发,最近笔者怀着浓厚的兴趣也顺应Rust 的潮流,尝试着将一款C语言开发的基础软件转化为 Rust 语言。本文的主要目的是通过记录此次转化过程中遇到的比较常见且有意思的问题以及解决此问题的方法与大家一起做相关的技术交流和讨论。
问题描述
本文将记录转化过程中遇到的另外一个问题。该问题是由已经转化完成的 Rust 代码使用到软件中引入的第三方软件包和链接库所导致的。设想这样一个场景:Rust 项目中完成某一个功能点需要用到一个或多个第三方软件包和链接库。这显然是很常见的用户场景,但是由于用户环境不同,用户安装的第三方软件包和链接库的版本不同,使得转化后的 Rust 代码必须要做适当的兼容处理。
这里所说的用户的环境不同,可以理解为芯片指令集的平台不同,如 Intel x86 以及国产的 ARM 麒麟服务器。当然更常见的情形是芯片平台相同,但是存在操作系统层面第三方软件包和链接库安装的差异,如 x86 下的 Ubuntu 和 CentOS 中用户安装了不同版本的第三方软件包和链接库等。
事实上,即使排除所有平台和系统层面的差异,由于用户安装了该基础软件所依赖的不同版本的第三方软件包和链接库,然而这些第三方软件包或者链接库由于自身的演进导致不同版本之间存在较大差异(可能实现相同功能的函数和函数签名都有千差万别),这给我重写该软件的工作带来了一些挑战。基于上述说明,在完成重写该基础软件的过程中如何使得转化后的 Rust 代码能兼容该基础软件所依赖的主流第三方软件包和链接库则是我遇到的最大挑战。需要说明的是这里的第三方软件包和链接库可能是基于 Rust 语言开发的,也可能是基于 C 语言开发的。
解决方案
对于此问题的解决方案需要使用 Rust FFI(Foreign Function Interface) [1],这基本上是没有太大争议的。因为在本次软件重写过程中我遇到的场景是:对于不同版本的链接库使用哪个版本的函数取决于用户的安装运行时环境,所以除了 Rust FFI,在代码适配上我还考虑了使用 Rust features [2] 机制。
下面我简化了一下场景和解决方案,同时我把样本代码放到了我的 github [3] 里,欢迎大家一起交流。如样本代码所示,my-rust-bin
文件夹中的一段业务代码需要调用到静态链接库 my_rust_lib
中的函数,该链接库有两个版本 v1
(在文件夹 my-rust-lib-v1
中) 和 v2
(在文件夹 my-rust-lib-v2
中), 且不同版本的库其函数不一样。
my-rust-lib-v1 对应的业务函数为:pub fn my_rust_lib_v1(left: usize, right: usize) -> usize
my-rust-lib-v2 对应的业务函数为:pub fn my_rust_lib_v2(left: usize, right: usize) -> usize
另外一个 lib
文件夹的目的其实是为了模拟用户本地安装的链接库。可以分别编译不同版本的静态链接库,然后把生成的库文件(在本例中是)libmy_rust_lib.a
, 然后把不同版本的库文件拷贝到此文件夹下,以此来模拟用户环境中安装的不同版本的链接库。解决方案中的关键点在于 my-rust-bin
中,
首先在 my-rust-bin
的 Cargo.toml
中有定义对应的 features,如下所示:
[features]
v1 = []
v2 = []
其次在 my-rust-bin
的 src/main.rs
下的代码如下:
#[cfg(feature = "v1")]
mod bindingmylib {
extern "C" {
pub fn my_rust_lib_v1(left: usize, right: usize) -> usize;
}
}
#[cfg(feature = "v2")]
mod bindingmylib {
extern "C" {
pub fn my_rust_lib_v2(left: usize, right: usize) -> usize;
}
}
#[cfg(not(any(feature = "v1", feature = "v2")))]
compile_error!("Please specify either 'v1' or 'v2' feature");
pub fn my_rust_lib(left: usize, right: usize) -> usize {
#[cfg(feature = "v1")]
unsafe {
return bindingmylib::my_rust_lib_v1(left, right);
}
#[cfg(feature = "v2")]
unsafe {
return bindingmylib::my_rust_lib_v2(left, right);
}
}
fn main() {
let r_value: usize = my_rust_lib(3, 5);
println!("The return value of my_rust_lib is [{}]", r_value);
}
现在我来解读一下这段代码。代码先分别定义一个相同的模块 bindingmylib
,然后根据 features
分别引入的依赖,使用的不同的静态链接库函数(my_rust_lib_v1
和 my_rust_lib_v2
), 同时通过 compile_error!
定义一个没有设置 v1 和 v2 features 的编译错误(防止编译时忘记设置 features选项,下面在编译环节的时候有用)。最后将两个有差异的函数统一为函数 my_rust_lib
,并在该函数中根据 features 定义分别调用不同的函数并返回相应的值。
最后是在 my-rust-bin
中编译二进制文件:
编译并运行 v1 的二进制文件
# 编译 v1 版本的 my-rust-bin
$ cd my-rust-bin
$ cargo build --features="v1"
# 运行 v1 版本的 my-rust-bin
$ target/debug/my-rust-bin
my_rust_lib_v1: 8
The return value of my_rust_lib is [8]
编译并运行 v2 的二进制文件
# 编译 v2 版本的 my-rust-bin
$ cd my-rust-bin
$ cargo build --features="v2"
# 运行 v2 版本的 my-rust-bin
$ target/debug/my-rust-bin
my_rust_lib_v2: 8
The return value of my_rust_lib is [8]
备注:如果编译的时候没有设置 --features
则会有如下输出:
$ cargo build
error: Please specify either 'v1' or 'v2' feature
--> src/main.rs:16:1
|
16 | compile_error!("Please specify either 'v1' or 'v2' feature");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
至此,用户在编译好该基础软件之后,就可以无感知的通过统一的函数入口调用不同版本的相同链接库中的不同函数了。
总结
本文主要是在简化了问题的实际场景以后,解决不同版本的同一软件包或者链接库中,函数及其函数签名不同导致的调用问题。之所以说简化,主要是本文所描述的场景中,my-rust-bin
和其依赖的外部链接库均是 Rust 编写。而在我的实际场景中则会更复杂一些,存在着 Rust 代码依赖 C 编写的外部链接库,同时存在混合的原来 C 代码部分依赖新改写的 Rust 外部链接库的情况。但是无论哪种情况,万变不离其宗,我们都可以从这种最简单的场景出发去解决遇到的问题。
关于作者
张怀龙曾就职于阿尔卡特朗讯,百度,IBM等企业从事云计算研发相关的工作。目前就职于 Intel 中国,担任云原生开发工程师并致力于云原生、服务网格等技术领域研究实践,也是Istio 的maintainer的开发者。曾多次在 KubeCon、ServiceMeshCon、IstioCon、GOTC 和 InfoQ/QCon 等大会上发表演讲。
关联博客
一次Rust重写基础软件的实践(一)
参考文档
[1] https://doc.rust-lang.org/nomicon/ffi.html
[2] https://doc.rust-lang.org/cargo/reference/features.html
[3] https://github.com/zhlsunshine/rust-lib-with-multi-versions-example