在 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?
- 如何解决 WSL 中文乱码问题?
顺带一提的是,博主通过 VS Code 使用 WSL,因为 Win 10 已经配置成 UTF-8 编码,所以并没有出现乱码问题 - Rustup 国内镜像有哪些?
- cargo 详细使用教程
- 甚至不会讲 Rust FFI 是什么
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++ 生态进行交互的能力, 并且指出几个在实践过程中容易浪费时间的坑:
- WSL的环境变量不生效 -> 使用
WSLENV
变量从 Windows 引入使用。 make share
的时候提示libcallrust.so
找不到 -> 需要在运行时指定LD_LIBRARY_PATH
变量,引入我们编译的libcallrust.so
路径。make static
的时候遇到了pthread_*
dy*
系列函数未定义问题 -> 通过动态链接系统库来支持运行。
以上是关于在 WSL 中学习 Rust ffi的主要内容,如果未能解决你的问题,请参考以下文章
为啥 Rust 函数和 FFI C++ 函数以相反的顺序执行?