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 build
或cargo 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 test 或 rustc --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 robots 或 rustc --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)]
,要求永远不要行内化。
- 如果函数或方法在一个包里定义,但在另一个包里调用,那么 Rust 就不会将其在行内扩展。除非它是泛型的(有类型参数)或者明确标记为
#[cfg]
和#[allow]
,可以添加到整个模块中,并应用于其中所有的特性。#[test]
和#[inline]
,只能添加到个别特性项。- 要将属性添加给整个包,需要在
main.rs
或lib.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(())
}
知识点
- 用于编译的 Rust 不要返回 Result 类型。Rust 认识它,但是 Python/C 都不认识
- 要让 Python 能调用,还需要把
&Vec<u8>
改为*const u8
和usize
,把&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
共有两个方案,优缺点如下:
- 使用 pyo3:
- 语言集成层: pyo3提供了丰富的Rust和Python之间的类型转换和交互的抽象,这让开发者能够更容易地编写“Python风格”的Rust代码。
- 安全性和易用性: Rust的所有权和生命周期模型,在pyo3的帮助下,能够保证在与Python交互的过程中不会出现内存安全问题。
- 构建和打包工具: pyo3与maturin或setuptools-rust等工具结合,可以简化Python扩展模块的构建和分发流程。
- Python API: 允许直接使用Python的API进行编程,而不是仅仅通过FFI(外部函数接口)调用。
- 自定义Python模块: 可以创建符合Python模块和扩展标准的包,可以使用import语句在Python代码中直接导入。
- 使用 ctypes 加载 Rust 编译后的共享库
- 手动类型转换: 使用ctypes时,开发者通常需要手动处理类型转换和内存管理,这可能会导致错误,并增加额外的工作量。
- 简单的FFI边界: 当你需要直接调用少量Rust函数,并且不需要复杂的Rust和Python之间的交互时,ctypes可能是一个简单的解决方案。
- 少量依赖: 使用ctypes时,你的Python代码不需要依赖于pyo3或其他构建工具,只需要Rust编译出的共享库文件。
- 跨语言调用开销: 由于你在Python和Rust之间建立的是直接的FFI界面,调用开销可能会比pyo3的高级抽象稍低,但这通常只在极少数性能关键的场景中才显著。
演示 Python 如何调用一个 Rust 项目
- Python 传给 Rust 这些数据结构:str, int, float, list, dict 等
- Rust 返回给 Python 这些数据结构:String, i32, f64, Vec
, Vec - 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
把包发布到 pypimaturin 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.js
和project_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]
打包步骤
- crates.io 上,生成api
cargo login 【api】
,这会把令牌保存到~/.cargo/credentials
cargo publish
- 一旦发布,不可删除、不可覆盖。但可以让某个版本 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