在 WSL 中学习 Rust FFI

博主最近从新学习 Rust FFI 的使用,但是手头上没有可用的 Linux 环境(Windows 编译c太麻烦了),于是就尝试着使用 WSL来搭建 Rust 环境和简易的 c 编译环境,并记录下中间遇到的一些坑。感谢 Unsafe Rust 群群友 @框框 对本文的首发赞助!感谢 Rust 深水群 @栗子 的 gcc 指导!

阅读须知

阅读本文,你可以知道:

  • 一些配置 WSL 全局变量的技巧
  • 快速配置 Rust 编译运行环境
  • 简单的 gcc 编译技巧

但是,本文不涉及:

WSL Rust 环境搭建

由于 WSL 是新装的,没有 Rust 和 gcc/g++ 环境,因此需要安装:

sudo apt install gcc -y

# 官方脚本
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

但是由于在国内访问 Rust 官方网站会很慢,因此设置镜像到 Windows 环境变量中:

RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup

然后,使用 WSLENV环境变量将上述变量共享到 WSL 中:

WSLENV=RUSTUP_DIST_SERVER:RUSTUP_UPDATE_ROOT

然后重启 WSL 终端,重新执行 Rust 一键脚本。

以下两个项目均来自 《Rust编程之道》一书,源代码仓库在这里

Rust 调用 C/C++

Rust 调用 C/C++ 代码可以使用 cc crate 配合 build.rs 预先编译好 C/C++ 的程序提供给 Rust 调用。

首先,创建一个 binary 项目:

cargo new --bin ffi_learn

项目目录结构如下:

cpp_src
    |-- sorting.h
    |-- sorting.cpp
src
    |-- main.rs
Cargo.toml
build.rs

然后编写 sorting.hsorting.cpp:

// sorting.h
#ifndef __SORTING_H__
#define __SORTING_H__ "sorting.h"
#include <iostream>
#include <functional>
#include <algorithm>
#ifdef __cplusplus
extern "C" {
#endif

void interop_sort(int[], size_t);

#ifdef __cplusplus
}
#endif
#endif
// sorting.cpp
#include "sorting.h"

void interop_sort(int numbers[], size_t size) {
    int* start = &numbers[0];
    int* end = &numbers[0] + size;

    std::sort(start, end, [](int x, int y) { return x > y; });
}

然后给 Cargo.toml[build-dependecies] 加上 cc crate 依赖:

# Cargo.toml
# 其他配置

[build-dependencies]
cc = "1"

接着,我们通过 cc 调用对应平台的c/c++编译器,因为我们这个项目是 WSL,所以和调用我们刚安装的 gcc:

// build.rs
// Rust 2018 不需要 extern crate 语句

fn main() {
    cc::Build::new()
        .cpp(true)
        .warnings(true)
        .flag("-Wall")
        .flag("-std=c++14")
        .flag("-c")
        .file("cpp_src/sorting.cpp")
        .compile("sorting");    // sorting.so
}

接着,我们在 Rust 主程序中,通过 extern 块引入sorting.cpp中的interop_sort函数,并调用它:

// main.rs
#[link(name = "sorting", kind = "static")]
extern "C" {
    fn interop_sort(arr: *mut i32, n: u32);
}

pub fn sort_from_cpp(arr: &mut [i32]) {
    unsafe {
        // 传入必然有效的数组引用,并通过传入数组的长度来保证不会出现越界访问,从而保证函数内存安全
        interop_sort(arr as *mut [i32] as *mut i32, arr.len() as u32);
    }
}

fn main() {
    let mut my_arr: [i32; 10] = [10, 42, -9, 12, 8, 25, 7, 13, 55, -1];
    println!("Before sorting...");
    println!("{:?}\n", my_arr);

    sort_from_cpp(&mut my_arr);

    println!("After sorting...");
    println!("{:?}\n", my_arr);
}

然后执行调用:

$ cargo run
   Compiling ffi_learning v0.1.0 (/mnt/c/Users/huangjj27/Documents/codes/ffi_learning)
warning: `extern` block uses type `[i32]`, which is not FFI-safe
 --> src/main.rs:3:26
  |
3 |     fn interop_sort(arr: &[i32], n: u32);
  |                          ^^^^^^ not FFI-safe
  |
  = note: `#[warn(improper_ctypes)]` on by default
  = help: consider using a raw pointer instead
  = note: slices have no C equivalent

    Finished dev [unoptimized + debuginfo] target(s) in 4.71s
     Running `target/debug/ffi_learn`
Before sorting...
[10, 42, -9, 12, 8, 25, 7, 13, 55, -1]

After sorting...
[55, 42, 25, 13, 12, 10, 8, 7, -1, -9]

我们看到,该函数提示我们 C 中并没有等价于 Rust slice 的类型,原因在于如果我们传递 slice,那么在 C/C++ 中就很容易访问超过数组长度的内存,造成内存不安全问题。但是,我们在 Rust 调用的时候,通过同时传入数组 arr 的长度 arr.len(), 来保证函数不会访问未经授权的内存。不过在实践中,应该划分模块,只允许确认过 内存安全的 safe Rust 功能跨越模块调用。

在 C/C++ 中调用 Rust

接下来我们反过来互操作。项目结构如下:

c_src
    |-- main.c
src
    |-- lib.rs
    |-- callrust.h
Cargo.toml
makefile

然后配置 Rust 生成两种库——静态库(staticlib)和c动态库(cdylib):

# Cargo.toml
# ...

[lib]
name = "callrust"   # 链接库名字
crate-type = ["staticlib", "cdylib"]

然后添加我们的 Rust 函数:

#![allow(unused)]
fn main() {
// lib.rs

// `#[no_mangle]` 关闭混淆功能以让 C 程序找到调用的函数
// `extern` 默认导出为 C ABI
#[no_mangle]
pub extern fn print_hello_from_rust() {
    println!("Hello from rust");
}
}

当然,为了给 C 调用我们还需要编写一个头文件:

// callrust.h
void print_hello_from_rust();

在我们的 main.c 中库并调用:

// main.c
#include "callrust.h"
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main(void) {
    print_hello_from_rust();
}

编写 makefile,先调度cargo 编译出我们需要的 Rust 库(动态或链接),然后再运行:

GCC_BIN ?= $(shell which gcc)
CARGO_BIN ?= $(shell which cargo)

# 动态链接 libcallrust.so
share: clean cargo
	mkdir cbin
	$(GCC_BIN) -o ./cbin/main ./c_src/main.c -I./src -L./target/debug -lcallrust

	# 注意动态链接再运行时也需要再次指定 `.so` 文件所在目录,否则会报错找不到!
	LD_LIBRARY_PATH=./target/debug ./cbin/main

# 静态链接 libcallrust.a
static: clean cargo
	mkdir cbin

	# libcallrust.a 缺少了一些pthread, dl类函数,需要链接进来
	$(GCC_BIN) -o ./cbin/main ./c_src/main.c -I./src ./target/debug/libcallrust.a -lpthread -ldl
	./cbin/main

clean:
	$(CARGO_BIN) clean
	rm -rf ./cbin

cargo:
	$(CARGO_BIN) build

小结

本文通过给出两个简单的示例来展示 Rust 通过 FFI 功能与 C/C++ 生态进行交互的能力, 并且指出几个在实践过程中容易浪费时间的坑:

  1. WSL的环境变量不生效 -> 使用 WSLENV 变量从 Windows 引入使用。
  2. make share 的时候提示 libcallrust.so 找不到 -> 需要在运行时指定 LD_LIBRARY_PATH 变量,引入我们编译的 libcallrust.so 路径。
  3. make static的时候遇到了pthread_* dy*系列函数未定义问题 -> 通过动态链接系统库来支持运行。