在 WSL 中学习 Rust ffi

Posted 跨链技术践行者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在 WSL 中学习 Rust ffi相关的知识,希望对你有一定的参考价值。

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

阅读须知

阅读本文,你可以知道:

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

但是,本文不涉及:

WSL Rust 环境搭建

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

1
2
3
4
sudo apt install gcc -y

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

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

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

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

1
WSLENV=RUSTUP_DIST_SERVER:RUSTUP_UPDATE_ROOT

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

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

Rust 调用 C/C++

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

首先,创建一个 binary 项目:

1
cargo new --bin ffi_learn

项目目录结构如下:

1
2
3
4
5
6
7
cpp_src
    |-- sorting.h
    |-- sorting.cpp
src
    |-- main.rs
Cargo.toml
build.rs

然后编写 sorting.h 和 sorting.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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

1
2
3
4
5
6
7
8
9
// 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 依赖:

1
2
3
4
5
# Cargo.toml
# 其他配置

[build-dependencies]
cc = "1"

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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");
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[link(name = "sorting", kind = "static")]
extern "C" {
    fn interop_sort(arr: &[i32], n: u32);
}

pub fn sort_from_cpp(arr: &mut [i32]) {
    unsafe {
        // 通过传入 数组的长度来保证不会出现越界访问,从而保证函数内存安全
        interop_sort(arr, 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);
}

然后执行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ 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

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

1
2
3
4
5
6
7
c_src
    |-- main.c
src
    |-- lib.rs
    |-- callrust.h
Cargo.toml
makefile

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

1
2
3
4
5
6
# Cargo.toml
# ...

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

然后添加我们的 Rust 函数:

1
2
3
4
5
6
7
8
// lib.rs

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

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

1
2
// callrust.h
void print_hello_from_rust();

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

1
2
3
4
5
6
7
8
9
// main.c
#include "callrust.h"
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main(void) {
    print_hello_from_rust();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GCC_BIN ?= $(shell which gcc)
CARGO_BIN ?= $(shell which cargo)

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

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

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

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

clean:
true$(CARGO_BIN) clean
truerm -rf ./cbin

cargo:
true$(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*系列函数未定义问题 -> 通过动态链接系统库来支持运行。

以上是关于在 WSL 中学习 Rust ffi的主要内容,如果未能解决你的问题,请参考以下文章

抓狂!当 Rust 从 C FFI 调用时,没有产生线程

如何在 Rust 的 FFI 中使用 C 预处理器宏?

为啥 Rust 函数和 FFI C++ 函数以相反的顺序执行?

如何创建 Rust 回调函数以传递给 FFI 函数?

如何将 C 字符串转换为 Rust 字符串并通过 FFI 转换回来?

如何将 Rust `Vec<T>` 暴露给 FFI?