【Rust2】打包、混合编程



2022年08月28日    Author:Guofei

文章归类: 合集    文章编号: 11202

版权声明:本文作者是郭飞。转载随意,但需要标明原文链接,并通知本人
原文链接:https://www.guofei.site/2022/08/28/rust2.html


https://crates.io/

// 引用
use std::fmt::Result;
use std::io::Result as IoResult;

// 引用多个
use std::{cmp::Ordering, io};

use std::io::{self, Write};

如何引入包

Cargo.toml 中引入包

[dependencies]
rand = "0.8.3"

如果依赖的项目不在 crates.io 上

[dependencies]
# 依赖的项目在本地
your_crate_name = { path = "/path/to/your/crate" }

# 依赖的项目在托管文件上(比如GitHub、GitLab等)
your_crate_name = { git = "https://your.git.repo/url.git" }
# 可以指定特定分支
your_crate_name = { git = "https://your.git.repo/url.git", branch = "develop" }
# 可以指定特定的提交
your_crate_name = { git = "https://your.git.repo/url.git", rev = "a1b2c3d" }

注意事项:当你使用本地路径或Git依赖时,每次构建项目都会检查这些依赖,所以确保你的本地路径或Git仓库是可访问的。

如何做包

pub mod mod_name1 {
    pub mod mod_name1_1 {
        pub fn func1() {
            println!("run: func1");
        }
    }
}

pub fn main() {
    mod mod_name {
        pub mod mod_name2 {
            pub fn func1() {
                println!("run: func1 in self")
            }
        }
    }

    // 可以有多个子模块
    mod mod_name3 {
        // 这里没加pub,下面就不能调用
        fn func3() {}
    }

    pub fn func4() {
        // 绝对路径
        crate::mod_name1::mod_name1_1::func1();

        // 相对路径
        mod_name::mod_name2::func1();
    }

    func4();
}
  • 不加 pub 则默认私有

多文件

参考:https://github.com/guofei9987/rs_lib

目录结构

my_project/
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── lib.rs
│   ├── main.rs
│   └── my_mod1.rs
└── tests
    └── test_file1.rs

Cargo.toml

[package]
name = "my-module"
version = "0.1.0"
edition = "2021"
authors = ["Guo Fei <me@guofei.site>"]
description = "一段描述"
license = "MIT"

[dependencies]

[lib]
name = "my_module"
path = "src/lib.rs"
# crate_type = ["dylib"] # "lib", "cdylib"

my_mod1.rs

pub mod mod_in_file {
    pub fn my_func1() -> i32 {
        println!("运行 my_func1");
        return 0;
    }
}

lib.rs

// 关联 my_mod1.rs,然后才能 use
pub mod my_mod1;
pub use crate::my_mod1::mod_in_file::my_func1;

// lib.rs 也可以有函数
pub fn my_func2() {
    println!("my_mod1!");
    my_func1();
    crate::my_mod1::mod_in_file::my_func1();
}

main.rs 里面这样引用

// main.rs
use my_module::{my_func1, my_func2};

fn main(){
    my_func1();
    my_func2();
}

测试

tests 文件夹下

use my_module::{my_func1, my_func2};

#[test]
fn func_1() { assert!(true, "可以填入报错信息,也可以不填。"); }

#[test]
fn func_2() { assert_eq!(my_func1(), 0, "两个值相等,使用 == 判断的"); }

#[test]
fn func_3() { assert_ne!(2 + 3, 4, "两个值不相等,使用 != 判断的"); }

// 有 panic
// panic 内容要与 expected 一致,才能通过测试
#[test]
#[should_panic(expected = "index out of bounds: the len is 3 but the index is 9")]
fn func_4() {
    let v = vec![1, 2, 3];
    let a = v[9];
}

#[test]
fn func_5() -> Result<(), String> {
    if true {
        Ok(())
    } else {
        Err(String::from("如果触发 Err,则测试不通过"))
    }
}

#[test]
#[ignore] // 忽略,不会测试
fn func_6() {}

// debug_assert! 和 debug_assert_eq!,只在调试构建中检查断言

说明 如果测试代码没有在 ./tests/ 路径,需要用 #[cfg(test)] 包裹起来

#[cfg(test)]
mod tests {
    #[test]
    fn func_1() { assert!(true, "可以填入报错信息,也可以不填。"); }
}

cargo test 测试

  • 默认使用多线程,如果不想就 cargo test -- --test-threads=1
  • 默认有输出截获,因此 print! 会失效,如果不想,就 cargo test -- --nocapture
  • 测试单个函数 cargo test func_2
    • 支持模糊 cargo test func 会把 func 开头的都跑一遍
    • 可以单独运行某一个文件里面的测试函数 cargo test cargo test --test filename。这个运行 test/filename.rs 里面的所有函数
  • 运行被 ignore 的函数 cargo test -- --ignored
  • 你可以在 test 区域中调用私有函数,进而测试私有函数
  • 标记为#[test] 的函数。在 cargo buildcargo build --release 时会跳过
  • 会自动 单元测试、集成测试、文档测试
  • 如果多个测试文件需要有个公用的mod,可以放到 tests/path/mod.rs 里面。它不会被当作测试用的文件,而是可以作为包。

如何在子目录中做包

src/my_path/mod.rs 。它是 my_path 这个文件夹所有文件的入口

// 标记 my_path/file1.rs 这个文件
mod file1;
// 引用那个文件中的 mod
pub use file1::mod_sub2; // 加 pub 后,外界就也能调它了

// mod.rs 自己也可以有 mod
pub mod my_mod_sub {
    pub fn func2() {
        println!("hello");
    }
}

src/my_path/file1.rs 这样写就可以被引用了:

pub mod mod_sub2 {
    pub fn func3() {
        println!("func3");
    }
}

如何引用同级文件中的内容(src/my_path/mod.rs 引用同级文件上面写了,这是 src/my_path/file2.rs 如何引用同级目录)

// 引用 mod.rs 中的内容
use super::my_mod_sub;
// 引用 file1.rs 中的内容
use super::mod_sub2;

mod mod3 {
    fn func3() {
        println!("func3");
    }
}

打包时怎样包含和排除

基本上不用特意配置,用默认的就可以

[package]

# 排除某些目录
exclude = ["tests/*"]

# 必须加入某些目录
include = ["src/**/*", "resources/*", "Cargo.toml"]

features

cargo.toml:

[features]
# 默认启动的功能
default = ["feature1"]

# 定义依赖关系
feature1 = []
feature2 = ["feature1"]

代码

#[cfg(feature = "feature1")]
pub fn func1(){
    ...
}

知识:

  • #[cfg(feature = "feature1")] 可以标记 mod 、代码块、use
  • 测试代码中,可以用 #[cfg(feature = "feature1")] 把代码块标记起来

用户使用时

[dependencies]
your_library = { version = "0.1.0", features = ["dct", "idct"] }

如果只有 featureA 依赖 library_a,那么这样写:

[dependencies]

# 库A是可选依赖,只有在启用 featureA 时才会被引入
library_a = { version = "1.0", optional = true }

[features]
# 定义 featureA,这个 feature 启用时会依赖库A
featureA = ["library_a"]

性能测试 benchmark

Rust 自带的 benchmark 工具还在 nightly,因此推荐 criterion

Cargo.toml

[dev-dependencies]
criterion = "*"

[[bench]]
name = "my_benchmark" 
harness = false

注意:这个 name = "my_benchmark" 对应的是 ./benches/my_benchmark.rs 这个文件名,这个文件里面放的是性能测试相关的代码,代码如下:

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 1,
        1 => 1,
        n => fibonacci(n-1) + fibonacci(n-2),
    }
}
// 这里的 fib 20 是唯一的测试名,多个测试需要改成不同的名字
fn criterion_benchmark(c: &mut Criterion) {
    c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}

// 如果要测试多个,可以 criterion_group!(benches, criterion_benchmark1,criterion_benchmark1) 等往里面加
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

测试性能的命令

cargo bench

然后结果在 ./target/criterion 里找到

代码规范性

CI 可以这么写:

- name: Build
working-directory: ./rust
run: cargo build --verbose

- name: Run Tests
working-directory: ./rust
run: cargo test --verbose

- name: Check Formatting
working-directory: ./rust
run: cargo fmt -- --check

- name: Lint with Clippy
working-directory: ./rust
run: cargo clippy -- -D warnings

解释:

  • cargo fmt -- --check 如果有代码不符合rust规范的地方,就报错。如果用 cargo fmt 则会直接把不规范的地方修正
  • cargo clippy -- -D warnings 如果有错误、低效、不安全操作,就报错

其它知识

  • std 会隐含的引入到所有程序中,它包含 Vec,Result 等常用的东西
  • src/lib.rs 是整个项目的根

装饰属性

allow:在编译时,禁用某些警告。

// 在编译时,不会报告关于non_camel_case_types关键字的警告
#[allow(non_camel_case_types)]
pub struct git_revspec {
    ...
}

cfg:将条件编译作为一个特型:

// 只在针对安卓编译时包含此模块
#[cfg(target_os = "android")]
mod mobile;
#[cfg(...)] 选项启用场景
test启用测试(当以 cargo testrustc --test 编译时)
debug_assertions启用调试断言(通常用于非优化构建)
unix为 Unix(包括 macOS)编译
windows为 Windows 编译
target_pointer_width = "64"针对 64 位平台。另一个可能值是 “32”
target_arch = "x86_64"针对 x86-64 架构,其他的值还有:“x86”、“arm”、“aarch64”、“powerpc”、“powerpc64” 和 “mips”
target_os = "macos"为 macOS 编译。其他的值还有 “windows”、“ios”、“android”、“linux”、“openbasd”、“netbsd”、“dragonfly” 和 “bitrig”
feature = "robots"启用用户定义的名为 “robots” 的特性(当以 cargo build --feature robotsrustc --cfg feature='"robots"' 编译时)。特性在 Cargo.toml 的 [feature] 部分声明
not(A)A 不满足时要提供一个函数的两个不同实现,将其中一个标记为#[cfg(x)],另一个标记为#[cfg(not(x))]
all(A, B)A 和 B 都满足时,等价于 &&
any(A, B)A 或 B 满足时,等价于 ||
  • #[inline]:对函数的行内扩展,进行一些微观控制。
    • 如果函数或方法在一个包里定义,但在另一个包里调用,那么 Rust 就不会将其在行内扩展。除非它是泛型的(有类型参数)或者明确标记为 #[inline]
    • #[inline(always)],要求每处调用都将函数进行行内扩展。
    • #[inline(never)],要求永远不要行内化。
  • #[cfg]#[allow],可以添加到整个模块中,并应用于其中所有的特性。
  • #[test]#[inline],只能添加到个别特性项。
  • 要将属性添加给整个包,需要在 main.rslib.rs 文件的顶部、任何特性之前添加,而且要用#! 而不是#标记。
    // lib.rs
    #![allow(non_camel_case_types)]  // 可以将属性添加给整个特性项,而不是其后的个别特性项
    pub struct git_revspec {
      ...
    }
    pub struct git_error {
      ...
    }
    
  • #![feature]属性:用于开启 Rust 语言和库的不安全特性。比如一些新增的测试功能。

文档

cargo doc --no-deps --open
# --no-deps:只生成目前包的文档,而不生成它依赖的包的文档
# --open:自动在浏览器打开
# 生成的文档文件保存在 `./target/doc` 中
  • //! 通常放到 src/libs.rs 的头部,代表整个包的文档。也可以用 /*!*/ 把多行包围起来
  • /// 在函数、类的前面,是它们的文档
  • 使用 pub use 把子目录的mod导入到当前目录后,这就让用户不必感知复杂的目录结构,而这个目录结构是为了开发的清晰而做的。
//! # 标题
//!
//! 整个包的文档
//! 按照 Markdown 语法解析

/// func1 的文档
// --略--
fn func1() {}

其它知识

  • Cargo 生成的文档基于库中的 pub 特型以及它们对应的文档注释生成。
  • #[doc] 属性:用来标注文档注释。
  • /// 开头的注释,会被视作#[doc] 属性;
    /// test
    等价于
    #[doc = "test"]
    
  • //! 开头的注释,也会被视作 #[doc] 属性,可以添加到相应的包含特性,通常是模块或包中。
  • 文档注释中的内容会被按照 Mardown 来解析。

文档自动测试

Rust 会将文档注释中的代码块,自动转换为测试。

  • /// #可以隐藏某些代码行。
  • no_run 注解:结合代码块界定符号,可以对特定的代码块禁用测试。
    /// ```no_run
    /// ...
    /// ```
    
  • ignore 注解:不希望测试代码被编译。
    /// ```ignore
    /// ...
    /// ```
    
  • 如果文档注释是其他语言,那么需要使用语言的名字标记。
    /// ```c++
    /// ...
    /// ```
    

自动化生成README

(这个包还不够完善)

cargo install cargo-readme

功能:自动从代码中同步版本号、示例代码,到 Readme.md 中

混合编程

Rust 调用本地的另一个 Rust 项目

[dependencies]
# 调用本地 Rust 项目
my_rust_project1 = { path = "../my_rust_project1" }
# 调用 crates.io 上的项目
rand = "*"

Rust 调用 Rust 编译后的 dylib

编译 project1

  • 默认编译为 *.rlib,这是 Rust 静态库 (Rust Library)
  • 也可以是动态链接库,.so/ .dylib / .dll
// 编译输出一个符合C语言调用规范的动态库文件
// 如果配置为 dylib,则输出是符合 rust 调用规范的动态库,只能在 rust 语言编写的项目之间互相调用,不能跨语言
// name:之后的生成的lib名称跟这个有关
[lib]
name= "my_rust_project1"
crate-type = ["cdylib"]

lib.rs

// no_mangle 避免 Rust 名称修饰。
// pub extern "C" 保证外部可以访问
#[no_mangle]
pub extern "C" fn my_fun1(x: i32, y: i32) -> i32 {
    let z = 2 * x + y;
    println!("{} + {} = {}", x, y, z);
    z
}

// 下面展示这个类如何被外面调用
#[repr(C)]
pub struct MyStruct {
    x: i32,
}

impl MyStruct {
    pub fn new(x: i32) -> Self {
        return Self {
            x
        };
    }

    pub fn add1(&mut self, y: i32) -> i32 {
        self.x += y;
        self.x
    }

    pub fn get_x(&self) -> i32 {
        self.x
    }
}

// 为了外面可以调用
// new 一个类
#[no_mangle]
pub extern "C" fn my_struct_new(x: i32) -> *mut MyStruct {
    Box::into_raw(Box::new(MyStruct::new(x)))
}


#[no_mangle]
pub extern "C" fn my_struct_add1(ptr: *mut MyStruct, y: i32) -> i32 {
    assert!(!ptr.is_null());
    unsafe {
        (*ptr).add1(y)
    }
}

#[no_mangle]
pub extern "C" fn my_struct_get(ptr: *mut MyStruct) -> i32 {
    assert!(!ptr.is_null());
    unsafe {
        (*ptr).get_x()
    }
}


// 释放
#[no_mangle]
pub extern "C" fn my_struct_free(ptr: *mut MyStruct) {
    assert!(!ptr.is_null());
    unsafe {
        Box::from_raw(ptr);
    }
}

编译:cargo build --release。编译后的文件在 /target/release/ 中可以找到

  • cargo build 则出现在 target/debug 多了个 文件
  • (Macbook 是 .dylib,linux是 .so,windows 是 .dll

my_project2 可以调用这个编译后的文件

// libloading = "0.7"
// 下面展示如何调用函数
fn call_dynamic(x: i32, y: i32) -> Result<u32, Box<dyn std::error::Error>> {
    println!("hello");
    // 1. 必须用 unsafe 包起来
    unsafe {
        // 2. 这里用绝对路径。也可以把文件复制到项目目录下,用相对路径
        let lib = libloading::Library::new("/Users/guofei/github/my_project1/target/release/libmy_rust_project1.dylib")?;
        // 3. 注意这个输入值和返回值需要指定一下
        let func: libloading::Symbol<unsafe extern fn(i32, i32) -> u32> = lib.get(b"my_fun1")?;
        Ok(func(x, y))
    }
}


#[test]
fn test1() {
    let _a = call_dynamic(1, 2);
    println!("{}", _a.expect("error"));
}


// 下面展示如何调用一个类
struct MyStruct {
    // 一个指向动态库中结构体的原始指针
    raw: *mut std::os::raw::c_void,
    lib: libloading::Library,
}

impl MyStruct {
    fn new(data: i32) -> Result<MyStruct, Box<dyn std::error::Error>> {
        // 编译后的文件放在 ./libs/ 路径下
        let lib_path = match std::env::consts::OS {
            "macos" => "libs/libmy_rust_project1.dylib",
            "linux" => "libs/libmy_rust_project1.so",
            "windows" => "libs/libmy_rust_project1.dll",
            others => return Err(format!("Unsupported OS {}", others).into())
        };
        let lib = unsafe { libloading::Library::new(lib_path)? };
        unsafe {
            let my_struct_new: libloading::Symbol<extern "C" fn(i32) -> *mut std::os::raw::c_void> = lib.get(b"my_struct_new")?;
            Ok(MyStruct { raw: my_struct_new(data), lib })
        }
    }

    fn add1(&self, value: i32) -> Result<i32, Box<dyn std::error::Error>> {
        unsafe {
            let my_struct_add1: libloading::Symbol<extern "C" fn(*mut std::os::raw::c_void, i32) -> i32> = self.lib.get(b"my_struct_add1")?;
            let res = my_struct_add1(self.raw, value);
            Ok(res)
        }
    }
//     不需要 Drop 方法,因为 这个类拥有 lib 的所有权,会被一起移除
}


#[test]
fn test2() -> Result<(), Box<dyn std::error::Error>> {
    let mut my_struct = MyStruct::new(10)?;
    let res = my_struct.add1(5)?;
    println!("{}", res);
    Ok(())
}

知识点

  1. 用于编译的 Rust 不要返回 Result 类型。Rust 认识它,但是 Python/C 都不认识
  2. 要让 Python 能调用,还需要把 &Vec<u8> 改为 *const u8usize,把 &str 改为 *const c_char

下面展示如何调用一个类(比上面的更灵活,但大多场景没必要)

// 下面展示如何调用一个类
struct MyStruct {
    // 这里我们只存储一个指向动态库中结构体的原始指针
    raw: *mut std::os::raw::c_void,
}

impl MyStruct {
    fn new(lib: &libloading::Library, data: i32) -> Result<MyStruct, Box<dyn std::error::Error>> {
        unsafe {
            let my_struct_new: libloading::Symbol<extern "C" fn(i32) -> *mut std::os::raw::c_void> = lib.get(b"my_struct_new")?;
            Ok(MyStruct { raw: my_struct_new(data) })
        }
    }

    fn add1(&self, lib: &libloading::Library, value: i32) -> Result<i32, Box<dyn std::error::Error>> {
        unsafe {
            let my_struct_add1: libloading::Symbol<extern "C" fn(*mut std::os::raw::c_void, i32) -> i32> = lib.get(b"my_struct_add1")?;
            let res = my_struct_add1(self.raw, value);
            Ok(res)
        }
    }
}

impl Drop for MyStruct {
    fn drop(&mut self) {
        // 你需要确保动态库在调用 free 函数之前没有被卸载
        // 因此可能需要在这里保持对动态库的引用
    }
}

#[test]
fn test2() -> Result<(), Box<dyn std::error::Error>> {
    let lib = unsafe { libloading::Library::new("/Users/guofei/github/my_project1/target/release/libmy_rust_project1.dylib") }?;
    let mut my_struct = MyStruct::new(&lib, 10)?;
    let res = my_struct.add1(&lib, 5)?;
    println!("{}", res);
    // 当 `my_struct` 离开作用域时,需要正确地释放它
    Ok(())
}

Rust 调用 Rust 编译后的静态库 .a

静态库包含所有依赖、需要为每个平台单独编译

静态库优缺点

  • 编译时把所有依赖都包含进去。相比之下动态库时运行时加载。
  • 运行时代码已经在内存中,性能更高、兼容性更好。
  • 最终文件较大
  • 如果多个程序使用同一个静态库,每个程序都有自己的库副本,内存占用大。

做法:

project1 -> project_middle (用来包装 .a 文件) -> project2

project1:

[lib]
crate-type = ["staticlib"]

然后 cargo build --release 得到 .a 文件

project_middle:

// 1. 把 .a 文件放到 ./src 里面
// 2. build.rs 这样写:
use std::env;

fn main() {
    let lib_dir = env::current_dir().unwrap().join("lib");
    println!("cargo:rustc-link-search=native={}", lib_dir.display());
    println!("cargo:rustc-link-lib=static=my_library");
}


// 3. lib.rs 这样写
extern "C" {
    pub fn my_fun1(x: i32, y: i32) -> u32;
}

然后 project2 正常引用 project_middle 即可

Python 调用 Rust :pyo3

项目示例:https://github.com/guofei9987/python_with_rust

共有两个方案,优缺点如下:

  1. 使用 pyo3:
    • 语言集成层: pyo3提供了丰富的Rust和Python之间的类型转换和交互的抽象,这让开发者能够更容易地编写“Python风格”的Rust代码。
    • 安全性和易用性: Rust的所有权和生命周期模型,在pyo3的帮助下,能够保证在与Python交互的过程中不会出现内存安全问题。
    • 构建和打包工具: pyo3与maturin或setuptools-rust等工具结合,可以简化Python扩展模块的构建和分发流程。
    • Python API: 允许直接使用Python的API进行编程,而不是仅仅通过FFI(外部函数接口)调用。
    • 自定义Python模块: 可以创建符合Python模块和扩展标准的包,可以使用import语句在Python代码中直接导入。
  2. 使用 ctypes 加载 Rust 编译后的共享库
    • 手动类型转换: 使用ctypes时,开发者通常需要手动处理类型转换和内存管理,这可能会导致错误,并增加额外的工作量。
    • 简单的FFI边界: 当你需要直接调用少量Rust函数,并且不需要复杂的Rust和Python之间的交互时,ctypes可能是一个简单的解决方案。
    • 少量依赖: 使用ctypes时,你的Python代码不需要依赖于pyo3或其他构建工具,只需要Rust编译出的共享库文件。
    • 跨语言调用开销: 由于你在Python和Rust之间建立的是直接的FFI界面,调用开销可能会比pyo3的高级抽象稍低,但这通常只在极少数性能关键的场景中才显著。

演示 Python 如何调用一个 Rust 项目

  1. Python 传给 Rust 这些数据结构:str, int, float, list, dict 等
  2. Rust 返回给 Python 这些数据结构:String, i32, f64, Vec, Vec
  3. Python 使用 Rust 类

用户在 pip install xxx 时编译

一、文件结构

├── README.md
├── requirements.txt
├── setup.py
├── my_rust_project/
│   ├── Cargo.toml
│   ├── src/
│   │   └── lib.rs
│   └── ...
└── python_with_rust/
    ├── __init__.py
    ├── python_module.py
    └── ...

requirements.txt

setuptools
setuptools-rust

./my_rust_project/Cargo.toml

[dependencies]
pyo3 = { version = "0.20.2", features = ["extension-module"] }

[lib]
name = "my_rust_project"
crate-type = ["cdylib"]

./my_rust_project/src/lib.rs:

use pyo3::prelude::*;

fn my_rust_fn1() {
    println!("调用了 Rust 函数:my_rust_fn1")
}


#[pyfunction]
fn my_rust_fn2() -> PyResult<()> {
    my_rust_fn1();
    println!("调用了 Rust 函数:my_rust_fn2");
    Ok(())
}

#[pymodule]
fn my_rust_project(_py: Python, m: &PyModule) -> PyResult<()> {
    // 这个函数名 my_rust_project 一定要按需修改,否则 import 的时候找不到
    m.add_function(wrap_pyfunction!(my_rust_fn2, m)?)?;
    Ok(())
}

setup.py:

from setuptools_rust import Binding, RustExtension

setup(
    ...
    rust_extensions=[

        RustExtension("python_with_rust.my_rust_project",
                      path="./my_rust_project/Cargo.toml", binding=Binding.PyO3)
    ],
    # python_with_rust 指的是编译后,install 阶段放到哪个目录
    # my_rust_project 指的是 Rust 项目名,与编译后的文件名有关
    # path 用来指明 Rust 项目的位置
)

用户如何使用:

from python_with_rust import my_rust_project

my_rust_project.my_rust_fn2()

发布方式

  • 【常用】发布预编译的 Wheels. 需要针对各种平台预编译,发布的包有几十个。通常用 maturin 和 Github Action 来执行
  • 发布源码 sdist,这要求用户电脑上已经安装了 Rust 工具链。

Python 调用 Rust:Maturin

说明

  • PEP 518 引入了 pyproject.toml,它是比 setup.py 更先进的方式

命令

  • maturin publish 把包发布到 pypi
  • maturin develop 把包安装在本地

借助 Github Action 发布多平台包的方法

  • 配置 API token
    • pypi.org 上新建 API token
    • 打开 GitHub-> 仓库 -> setting,选择 “Secrets and variables” -> “Actions” -> “New repository secret” ,把 Token 黏贴进去
  • 项目结构,参见 python_with_rust
    python_with_rust/
    ├── Cargo.toml
    ├── pyproject.toml
    ├── src/
    │   └── lib.rs
    ├── python_with_rust/
    │   └── __init__.py
    ├── README.md
    
  • 使用 CI 在多平台、多环境分别做预编译,然后向 pypi 发布编译结果

Python 调用 Rust 编译后

import ctypes

my_so = ctypes.cdll.LoadLibrary("liblearn_rust.dylib")
my_so.lib_func()

rust返回array,这样处理

class Int32_4(ctypes.Structure):
  # 这个6是rust返回的 array 的长度
    _fields_ = [("array", ctypes.c_int32 * 6)]


my_so.make_array.restype = Int32_4
tmp = my_so.make_array()
print(tmp.array[:])

参考:

  • https://www.cnblogs.com/pu369/p/15238880.html
  • (使用ffi,没用上)https://zhuanlan.zhihu.com/p/421707475

Python 传递 str,i32,f64

演示包括了

  • Python 向 Rust 传递 str/i32/f64
  • Rust 向 Python 传递 str/i32/f64
#[no_mangle]
pub extern fn rust_func2(inp_str: *const c_char, inp_int: i32, inp_float: f64) -> *mut std::ffi::c_char {
    let cstr_tmp = unsafe { CStr::from_ptr(inp_str) };
    let str1 = str::from_utf8(cstr_tmp.to_bytes()).unwrap();

    println!("{:?},{},{}", str1, inp_int, inp_float);

    // 展示如何返回 f64
    // return inp_float + 5.0;

    // 展示如何返回 string
    let s=CString::new(str1).unwrap().into_raw();
    println!("{:?}",s);
    return s;
}
# 1. 如果返回 f64
# lib.rust_func2.restype = ctypes.c_double
# 2. 如果返回 string
lib.rust_func2.restype = ctypes.c_char_p
res = lib.rust_func2(b"hello", 4, ctypes.c_double(9.4))

参考:

  • https://stackoverflow.com/questions/30312885/pass-python-list-to-embedded-rust-function/30313295#30313295
  • https://stackoverflow.com/questions/31074994/passing-a-list-of-strings-from-python-to-rust

Python 传入list(string或者int)

Rust

// libc = "*"
// 传入list<string>, list<int>,同时要传入他们的长度
#[no_mangle]
pub extern fn rust_func(
    arr_str: *const *const c_char, len_str: size_t, arr_int: *const int32_t, len_int: size_t,
) {

    // 传入的是 list<str>
    let str_tmp = unsafe { slice::from_raw_parts(arr_str, len_str as usize) };
    let strs: Vec<&str> = str_tmp.iter()
        .map(|&p| unsafe { CStr::from_ptr(p) })  // iterator of &CStr
        .map(|cs| cs.to_bytes())                 // iterator of &[u8]
        .map(|bs| str::from_utf8(bs).unwrap())   // iterator of &str
        .collect();
    println!("input array of str = {:?}", strs);


    // 传入的是 list<int>
    let nums = unsafe { slice::from_raw_parts(arr_int, len_int as usize) };
    println!("input array of int = {:?}", nums)
}

Python

import ctypes

lib = ctypes.cdll.LoadLibrary("libproject.dylib")

array_str = [b'blah', b'blah', b'blah', b'blah']
c_array_str = (ctypes.c_char_p * len(array_str))(*array_str)

nums = [2, 3, 5, 6]
c_array_int = (ctypes.c_int32 * len(nums))(*nums)

lib.rust_func(c_array_str, len(array_str), c_array_int, len(nums))

Java 调用 Rust

[dependencies]
jni = {version = '0.19'}
// lib.rs
use jni::objects::*;
use jni::JNIEnv;

#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_init(env: JNIEnv, _class: JClass) {
    println!("rust-java-demo inited");
}

Rust 调用 C 源文件

例子:https://github.com/guofei9987/rs_lib

一、build.rs:编译前运行的脚本,使用前需要在 Cargo.toml 中使用它

extern crate cc;

fn main() {
    let mut build = cc::Build::new();
    build.opt_level(3);

    cc::Build::new().file("src/double.c").compile("libdouble.a");
    cc::Build::new().file("src/third.c").compile("libthird.a");
}

二、Cargo.toml

# 指定 build 文件的位置,在编译前运行的脚本
build = "src/build.rs"

[dependencies]
libc = "0.2"

# 编译期间的依赖
[build-dependencies]
cc = "1.0"

三、C语言文件

// double.c
int double_input(int input)
{
    return input * 2;
}

// third.c
int third_input(int input)
{
    return input * 3;
}

四、对应的 Rust 代码,文件名例如 c_script.rs

// extern crate libc;

extern "C" {
   pub fn double_input(input: libc::c_int) -> libc::c_int;
   pub fn third_input(input: libc::c_int) -> libc::c_int;
}

然后可以在别的地方调用它了

#[test]
fn test3() {
    let input = 4;
    let output = unsafe { double_input(input) };
    let output2: i32 = unsafe { third_input(input) };
    println!("{} * 3 = {}", input, output2);
    println!("{} * 2 = {}", input, output);
}

另外:TODO: 如何送各种数据

Rust 调用 C 编译后的文件

下面的方案,需要 编译后的文件 放入系统目录中(需要 C 项目里 make install
有点麻烦,更建议用上面的 Rust 调 Rust 的方案

rust 调用 c语言编译成的 dylib 文件,假设文件名为 libfoo.dylib(或者 libfoo.so

toml 文件添加这个

[lib]
name= "learn_rust"
crate-type = ["cdylib"]

Rust:

// 假设 文件名为 libfoo.dylib
#[link(name = "foo")]
extern "C" {
    fn add(a: i32, b: i32) -> i32;
}

fn main() {
    let result = unsafe { add(1, 2) };
    println!("1 + 2 = {}", result);
}

rust 调用c的时候,c返回的是结构体

// 1. 定义一个结构体,确保与 C 中的结构体具有相同的内存布局
#[repr(C)]
struct MyStruct {
    a: i32,
    c: i32,
}

// 2. 同上的使用方法
#[link(name = "foo")]
extern "C" {
    fn my_func(a: i32, b: i32) -> MyStruct;
}

fn main() {
    // 使用结构体
    let obj = unsafe { my_func(1, 9) };
    println!("{},{}", obj.a, obj.c);
}

rust调用c的时候,返回的是一个指向结构体的指针

#[link(name = "foo")]
#[repr(C)]
struct MyStruct {
    a: i32,
    b: *mut i32,
}

extern "C" {
    fn c_function() -> *mut MyStruct;
}

fn main() {
    let result = unsafe { c_function() };
    // 要访问结构体中的字段,需要用 std::ptr::read 来读取指针指向的值
    let value = unsafe { std::ptr::read((*result).b) };
    println!("a = {}, b = {}", (*result).a, value);
}

rust 调用c的时候,传入 char * 类型的字符串,

#[link(name = "foo")]
extern "C" {
    fn my_c_function(arg: *const c_char) -> c_int;
}

let arg = CString::new("hello").unwrap().as_ptr();
let result = unsafe { my_c_function(arg) };

rust 调用 c 的时候,入参是 char ** (指向字符串)

#[link(name = "foo")]
extern "C" {
    fn c_function(arg: *mut *mut c_char);
}


let my_string = "Hello, world!";
let c_string = CString::new(my_string).unwrap();
let c_string_ptr = c_string.as_ptr() as *mut c_char;

unsafe {
    c_function(&mut c_string_ptr);
}

rust 调用 c 的时候,入参是 char ** (指向一个字符串数组)

use std::ffi::CString;
use std::os::raw::{c_char, c_int};

#[link(name = "foo")]
extern "C" {
    fn my_func(len_s: c_int, strings: *mut *mut c_char);
}


fn main() {
    let strings = vec!["hello", "world!"];
    let c_strings: Vec<_> = strings
        .iter()
        .map(|s| CString::new(*s).unwrap().into_raw())
        .collect();
    let mut c_string_ptrs: Vec<_> = c_strings.iter().map(|s| *s as *mut c_char).collect();
    let res = unsafe { my_func(c_string_ptrs.len() as i32, c_string_ptrs.as_mut_ptr()) };
}

rust 调用c,其中c返回一个int类型的数组

use libc::{c_int, size_t};
use std::slice;

extern "C" {
    fn my_c_function() -> *const c_int;
}

fn main() {
    unsafe {
        let ptr = my_c_function();
        let len = 10; // 假设数组长度为 10
        let slice = slice::from_raw_parts(ptr, len as usize);
        for i in 0..len {
            println!("array[{}] = {}", i, slice[i as usize]);
        }
    }
}

Wasm:在网页上运行 Rust

Wasm(WebAssembly)允许在浏览器中运行代码

  • 跨平台性,编译一次在任何浏览器中运行
  • 高性能,比 Javascript 更快
  • 兼容性:可以与 JavaScript 无缝集成
  • 安全性:沙盒运行

第一步:安装必要的工具(如已安装,则忽略) cargo install wasm-pack

第二步:编写 Rust 代码

Cargo.toml

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"

lib.rs

use wasm_bindgen::prelude::*;

// 导出一个简单的函数到 JavaScript
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

第三步:编译

wasm-pack build --target web

编译后,会在 ./pkg 生成编译好的文件,(文件名取决你你的项目名)

  • (必须保留的) project_name.jsproject_name_bg.wasm,分别是中间桥梁、rust编译后的文件
  • package.json 这是 Node.js 配置文件,包含元数据等,用于作为 npm 包发布
  • xx.ts, xx.d.ts,这是 TypeScript 声明文件,如果不用 TypeScript,则可以删除

第四步:编写网页代码

index.js

// !!!注意,这里的文件名要改成实际的文件名!
import init, { greet } from './pkg/project_name.js';

async function run() {
    await init();

    const greetBtn = document.getElementById('greet-btn');
    const nameInput = document.getElementById('name');
    const greetingP = document.getElementById('greeting');

    greetBtn.addEventListener('click', () => {
        const name = nameInput.value;
        const greeting = greet(name);
        greetingP.textContent = greeting;
    });
}

run();

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Rust WASM 示例</title>
</head>
<body>
<h1>Rust WASM 示例</h1>
<input type="text" id="name" placeholder="输入名字">
<button id="greet-btn">Greet</button>
<p id="greeting"></p>

<script type="module" src="index.js"></script>
</body>
</html>

跨平台编译(没搞定)

参考:https://betterprogramming.pub/cross-compiling-rust-from-mac-to-linux-7fad5a454ab1

// Linux
rustup target add x86_64-unknown-linux-gnu
cargo build --target=x86_64-unknown-linux-gnu

// Linux
x86_64-unknown-linux-musl

// Andriod
arm-linux-androideabi

// Windows
x86_64-pc-windows-gnu

打包上传到 crates.io

Cargo.toml

定义优化等级(可以不填)

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

定义包的基本情况

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
authors = ["Guo Fei <me@guofei.site>"]
description = "一段简单的描述信息,要好好写."
license = "MIT OR Apache-2.0"

[dependencies]

打包步骤

  1. crates.io 上,生成api
  2. cargo login 【api】,这会把令牌保存到 ~/.cargo/credentials
  3. cargo publish
  4. 一旦发布,不可删除、不可覆盖。但可以让某个版本 yank

子项目

根目录的 Cargo.toml

[workspace]

members = [
    "project1",
    "project2",
]

project1 如何引用 project2?在 /project1/Cargo.toml

[dependencies]

add-one = { path = "../add-one" }

如何运行 project1?

cargo run -p project1

而如果在根目录执行 cargo run/build/test,则会把所有的子项目都执行

参考

https://rustwiki.org/zh-CN/book/ch03-05-control-flow.html


您的支持将鼓励我继续创作!