【Rust2】包、Crate、模块



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};
// 上面的等价于:
use std::io;
use std::io::Write;

use std::collections::*;

私有

mod front_of_house {
  // 默认私有,加 pub 后才能让外面使用
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// use 前面加 pub 是重导出。调用这段代码时,也能调这个
pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

如何引入包

文件名 Cargo.toml

[dependencies]
rand = "0.8.3"

如何做包

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();
}

多文件

目录结构

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

Cargo.toml

# Cargo.toml
[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.rsmy_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");
    }
}

其它知识

  • 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:自动在浏览器打开
  • Cargo 把新生成的文档文件保存在 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++
    /// ...
    /// ```
    

一些基本配置问题

Blocking waiting for file lock on package cache

  • rm -rf ~/.cargo/registry/index/*
  • rm -rf ~/.cargo/.package-cache

Updating crates.io index 卡住

  • 把crates.io换国内的镜像源
  • vim ~/.cargo/config,修改为下面的
# 放到 `$HOME/.cargo/config` 文件中
[source.crates-io]
#registry = "https://github.com/rust-lang/crates.io-index"

# 替换成你偏好的镜像源
replace-with = 'ustc'
#replace-with = 'sjtu'

# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

# 中国科学技术大学
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

# 上海交通大学
[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"

# rustcc社区
[source.rustcc]
registry = "git://crates.rustcc.cn/crates.io-index"

混合编程

// 编译输出一个符合C语言调用规范的动态库文件
// 如果配置为dylib,则输出是符合rust调用规范的动态库,只能在rust语言编写的项目之间互相调用,不能跨语言
// name:之后的生成的lib名称跟这个有关
[lib]
name= "learn_rust"
crate-type = ["cdylib"]
// lib.rs
// #[no_mangle] 可以要求编译时不要混淆函数名,否则跨语言可能不认
#[no_mangle]
pub extern fn lib_func(x: i32, y: i32) -> i32 {
    println!("调用rust成功");
    return x + y;
}

然后编译:

cargo build

会发现 target/debug 多了个 文件 (Macbook 是 .dylib,linux是 .so,windows 是 .dll

Rust 调用 Rust

// libloading = "0.7"
extern crate libloading;

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/git/learn/untitled/target/debug/liblearn_rust.dylib")?;
        // 3. 注意这个输入值和返回值需要指定一下
        let func: libloading::Symbol<unsafe extern fn(i32, i32) -> u32> = lib.get(b"lib_func")?;
        Ok(func(x, y))
    }
}

fn main() {
    let _a = call_dynamic(1, 2);
    println!("{}", _a.expect("error"));
}

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
  • PyO3,需要在rust 里面使用

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 源文件

一、在 build.rs

extern crate cc;

fn main() {
    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 主函数

extern crate libc;

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

fn main() {
    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 编译后的文件

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]);
        }
    }
}

跨平台编译(没搞定)

参考: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


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