From 0c4053ee6459656df0eb5f2cc80c0e53a372ea88 Mon Sep 17 00:00:00 2001 From: "nap.liu" Date: Tue, 19 Dec 2023 16:30:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E4=B9=A6=E5=AE=8C=E7=BB=93=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8F=8F=E8=BF=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 +- 20.Std misc/Cargo.lock | 7 + 20.Std misc/Cargo.toml | 8 + 20.Std misc/a/b.txt | 1 + 20.Std misc/a/c/b.txt | 1 + 20.Std misc/hello.txt | 1 + 20.Std misc/hosts.txt | 2 + 20.Std misc/lorem_ipsum.txt | 6 + 20.Std misc/src/main.rs | 646 +++++++++++++++++++++++++ 21.Testing/Cargo.lock | 32 ++ 21.Testing/Cargo.toml | 11 + 21.Testing/src/lib.rs | 75 +++ 21.Testing/src/main.rs | 144 ++++++ 21.Testing/tests/common/mod.rs | 3 + 21.Testing/tests/integration_test.rs | 31 ++ 22.Unsafe Operations/Cargo.lock | 7 + 22.Unsafe Operations/Cargo.toml | 8 + 22.Unsafe Operations/src/main.rs | 453 +++++++++++++++++ 23.Compatibility/Cargo.lock | 7 + 23.Compatibility/Cargo.toml | 8 + 23.Compatibility/src/main.rs | 41 ++ 24.Meta/24.1 documentation/Cargo.lock | 7 + 24.Meta/24.1 documentation/Cargo.toml | 8 + 24.Meta/24.1 documentation/src/main.rs | 74 +++ 24.Meta/24.2 Playground/Cargo.toml | 8 + 24.Meta/24.2 Playground/src/main.rs | 41 ++ readme.md | 19 + 27 files changed, 1650 insertions(+), 1 deletion(-) create mode 100644 20.Std misc/Cargo.lock create mode 100644 20.Std misc/Cargo.toml create mode 100644 20.Std misc/a/b.txt create mode 120000 20.Std misc/a/c/b.txt create mode 100644 20.Std misc/hello.txt create mode 100644 20.Std misc/hosts.txt create mode 100644 20.Std misc/lorem_ipsum.txt create mode 100644 20.Std misc/src/main.rs create mode 100644 21.Testing/Cargo.lock create mode 100644 21.Testing/Cargo.toml create mode 100644 21.Testing/src/lib.rs create mode 100644 21.Testing/src/main.rs create mode 100644 21.Testing/tests/common/mod.rs create mode 100644 21.Testing/tests/integration_test.rs create mode 100644 22.Unsafe Operations/Cargo.lock create mode 100644 22.Unsafe Operations/Cargo.toml create mode 100644 22.Unsafe Operations/src/main.rs create mode 100644 23.Compatibility/Cargo.lock create mode 100644 23.Compatibility/Cargo.toml create mode 100644 23.Compatibility/src/main.rs create mode 100644 24.Meta/24.1 documentation/Cargo.lock create mode 100644 24.Meta/24.1 documentation/Cargo.toml create mode 100644 24.Meta/24.1 documentation/src/main.rs create mode 100644 24.Meta/24.2 Playground/Cargo.toml create mode 100644 24.Meta/24.2 Playground/src/main.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index caa2e2b..878677d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "rust-analyzer.linkedProjects": [ - "./19.Std Library types/Cargo.toml" + "./24.Meta/24.1 documentation/Cargo.toml" ] } diff --git a/20.Std misc/Cargo.lock b/20.Std misc/Cargo.lock new file mode 100644 index 0000000..9dcd3cf --- /dev/null +++ b/20.Std misc/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "std_misc" +version = "0.1.0" diff --git a/20.Std misc/Cargo.toml b/20.Std misc/Cargo.toml new file mode 100644 index 0000000..2d66e5b --- /dev/null +++ b/20.Std misc/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "std_misc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/20.Std misc/a/b.txt b/20.Std misc/a/b.txt new file mode 100644 index 0000000..b6fc4c6 --- /dev/null +++ b/20.Std misc/a/b.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/20.Std misc/a/c/b.txt b/20.Std misc/a/c/b.txt new file mode 120000 index 0000000..4e19a04 --- /dev/null +++ b/20.Std misc/a/c/b.txt @@ -0,0 +1 @@ +../b.txt \ No newline at end of file diff --git a/20.Std misc/hello.txt b/20.Std misc/hello.txt new file mode 100644 index 0000000..c57eff5 --- /dev/null +++ b/20.Std misc/hello.txt @@ -0,0 +1 @@ +Hello World! \ No newline at end of file diff --git a/20.Std misc/hosts.txt b/20.Std misc/hosts.txt new file mode 100644 index 0000000..587caf6 --- /dev/null +++ b/20.Std misc/hosts.txt @@ -0,0 +1,2 @@ +127.0.0.1 +192.168.0.1 \ No newline at end of file diff --git a/20.Std misc/lorem_ipsum.txt b/20.Std misc/lorem_ipsum.txt new file mode 100644 index 0000000..0dc0fba --- /dev/null +++ b/20.Std misc/lorem_ipsum.txt @@ -0,0 +1,6 @@ +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse +cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non +proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/20.Std misc/src/main.rs b/20.Std misc/src/main.rs new file mode 100644 index 0000000..648ede3 --- /dev/null +++ b/20.Std misc/src/main.rs @@ -0,0 +1,646 @@ +//! +//! 标准库还提供了很多其他的特性常用的有下面这些 +//! +//! - 线程(Threads) +//! - 通道 (Channels) +//! - 文件 (File I/O) +//! +//! 这些扩展方法提供了更多操作系统底层的能力。 +//! + +/// +/// 线程 +/// `Rust` 提供了一种机制来可以直接调用系统的线程能力,`Rust` 线程和系统线程是 `1:1` 的关系, +/// 也就是一个 `Rust` 线程等于一个系统的线程。 +/// +fn threads() { + use std::thread; + + const NTHREADS: u32 = 10; + + // 创建一个动态数组来保存线程信息。 + let mut children = vec![]; + + for i in 0..NTHREADS { + // 使用标准库 `spawn` 方法来创建线程,并把创建线程的句柄(线程信息)保存在当前线程的变量中。 + // `move` 关键字会转移变量所有权到线程内部。 + children.push(thread::spawn(move || { + println!("this is thread number {}", i); + })); + } + + for child in children { + // 线程句柄对象上提供了一个方法叫 `join` 来等待线程执行完成,该方法会阻塞当前线程, + // 直到等待的线程结束退出,当前线程才会继续执行。 + let _ = child.join(); + } +} + +/// +/// 线程小测验 +/// +/// `Rust` 可以通过线程非常容易的让处理数据的过程并行,并消除了传统上多线程的一些痛点。 +/// +/// 标准库提供了非常好用的线程处理能力,组合使用 `Rust` 的所有权和别名规则,则可以自动的防止数据竞争的问题。 +/// +/// 别名规则 (只允许一个可变引用或者多个不可变引用)自动的帮你避免了在多线程中操作数据不一致的问题。 +/// 如果真的需要多个线程同时修改同一个数据的话可以通过标准库提供的 `Mutex` 或 `Channel` 来实现。 +/// +/// 下面这个例子我们会把一堆数字做一个汇总,这里使用多线程能力并行的处理每一小块数据,每一个线程会单独计算 +/// 一小块数据,然后我们会把所有线程计算的结果再进行汇总计算。 +/// +/// 注意一下,因为我们只是给线程传递了一个引用,`Rust` 明白我们只是传递了一个不可变引用,所以不会产生安全 +/// 和数据的竞争问题。 +/// 又因为我们传递的引用拥有 `'static` 声明周期,`Rust` 可以知道我们的数据不会在子线程的执行过程中被销毁。 +/// (当你需要在多个线程之间分享一个不是 `'statis` 声明周期的数据的时候,可以使用智能指针 `Arc` 来 +/// 保证数据在线程的执行过程中不会被销毁。) +/// +fn thread_test_case_map_reduce() { + use std::thread; + + // 这个是我们需要处理的数据,我们会使用线程计算每一行的所有数字的和,然后把每一行的和再进行合并。 + + // TODO: 尝试插入一个空格,看看会发生什么 + let data = "86967897737416471853297327050364959 + 11861322575564723963297542624962850 + 70856234701860851907960690014725639 + 38397966707106094172783238747669219 + 52380795257888236525459303330302837 + 58495327135744041048897885734297812 + 69920216438980873548808413720956532 + 16278424637452589860345374828574668"; + + // 这里定义一个动态数组来存放线程句柄。 + let mut children = vec![]; + + // 这里通过内置方法把上面的字符串按照空白字符分割成字符串的数组。 + for line in data.split_whitespace() { + // 这里通过标准库的线程方法对每一个字符串进行加和计算,并返回加和的结果值 + children.push(thread::spawn(move || -> u32 { + // 使用字符串的 `chars` 方法创建一个迭代器,这个迭代器每次都返回一个数字的字符。 + line.chars() + // 对每一个数字的字符转换成数字 + .map(|f| f.to_digit(10).expect("必须是数字字符串")) + // 对转换完的数字结果进行加和 + .sum() + })); + } + + // 让当前线程等待子线程的计算完成,然后对计算结果再次进行加和计算,最终保存结果到 `sum` 中。 + let sum = children.into_iter().map(|f| f.join().unwrap()).sum::(); + + println!("thread testcase: sum result {}", sum); + + // 实际上对于上面这种处理逻辑是不明智的,因为这里会根据用户输入的数据来创建的线程,而线程是非常宝贵的计算资源, + // 应该按照一个固定的线程数量来处理用户输入的数据,而不是动态的创建数量不定的线程。 +} + +/// +/// 通道(channels) +/// +/// Rust 提供了异步的 `channels` 来让多个线程之间进行数据交换,`channel` 通过 `Sender`,`Receiver` 提供了一个单向的数据交换的能力。 +/// +fn channels() { + use std::sync::mpsc; + use std::thread; + + let NTHREADS = 3i32; + + // 创建一个 `channel` + let (tx, rx) = mpsc::channel::(); + + let mut children = vec![]; + + for id in 0..NTHREADS { + // 复制一个 `channel` 给每一个线程 + let thread_tx = tx.clone(); + + let child = thread::spawn(move || { + // 通过 `channel` 发送异步数据。 + thread_tx.send(id).unwrap(); + }); + + // 保存线程句柄 + children.push(child); + } + + // 创建一个数组用于保存结果。 + let mut ids = Vec::with_capacity(NTHREADS as usize); + for _ in 0..NTHREADS { + // 通过 `channel` 的接收端接收发送的数据。 + ids.push(rx.recv().unwrap()); + } + + for child in children { + // 等待所有线程退出 + child.join().expect("oops! the child thread panicked"); + } + + // 这里接收到的结果不保证顺序,因为线程是由操作系统来调度的,所以没办法保证顺序。 + println!("{:?}", ids); +} + +/// +/// 路径 (Path) +/// +/// Path 结构是用于描述底层文件系统的。有两种常用的路径,一种是 `posix::Path` 用于 `UNIX-like` 的系统, +/// 另外一种是 `windows::Path` 是 `Windows` 系统使用的,`Rust` 中会根据当前的系统自动的选择这两种的其中一种。 +/// +/// `Path` 可以通过 `OsStr` 创建,并且提供了一些方法来获取路径上对应的信息,是一个文件还是文件夹。 +/// +/// `Path` 是不可变的,可变版本的 `Path` 是 `PathBuf`,这两个的关系和 `str` 和 `String` 的关系非常相似, +/// `PathBuf` 是可变的并且可以取消对原始的 `Path` 的引用关系。 +/// +/// 注意 `Path` 不会保证路径的内容是 `UTF-8` 格式的,内部使用的是 `OsString` 这个类型来保存数据,因此把 `Path` 转换为 +/// `&str` 可能会失败,但是 `Path` 可以通过 `into_os_string` 和 `as_os_str` 转换为 `OsString` 或 `&OsStr`,并且一定会成功。 +/// +/// +fn path_() { + use std::path::Path; + + // 从 `&'static str` 转换为 `Path` + let path = Path::new("."); + + // `display` 方法返回一个可以输出的版本。 + let _display = path.display(); + + // `join` 方法可以合并两个路径成一个完整的路径,会返回一个 `PathBuf` 的结构。 + let mut new_path = path.join("a").join("b"); + + // 使用 `push` 可以在现有的路径上继续添加新的路径。 + new_path.push("c"); + new_path.push("myfile.tar.gz"); + + // `set_file_name` 方法可以在路径上添加一个文件名。 + new_path.set_file_name("package.tgz"); + + // 转换路径到 `&str` 这里可能会失败,因为 `Path` 中可能包含非 `UTF-8` 的内容 + match new_path.to_str() { + None => panic!("new path is not a valid UTF-8 sequence"), + Some(s) => println!("new path is {}", s), + } +} + +/// +/// 文件 I/O 操作 +/// +/// File 对象是一个已经打开的文件(内部封装了文件描述符 FD File Descriptor),并且给你提供了 +/// 对于底层文件的读写能力。 +/// +/// 由于文件处理的过程中很多动作可能会失败,所以所有的文件操作都会返回 `io::Result` 类型,也就是 +/// `Result` 类型。 +/// +/// 这样所有的操作都会明确的告诉你可能会失败,多亏了这层包装,开发者可以非常明确的看到所有可能失败的操作是什么, +/// 并鼓励积极主动的处理这些问题。 +/// +fn file_I_O() { + use std::fs::{self, File}; + // 把常用的方法扩展都引用进来 + use std::io::prelude::*; + use std::io::{self, BufRead}; + use std::path::Path; + + // 打开并读取文件,只读模式 + { + let path = Path::new("hello.txt"); + let display = path.display(); + + let mut file = match File::open(&path) { + Err(why) => panic!("couldn't open {}: {}", display, why), + Ok(file) => file, + }; + + let mut s = String::new(); + match file.read_to_string(&mut s) { + Err(why) => panic!("cloldn't read: {}: {}", display, why), + Ok(_) => println!("{} contains: \n{}", display, s), + } + } + + // 创建文件 + // create 方法会用写文件(write-only)的权限打开指定的文件,如果文件已经存在了则会清空原始文件内容, + // 文件不存在的话则会创建一个新的文件。 + { + let LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse +cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non +proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +"; + let path = Path::new("lorem_ipsum.txt"); + let display = path.display(); + + // 只写模式打开文件 返回 `io::Result` 对象。 + let mut file = match File::create(&path) { + Err(why) => panic!("couldn't create {}: {}", display, why), + Ok(file) => file, + }; + + // 把 `LOREM_IPSUM` 写入到文件中,返回 `io::Result<()>`。 + match file.write_all(LOREM_IPSUM.as_bytes()) { + Err(why) => panic!("couldn't write to {}: {}", display, why), + Ok(_) => println!("successfully wrote to {}", display), + } + + // 还可以使用 `OpenOptions` 这个对象来配置打开文件的权限 + } + + // `read_lines` 很多种场景下都会需要按行读取文件内容,一开始接触文件系统的时候可能会写出下面的方法。 + // 下面实现了三种方法 `read_lines_with_for`,`read_lines_with_iter`,`read_lines`, + // 这三种方法中前两种在处理大文件的时候都会有很严重的性能问题,因为一次性读取了所有的文件内容,所以会占用很多的内存空间 + // 并且因为所有的内容都被读取到文件中了,所以 `lines` 会处理很大的内容。 + // 而第三种方法 `read_lines` 则不会有这种问题,因为每次读取的时候只会读取一行内容,并且只在需要的时候才会去读取, + // 如果没有对 `read_lines` 返回的迭代器进行读取操作的话,则文件不会被读取。 + { + // 最简单直接的读取文件,使用 `for` 循环来处理每一行 + fn read_lines_with_for(filename: &str) -> Vec { + let mut result = Vec::new(); + // 一次性读取所有的文件内容,然后遍历保存到动态数组中, + // 这里 `read_to_string` 方法会读取全部的文件内容到内存中,这样的话如果文件非常大,则会造成内存的过度使用。 + for line in fs::read_to_string(filename).unwrap().lines() { + result.push(line.to_string()); + } + result + } + + // 使用迭代器来处理每一行,这两种方法都会造成内存的过度使用。 + fn read_lines_with_iter(filename: &str) -> Vec { + // 一次性读取所有的文件内容 + fs::read_to_string(filename) + .unwrap() + // 按照每一行分割 + .lines() + // 转化成 `String` 类型 + .map(|f| f.to_string()) + // 把迭代器转换成数组 + .collect() + } + + // 使用 `std::io::BufReader` 对象来分批次的读取文件,这里使用了 `lines` 方法每次只从文件中读取一行内容。 + fn read_lines

(filename: P) -> io::Result>> + where + P: AsRef, // 这里的类型定义为了保持和 `File::open` 的定义相同 + { + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) + } + + // 文件 hosts.txt 必须要存在 + if let Ok(lines) = read_lines("./hosts.txt") { + // 使用迭代器方法每次只读取一行内容,直到文件全部都被读取完,因为读取操作可能失败, + // 所以这里使用 `if let` 方法进行解构。 + for line in lines { + if let Ok(ip) = line { + println!("{}", ip); + } + } + } + } +} + +/// +/// 子进程 +/// 另一个非常常见的场景就是启动一个子进程来处理一下事情,Rust 提供了一个 `process::Command` 对象来专门 +/// 提供了这种能力,这个方法会返回一个 `process:Output` 的对象来保存子进程的返回值。 +/// +fn child_processes() { + use std::process::Command; + + let output = Command::new("rustc") + .arg("--version") + .output() + .unwrap_or_else(|e| panic!("failed to execute process: {}", e)); + + if output.status.success() { + let s = String::from_utf8_lossy(&output.stdout); + + print!("rustc succeeded and stdout was:\n{}", s); + } else { + let s = String::from_utf8_lossy(&output.stderr); + + print!("rustc failed and stderr was:\n{}", s); + } +} + +/// +/// 管道(pipes) +/// +/// `process::Command` 会返回一个 `process::Child` 的实例对象,该对象表示启动的子进程。 +/// +/// `process::Child` 对象上提供了 `stdin`,`stdout`,`stderr` 方法来操作系统进程底层的管道 +/// 来和子进程进行交互。 +/// +fn pipes() { + use std::io::prelude::*; + use std::process::{Command, Stdio}; + + static PANGRAM: &'static str = "the quick brown fox jumped over the lazy dog\n"; + // 启动一个 `wc` 的子进程。 + let process = match Command::new("wc") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + { + Err(why) => panic!("couldn't spawn wc: {}", why), + Ok(process) => process, + }; + + // 向 `wc` 子进程的输入管道中写入数据, + // 这里使用了 `unwrap` 获取了 `stdin` 对象并转移了 `stdin` 的所有权。 + // 当 `write_all` 方法执行完以后,内部保留的 `stdin` 对象就会被回收,也就是底层的管道会被自动关闭。 + match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) { + Err(why) => panic!("couldn't write to wc stdin: {}", why), + Ok(_) => println!("sent pangram to wc"), + } + + // `stdout` 字段是使用 `Option` 包装的所以也需要使用 `unwrap` 解包装。 + let mut s = String::new(); + match process.stdout.unwrap().read_to_string(&mut s) { + Err(why) => panic!("couldn't read wc stdout: {}", why), + Ok(_) => print!("wc responded with:\n{}", s), + } +} + +/// +/// 有很多时候需要等待子进程的退出,可以使用 `Child::wait` 方法来实现该功能, +/// 这个方法会等待子进程退出并且返回进程的退出状态 `process::ExitStatus` +/// +fn wait() { + use std::process::Command; + // 这里启动一个子进程 `sleep` 并且传递了一个参数 `5` + let mut child = Command::new("sleep").arg("5").spawn().unwrap(); + // `sleep` 进程会等待 `5` 秒钟以后退出,所以这里也会等待 `5` 秒钟。 + let _result = child.wait().unwrap(); + println!("reached end of main"); +} + +/// +/// 文件系统的操作示例 +/// Rust 的 std::fs 模块提供了很多方法来操作底层的文件系统 +/// +fn filesystem_operations() { + use std::fs; + use std::fs::{File, OpenOptions}; + use std::io; + use std::io::prelude::*; + use std::os::unix; + use std::path::Path; + + // 实现一个简单版本的 `cat path` 能力 + fn cat(path: &Path) -> io::Result { + let mut f = File::open(path)?; + let mut s = String::new(); + + // 这个 match 可以使用下面的代码代替,更简洁一些 + // match f.read_to_string(&mut s) { + // Ok(_) => Ok(s), + // Err(e) => Err(e), + // } + f.read_to_string(&mut s)?; + Ok(s) + } + + // 实现一个简单版本的 `echo s > path` + fn echo(s: &str, path: &Path) -> io::Result<()> { + let mut f = File::create(path)?; + + f.write_all(s.as_bytes()) + } + + // 简单版本的 `touch path` 忽略文件已经存在的错误 + fn touch(path: &Path) -> io::Result<()> { + match OpenOptions::new().create(true).write(true).open(path) { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + + println!("`mkdir a`"); + // 创建文件夹,该方法返回 `io::Result<()>` + match fs::create_dir("a") { + Err(why) => println!("! {:?}", why.kind()), + Ok(_) => {} + } + + println!("`echo hello > a/b.txt`"); + // 使用自己实现的能力来写文件,如果出错了的话打印错误 + echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + + println!("`mkdir -p a/c/d`"); + // 递归创建文件夹,如果出错打印出错误 + fs::create_dir_all("a/c/d").unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + + println!("`touch a/c/e.txt`"); + // 使用自己实现的方法来创建一个文件,创建失败打印错误。 + touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + + println!("`ln -s ../b.txt a/c/b.txt`"); + // 创建一个软连接,这里只实现 `unix` 的版本 + if cfg!(target_family = "unix") { + unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + } + + println!("`cat a/c/b.txt`"); + // 使用自己的方法来输出文件。 + match cat(&Path::new("a/c/b.txt")) { + Err(why) => println!("! {:?}", why.kind()), + Ok(s) => println!("> {}", s), + } + + println!("`ls a`"); + // 读取文件夹 + match fs::read_dir("a") { + Err(why) => println!("! {:?}", why.kind()), + Ok(paths) => { + for path in paths { + println!("> {:?}", path.unwrap().path()); + } + } + } + + println!("`rm a/c/e.txt`"); + // 删除文件 + fs::remove_file("a/c/e.txt").unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + + println!("`rmdir a/c/d`"); + // 删除一个空目录 + fs::remove_dir("a/c/d").unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); +} + +/// +/// 进程参数 +/// +/// 标准库提供了 `std::env::args` 用于获取启动进程的参数,该方法返回一个参数的迭代器。 +/// +fn program_arguments() { + use std::env; + let args: Vec = env::args().collect(); + + // 读取第一个参数,通常情况下第一个参数都是当前的进程的完整路径。 + println!("My path is {}.", args[0]); + + // 剩下的参数才是启动进程的时候传递的真正参数 + // $ ./args arg1 arg2 + println!("I got {:?} arguments: {:?}.", args.len() - 1, &args[1..]); +} + +/// +/// 参数的使用 +/// +fn arguments_parsing() { + use std::env; + + fn increase(number: i32) { + println!("{}", number + 1); + } + + fn decrease(number: i32) { + println!("{}", number - 1); + } + + fn help() { + println!( + "usage: +match_args + Check whether given string is the answer. +match_args {{increase|decrease}} + Increase or decrease given integer by one." + ); + } + + let args: Vec = env::args().collect(); + + match args.len() { + // 没有参数 + 1 => { + println!("My name is 'match_args'. Try passing some arguments!"); + } + // 一个参数 + 2 => match args[1].parse() { + Ok(42) => println!("This is the answer!"), + _ => println!("This is not the answer."), + }, + // 两个参数,一个是方法,一个是具体数字 + 3 => { + let cmd = &args[1]; + let num = &args[2]; + // 格式化数字 + let number: i32 = match num.parse() { + Ok(n) => n, + Err(_) => { + eprintln!("error: second argument not an integer"); + help(); + return; + } + }; + // 选择使用的方法 + match &cmd[..] { + "increase" => increase(number), + "decrease" => decrease(number), + _ => { + eprintln!("error: invalid command"); + help(); + } + } + } + // 都不匹配的话则全部都走一个兜底的方法 + _ => { + // 显示帮助消息 + help(); + } + } +} + +/// +/// 外部函数引用(FFI Foreign Function Interface) +/// +/// Rust 提供了引用外部 `C` 库函数的能力(FFI),外部函数必须定义在 `extern` 块内, +/// 然后通过属性宏 `#[link(name = "m")]` 声明,下面定义的函数名在哪个库内。 +/// +fn foreign_function_interface() { + use std::fmt; + + // Minimal implementation of single precision complex numbers + // 单精度复数的最小实现 + #[repr(C)] // 定义数据的对齐方式使用 `C` 的方式 + #[derive(Clone, Copy)] + struct Complex { + re: f32, + im: f32, + } + + impl fmt::Debug for Complex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.im < 0. { + write!(f, "{}-{}i", self.re, -self.im) + } else { + write!(f, "{}+{}i", self.re, self.im) + } + } + } + + // 这个表示这里定义的所有方法都是 `libm` 这个库中的方法。 + #[link(name = "m")] + extern "C" { + // 这个方法是计算单精度复数的平方根,调用的是 `libm` 库中的方法。 + fn csqrtf(z: Complex) -> Complex; + fn ccosf(z: Complex) -> Complex; + } + + // 因为外部方法默认都是不安全的 (unsafe),必须使用 `unsafe {}` 来包裹外部的不安全方法调用, + // 经过包装以后对外提供一个安全方法,可以直接调用,不再需要 `unsafe {}` 来包裹了。 + fn cos(z: Complex) -> Complex { + unsafe { ccosf(z) } + } + // z = -1 + 0i + let z = Complex { re: -1., im: 0. }; + + // 直接调用外部方法 需要使用 `unsafe {}` 来包裹 + let z_sqrt = unsafe { csqrtf(z) }; + + println!("the square root of {:?} is {:?}", z, z_sqrt); + + // calling safe API wrapped around unsafe operation + // 这里调用我们自己封装的安全方法,这样的话就不需要 `unsafe {}` 包裹了 + println!("cos({:?}) = {:?}", z, cos(z)); +} + +fn main() { + // 线程操作 + threads(); + // 线程小测验 + thread_test_case_map_reduce(); + // 通道 + channels(); + // 文件路径 + path_(); + // 文件IO + file_I_O(); + // 子进程 + child_processes(); + // 子进程和管道 + pipes(); + // 等待子进程 + wait(); + // 操作文件系统 + filesystem_operations(); + // 进程参数 + program_arguments(); + // 进程参数的使用 + arguments_parsing(); + // 外部函数使用 `FFI` + foreign_function_interface(); +} diff --git a/21.Testing/Cargo.lock b/21.Testing/Cargo.lock new file mode 100644 index 0000000..af179cf --- /dev/null +++ b/21.Testing/Cargo.lock @@ -0,0 +1,32 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "testing" +version = "0.1.0" +dependencies = [ + "pretty_assertions", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/21.Testing/Cargo.toml b/21.Testing/Cargo.toml new file mode 100644 index 0000000..65d6a56 --- /dev/null +++ b/21.Testing/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "testing" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[dev-dependencies] +pretty_assertions = "1" diff --git a/21.Testing/src/lib.rs b/21.Testing/src/lib.rs new file mode 100644 index 0000000..f8a750c --- /dev/null +++ b/21.Testing/src/lib.rs @@ -0,0 +1,75 @@ +//! +//! 文档测试的设计理念 +//! +//! 文档测试主要是用于说明提供的能力以及使用方法,它是非常重要的指导方针之一, +//! 它允许使用完整的实例来说明提供的功能是什么。 +//! + +/// +/// 文档测试 +/// 主要的测试方式是通过源代码中使用 `///` 开头的代码注释中的代码块来进行测试, +/// 由于 `///` 开头的注释支持 `Markdown` 语法,所以在注释中出现的代码块会默认是 +/// `Rust` 的测试代码,可以由 `Rust` 对代码块中的代码进行单元测试。 +/// +/// ``` +/// let sum = testing::add(1, 2); +/// assert!(sum == 3); +/// ``` +/// +pub fn add(a: i32, b: i32) -> i32 { + a + b +} + +/// +/// 文档测试中还支持异常测试,这个例子里面包含了两个测试块,`示例`,`异常` +/// +/// 下面的函数是两数相除 +/// +/// # 示例 +/// +/// ``` +/// let result = testing::div(10, 2); +/// assert_eq!(result, 5); +/// ``` +/// +/// # 异常 +/// +/// 第二个数字为 `0` 的时候将产生异常 +/// +/// 这里使用了特殊的属性标记 `rust,should_panic`,其中 `rust` 是语言类型,`should_panic` 是告诉编译器这里的代码应该产生异常。 +/// +/// ```rust,should_panic +/// let result = testing::div(10, 0); // 这个函数会产生异常。 +/// ``` +/// +pub fn div(a: i32, b: i32) -> i32 { + if b == 0 { + panic!("Divide-by-zero error"); + } + + a / b +} + +/// +/// 有些时候写的测试代码可能会需要包含 `?` 操作符来快捷展开 `Result` 的结果, +/// 这时候会文档测试会失败,因为默认的测试代码返回值应该是 `()`,针对这个情况 +/// 提供了一个隐式的包装代码能力。 +/// +/// ``` +/// # // 使用特殊标记 `#` 来让一些代码不显示在最终的文档中,但是这些代码依旧会参与实际的测试编译中。 +/// # fn try_main() -> Result<(), String> { // 这个代码是有效的包装代码,不会显示在文档中,但是会参与测试的编译 +/// let res = testing::try_div(10, 2)?; // 只有这行代码会展示在最终的文档中 +/// # Ok(()) // 这里返回一个 `()` +/// # } +/// # fn main() { // 测试代码的主入口和正常 `main` 函数是一样的 +/// # try_main().unwrap(); // 调用 `try_main` 方法来执行测试逻辑 +/// # // 如果测试有问题的话 这里应该会 `panic!` +/// # } +/// ``` +pub fn try_div(a: i32, b: i32) -> Result { + if b == 0 { + Err(String::from("Divide-by-zero")) + } else { + Ok(a / b) + } +} diff --git a/21.Testing/src/main.rs b/21.Testing/src/main.rs new file mode 100644 index 0000000..525c26d --- /dev/null +++ b/21.Testing/src/main.rs @@ -0,0 +1,144 @@ +//! +//! Rust 非常注重正确性并且语言支持内部直接写测试代码。 +//! +//! 测试分为了三种类型 +//! - 单元测试(Unit testing) +//! - 文档测试 (Doc testing) +//! - 集成测试 (Integration testing) +//! +//! Rust 还支持特殊的依赖项,专门为测试使用的依赖。 +//! 测试使用的依赖项 (Dev-dependencies) +//! + +/// 测试是 `Rust` 的函数,用来测试一些非测试代码的正确性,有些测试函数是为了初始化一些环境, +/// 在测试函数中可以使用断言来验证结果是否符合预期 +/// +/// 大多数单元测试都是在 `tests` 模块中定义的,这个模块使用属性宏 `#[cfg(test)]` 来标记模块, +/// 测试函数使用 `#[test]` 宏来标记是一个测试函数。 +/// +/// 测试中可以使用断言来触发 `panic!`,下面是常用的一些断言宏。 +/// +/// - `assert!(expression)` 表达式为 `false` 的时候触发 `panic!`。 +/// - `assert_eq!(left, right)` 和 `assert_nq!(left, right)` 左右两个值不相等的时候触发 `panic!`。 +/// + +pub fn add(a: i32, b: i32) -> i32 { + a + b +} + +// 这个是一个错误的加法实现,用于测试失败的情况。 +#[allow(dead_code)] +fn bad_add(a: i32, b: i32) -> i32 { + a - b +} + +// 求平方根 +#[allow(dead_code)] +fn sqrt(number: f64) -> Result { + if number >= 0.0 { + Ok(number.powf(0.5)) + } else { + Err("negative floats don't have square roots".to_owned()) + } +} + +pub fn divide_non_zero_result(a: u32, b: u32) -> u32 { + if b == 0 { + panic!("Divide-by-zero error"); + } else if a < b { + panic!("Divide result is zero"); + } + a / b +} + +/// +/// 定义单元测试模块,用于测试内部实现的代码逻辑。 +/// +/// 可以使用附加的参数来运行指定的测试 +/// - `cargo test test_any_panic` 只运行 `test_any_panic` 单元测试 +/// - `catgo test -- --ignored` 只运行包含 `#[ignore]` 的单元测试 +/// +#[cfg(test)] +mod tests { + // 使用 `super` 关键字来把父级作用域下的方法都引入到当前模块中。 + use super::*; + + #[test] + fn test_add() { + assert_eq!(add(1, 2), 3); + } + + #[test] + #[should_panic] + fn test_bad_add() { + // 这里使用故意写错的函数来测试失败的情况 + assert_eq!(bad_add(1, 2), 3); + } + + #[test] + fn test_add_hundred() { + assert_eq!(add(100, 2), 102); + assert_eq!(add(2, 100), 102); + } + + #[test] + #[ignore] // 这个测试默认是不会运行的 + fn ignored_test() { + assert_eq!(add(0, 0), 0); + } + + #[test] + fn test_sqrt() -> Result<(), String> { + let x = 4.0; + // 使用 `?` 操作符快捷返回出错的情况 + assert_eq!(sqrt(x)?.powf(2.0), x); + Ok(()) + } + + #[test] + fn test_divide() { + assert_eq!(divide_non_zero_result(10, 2), 5); + } + + #[test] + #[should_panic] // 使用 `should_panic` 宏来标记 函数应该要触发 `panic!` + fn test_any_panic() { + divide_non_zero_result(1, 0); + } + + #[test] + #[should_panic(expected = "Divide result is zero")] // 给 `should_panic` 传递参数,来表示 `panic!` 的信息应该包含指定的内容。 + fn test_specific_panic() { + divide_non_zero_result(1, 10); + } +} + +/// +/// 有很多时候会需要引用一些模块是专门为了测试时候使用的(用于代码示例,或者用于基准测试), +/// 这些模块可以定义在 `Cargo.toml` 的 `[dev-dependencies]` 这个字段下, +/// +/// ```toml +/// [package] +/// name = "testing" +/// version = "0.1.0" +/// edition = "2021" +/// [dev-dependencies] +/// pretty_assertions = "1" +/// +/// ``` +/// +#[cfg(test)] +mod tests_dependencies { + use super::*; + // 这个模块只能在测试代码中使用,其他的非测试代码不能使用。 + use pretty_assertions::assert_eq; + + #[test] + fn test_add() { + assert_eq!(add(2, 3), 5) + } +} + +fn main() { + println!("Hello, world!"); +} diff --git a/21.Testing/tests/common/mod.rs b/21.Testing/tests/common/mod.rs new file mode 100644 index 0000000..26d55d1 --- /dev/null +++ b/21.Testing/tests/common/mod.rs @@ -0,0 +1,3 @@ +pub fn setup() { + println!("common testing setup"); +} diff --git a/21.Testing/tests/integration_test.rs b/21.Testing/tests/integration_test.rs new file mode 100644 index 0000000..4c2e630 --- /dev/null +++ b/21.Testing/tests/integration_test.rs @@ -0,0 +1,31 @@ +//! +//! 集成测试(Integration testing) +//! +//! 单元测试可以让我们对单个模块和内部的代码分别的进行测试, +//! 集成测试是用来对整个包(crate)的公开功能进行测试, +//! 来保证对外公开的功能是符合预期的。 +//! +//! 集成测试的所有代码都被放置在根目录下的 `tests` 目录中, +//! 该目录下的所有代码都会被当做一个集成测试的单元,很多情况下 +//! 需要定义一些公共的方法来进行一些测试的初始化,因为这个目录下的所有代码 +//! 都会被当做集成测试的具体实现,为了同时支持独立文件集成测试和公共的测试代码抽取, +//! 所以这里可以使用老式的模块定义形式,也就是 `tests/somemodule/mod.rs` 来定义 +//! 一个公共的测试模块,这样的话测试的过程中这种老模块声明的模块不会被当做集成测试的入口, +//! 但是这个模块定义的方法可以被其他的继承测试使用。 +//! +//! + +/// +/// 使用老模块的文件结构声明的模块 不会被当做集成测试的代码 +/// 基于这样的特性 +/// +mod common; // 引入公共的测试代码,包含一些公共的方法。 + +use common::*; +use testing::*; + +#[test] +fn test_add() { + setup(); + assert_eq!(add(3, 2), 5); +} diff --git a/22.Unsafe Operations/Cargo.lock b/22.Unsafe Operations/Cargo.lock new file mode 100644 index 0000000..3c37d2c --- /dev/null +++ b/22.Unsafe Operations/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "unsafe_operations" +version = "0.1.0" diff --git a/22.Unsafe Operations/Cargo.toml b/22.Unsafe Operations/Cargo.toml new file mode 100644 index 0000000..e131741 --- /dev/null +++ b/22.Unsafe Operations/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "unsafe_operations" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/22.Unsafe Operations/src/main.rs b/22.Unsafe Operations/src/main.rs new file mode 100644 index 0000000..0673624 --- /dev/null +++ b/22.Unsafe Operations/src/main.rs @@ -0,0 +1,453 @@ +//! +//! 不安全操作(Unsafe Operations) +//! +//! 这一章是从 [`官方文档`](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html),“应该尽量减少 +//! 代码库中不安全代码的数量”,基于这个理念,我们开始这一章。 +//! +//! 不安全声明是为了绕过 Rust 编译器的安全检查,使用不安全代码是为了解决下面这些问题。 +//! +//! - 直接使用原始指针 +//! - 使用 `unsafe` 关键字来调用一些方法(比如 `FFI`,或一些不安全的操作) +//! - 修改静态可变的一些变量(比如使用 `static` 关键字定义的常量) +//! - 实现一些不安全的特性 +//! + +/// +/// 原始指针使用 `*` 操作符来访问,和 `&T` 中解引用的行为类似, +/// 但是引用始终都是安全的并且保证引用的数据永远都是有效的,并且会被编译使用借用规则来校验。 +/// +/// 解引用一个原始指针只能使用 `unsafe` 关键字内部使用。 +/// +fn raw_pointers() { + let raw_p: *const u32 = &10; + unsafe { + assert!(*raw_p == 10); + } +} + +/// +/// 有些不安全的函数只能使用 `unsafe` 块来包裹才能使用,使用这些不安全函数 +/// 只能由使用者来保证函数的正常运行,编译器不会进行安全检查。 +/// 下面这个例子是 `std::slice::from_raw_parts` 它可以通过原始指针和元素长度来创建 +/// 一个切片。 +/// +fn calling_unsafe_functions() { + use std::slice; + + let some_vector = vec![1, 2, 3, 4]; + + let pointer = some_vector.as_ptr(); + let length = some_vector.len(); + + unsafe { + // 这里必须要保证传递给函数的指针和长度以及数据类型是正确的, + // 因为编译器不会检查这些参数是否是有效的,如果数据无效则会导致未知的行为。 + let my_slice: &[i32] = slice::from_raw_parts(pointer, length); + assert_eq!(some_vector.as_slice(), my_slice); + } +} + +/// +/// 内联汇编(Inline assembly) +/// +/// Rust 提供了 `asm!` 宏可以直接在代码中嵌入汇编代码,这个特性在开发嵌入式的时候非常有用, +/// 这些通过 `asm!` 宏书写的汇编代码会被编译器直接嵌入到最终的二进制文件中。 +/// 通常情况下这个特性不会被使用,但是有些性能敏感的操作无法通过常规代码实现的场景中非常有用。 +/// +/// 使用底层机器语言或者内核编程会使用这个特性。 +/// +/// 注意:下面这个实例仅仅是针对 `x86/x86-64` 架构的汇编,其他的架构可以通过自己查阅硬件手册即可。 +/// +/// 内联汇编当前支持的架构有 +/// +/// - `x86/x86-64` +/// - `ARM` +/// - `AArch64` +/// - `RISC-v` +/// +fn inline_assembly() { + use std::arch::asm; + + // 基础示例 + { + unsafe { + // NOP(no operation) 汇编指令是 x86 架构下的一个指令,该指令占一个字节,字节码是 `0x90`,含义为当前指令没有任何操作。 + asm!("nop"); + } + } + + // 输入和输出 + { + let x: u64; + unsafe { + // mov 这个是 x86 架构下的一个移动数据的指令,使用两个参数,一个是目标寄存器,一个是向寄存器中写入的数据。 + // 这里实际上的操作是 `x = 5` ,可以看到这里写的内容是字符串,实际上该字符串是一个模版字符串,它和普通的模版字符串拥有相同的使用规则, + // 这里有一点区别的是模版字符串后面跟随的参数,这里首先要先定义我们使用的变量是输入还是输出, + // `out(reg) x` 代表的是输出,还有就是我们需要指定需要使用哪个寄存器,在这个示例中 + // 我们使用了一个任意可用的寄存器也就是 `reg` ,编译器会自动选择一个在当前代码的上下文中可以被使用的寄存器, + // 然后会自动把 `reg` 替换成一个可用的具体寄存器。 + + asm!("mov {}, 5", out(reg) x); + } + + assert_eq!(x, 5); + + let i: u64 = 3; + let o: u64; + + unsafe { + asm!( + "mov {0}, {1}", // 汇编的赋值操作符,把数据 i 和 o 读取到寄存器中,并把 o 赋值为 i,也就是说 `o = i`, + "add {0}, 5", // 汇编加法,再把 o 的数据 +5,也就是 `o += 5`, + out(reg) o, + in(reg) i, + ) + } + assert_eq!(o, 8); + + let mut x: u64 = 3; + + unsafe { + // 使用 `inout` 关键字来定义 `x` 既是输入也是输出, + // 然后调用汇编加法对变量进行加法操作,所有的操作都会发生在同一个寄存器上。 + asm!("add {0}, 5", inout(reg) x); + } + + assert_eq!(x, 8); + + let x: u64 = 3; + let y: u64; + unsafe { + // 使用 `inout(reg) x => y` 来简化输入输出的变量定义, + // `x` 是输入 `y` 是输出 + // 这里也会保证操作只发生在同一个寄存器上。 + asm!("add {0}, 5", inout(reg) x => y); + } + assert_eq!(y, 8); + } +} + +/// +/// 延迟输出操作符 (独占寄存器标记) +/// +/// `Rust` 会假设对于变量的写入再汇编代码的任意位置都可能发生,那么对于给该变量 +/// 分配的寄存器就不能被其他变量占用,而 `Debug` 情况下默认是不会进行代码优化的, +/// 所以在 Debug 的时候可能代码运行的结果没有问题,但是使用 `Release` 模式输出的结果 +/// 就是错误的,因为 `Release` 模式下 `Rust` 编译器会尽可能的少申请寄存器使用, +/// 可能好几个变量使用的是同一个寄存器,那么在这种情况下可能会造成中间状态的错误,最终导致结果错误! +/// +/// 为了避免这种情况的发生 `Rust` 提供了 `lateout` 用来标记该变量需要一个独占的寄存器, +/// 该标记还有一个 `inlateout` 的变种,这个变种标记这个变量既是输入也是输出。 +/// +/// +fn late_output_operands() { + { + use std::arch::asm; + + let mut a: u64 = 4; + let b: u64 = 4; + let c: u64 = 4; + unsafe { + asm!( + "add {0}, {1}", + "add {0}, {2}", + inout(reg) a, // 这里在 `Release` 模式下可能会出错,因为 三个变量都是 `in` 而且值是一样的, + in(reg) b, // 所以三个变量在 `Release` 模式下会被分配相同的寄存器,这样就会导致中间结果错误,最终导致结果错误。 + in(reg) c, + ); + } + assert_eq!(a, 12); + } + { + use std::arch::asm; + + let mut a: u64 = 4; + let b: u64 = 4; + unsafe { + // 这里使用 `inlateout` 来标记 `a` 的寄存器是独占的,这样的话 `a` 和 `b` 就各自拥有了不同的寄存器。 + asm!("add {0}, {1}", inlateout(reg) a, in(reg) b); + } + assert_eq!(a, 8); + } +} + +/// +/// 手动指定寄存器 +/// +/// 有些特殊的指令要求指定的操作数要在指定的寄存器下才生效, +/// 因此 Rust 内联汇编提供了特殊的标记来标识使用的是哪一个寄存器, +/// +/// `reg` 是一个通用的寄存器标志它可以自动选择对应平台当前可用的寄存器, +/// 而明确指定寄存器是一个高度和目标平台相关的你使用的寄存器必须是目标平台提供的寄存器, +/// 比如 `x86` 平台 通用的寄存器有 `eax`,`ebx`,`ecx`,`edx`,`ebp`,`esi`,`edi` +/// 还有一些特殊的寄存器没有名字可以通过寄存器地址来访问。 +/// +/// 注意:不像是其他的操作符,手动指定寄存器 +/// +fn explicit_register_operands() { + use std::arch::asm; + + let cmd = 0xd1; // 这个指令是控制输出端口的写入动作 + unsafe { + // 这里直接使用特殊的寄存器地址 `0x64` 来操作寄存器,这个寄存器是控制寄存器 + // 这个特殊的寄存器只能通过 `eax` 这个寄存器来操作,所以这里手动指定使用的寄存器是 `eax` + asm!("out 0x64, eax", in("eax") cmd); + } + + fn asm_add(a: u64, b: u64) -> u128 { + let lo: u64; + let hi: u64; + + // `x86` 的通用寄存器 `eax` `ebx` `ecx` `edx` 等等都是 `32` 位寄存器 + // `rax` `rbx` `rcx` `rdx` 等等是 `64` 位寄存器 + + unsafe { + asm!( + // x86 的乘法指令只有一个操作数,这个操作数接收一个数字, + // 然后第二个数字必须存放在 `rax` 寄存器中,计算的最终结果会被放在 `rax:rdx` 中,把两个寄存器当成一个寄存器使用。 + "mul {}", + in(reg) a, + inlateout("rax") b => lo, + lateout("rdx") hi + ); + } + + // 这里进行强制类型转换然后通过位运算合并计算结果成一个最终结果。 + ((hi as u128) << 64) + lo as u128 + } + + let test = asm_add(100, 200); + assert_eq!(test, 300); +} + +/// +/// 在很多场景下内联汇编只用于修改一些状态并不需要输出数据,但是因为操作可能会导致寄存器上下文丢失, +/// 通常都是一些只需要汇编内部自处理的逻辑,或者操作了一些会破坏当前寄存器的汇编代码。 +/// +/// 这样的代码被称为 `clobbered(破坏性代码)`,这里就需要明确的告诉编译器,我们的内联汇编可能 +/// 会丢失汇编块周围的寄存器上下文,需要先保存这些寄存器的值,然后再执行汇编代码,最后恢复寄存器的值 +/// +fn clobbered_registers() { + use std::arch::asm; + + // 有三条数据,每条数据四个字节 + let mut name_buf = [0_u8; 12]; + + // `ascii` 格式的字符串会被按顺序保存在 `ebx` `edx` `ecx` 寄存器中 + // 因为 `ebx` 是一个有特殊作用的寄存器,所以写汇编代码的时候需要先保存 `ebx` 这个寄存器的数值,使用完成后再恢复回去。 + // 在 `64位` 架构的 `64位` 系统上不允许 `push/pop` 指令直接使用 `32位` 的寄存器,所以这里只能使用扩展后的 `rbx` 寄存器,(`rbx` 是 `ebx` 的 `64位` 扩展) + + unsafe { + asm!( + "push rbx", // 这里使用 `rbx` 来代替 `ebx` + "cpuid", // 特殊指令 读取 `cpuid` 读取的结果会依次存放到 `ebx` `edx` `ecx` + "mov [rdi], ebx", + "mov [rdi + 4], edx", + "mov [rdi + 8], ecx", + "pop rbx", // 恢复寄存器 + // 使用具名的寄存器 `rdi` 来保存数组的原始指针。 + in("rdi") name_buf.as_mut_ptr(), + // 通过 `eax` 寄存器明确指定 `cpuid` 指令的参数为 `0` 获取 `cpu` 的厂商信息,并且告诉编译器这个寄存器的数据会被覆盖掉 + inout("eax") 0 => _, + // 这里的两个寄存器的数据也会被覆盖掉 + out("ecx") _, + out("edx") _, + ); + } + + let name = core::str::from_utf8(&name_buf).unwrap(); + println!("CPU Manufacturer ID: {}", name); + + { + // 使用位移和加法实现 `x * 6`; + let mut x: u64 = 4; + + unsafe { + asm!( + "mov {tmp}, {x}", // 复制数据到 `tmp` 寄存器中 + "shl {tmp}, 1", // 使用 `shl` 对 `tmp` 左移 `1` 位 等于 `tmp * 2` + "shl {x}, 2", // 使用 `shl` 对 `x` 左移 `2` 位 等于 `x * 4` + "add {x}, {tmp}", // 对 `x` 和 `tmp` 结果相加并把结果放在 `x` 中 + x = inout(reg) x, + tmp = out(reg) _, + ); + } + assert_eq!(x, 4 * 6); + } +} + +/// +/// 默认情况下内联汇编会认为所有没被标记为输出的寄存器的值都有汇编代码来维护其内容, +/// 换句话来说,就是内联汇编中没有被标记为输出的寄存器的内容都会被上下文保留, +/// 也就是说,汇编代码中硬编码了一些其他的寄存器这些寄存器的值被你手动修改过了, +/// 但是这个寄存器并没有被标记为输出寄存器,那么这个时候当汇编代码被执行完以后, +/// 这个寄存器的值就是你在汇编代码中最后修改的值,这样的话可能会造成其他的运行错误, +/// 因为你修改了一个寄存器,这个寄存器可能保存了原来的代码上下文环境。 +/// +/// 因此可以使用 `clobber_abi("C")` 关键字来指定所有的非改调用约定的寄存器的值 +/// 在汇编运行之前会自动保留,等汇编执行完成后会自动恢复这些寄存器的值,以保证代码的正确性。 +/// +fn symbol_operands_and_abi_clobbers() { + use std::arch::asm; + + extern "C" fn foo(arg: i32) -> i32 { + println!("arg = {}", arg); + arg * 2 + } + + fn call_foo(arg: i32) -> i32 { + unsafe { + let result; + asm!( + "call {}", + // 传入函数指针 + in(reg) foo, + // `64位` 系统下默认使用 `rdi` 寄存器的值当做第一个参数。 + in("rdi") arg, + // 返回值保存在 `rax` 寄存器 + out("rax") result, + // 让所有非 `C` 调用约定下的寄存器全都要保存快照,汇编结束后自动恢复。 + clobber_abi("C"), + ); + result + } + } + + let x = call_foo(100); + println!("{}", x); +} + +/// +/// 有些时候需要对单个寄存器进行拆分使用,这时候可以通过模版字符串来进行精细化控制, +/// 在汇编语言中一个寄存器可能会被拆分成多个小的子集,比如说 `eax` 是 rax 的低 `32位`, +/// `ax` 是 `eax` 的低 `16位`,`ah` 是 `ax` 的高 `8位`,`al` 是 `ax` 的低 `8位`, +/// 等等以此类推。 +/// +/// 默认情况下编译器都会优先使用完整的寄存器名称 比如说 64位系统下默认选择 `rax`,在 `32位` 系统下 +/// 默认选择 `eax` 等等。 +/// +/// 这些默认行为可以通过模版字符串中指定操作数来修改。 +/// +fn register_template_modifiers() { + use std::arch::asm; + + let mut x: u16 = 0xab; + + unsafe { + asm!( + // 这里最终会被展开成 `move ah, al`。 + "mov {0:h}, {0:l}", + // 这里选择告诉编译器可以选择 `eax` `ebx` `ecx` `edx` 的任意一个寄存器来使用, + // 具体选择了哪一个由编译器来决定,所以这里实际上是把 `x` 的数值放到了 `ax` `bx` `cx` `dx`, + // 的任意一个寄存器中,然后模版字符串中通过 `0:l` 来访问低八位,通过 `0:h` 来访问高八位。 + inout(reg_abcd) x + ); + } + + assert_eq!(x, 0xabab); +} + +/// +/// 有些情况下汇编指令需要操作的是一个间接的内存地址, +/// 你需要使用内存地址访问的特殊语法(这个和目标平台相关,需要查阅不同平台的手册)来进行内存地址的访问。 +/// +fn memory_address_operands() { + use std::arch::asm; + let control: u16 = 0; + unsafe { + // 在 `x86` 体系中 使用 `[]` 来访问内存地址 + asm!("fldcw [{}]", in(reg) &control, options(nostack)); + } +} + +/// +/// 汇编中的标签重用可能会导致编译或者连接错误或者其他的一些奇怪的行为, +/// 在下面这些情况下都会可能会出现标签的重用。 +/// +/// - 明确的标签重用,同一个标签在一个 `asm!` 块中出现了多次,或者一个标签在多个 `asm!` 快中出现多次。 +/// - 内联宏(`#[inline]`)产生的隐式标签,编译器允许同一个 `asm!` 被内联到多个地方中,这个时候就会出现标签的重复使用。 +/// - 隐式的编译器优化(`LTO`),编译器优化的时候会使用其他包的代码合并到一起再次尝试优化,这时候也会出现标签的重复使用。 +/// +/// 所以这里更推荐使用 `GNU` 数字表示的局部标签来定义标签名,这样的话就不会产生因为标签的重定义问题导致的汇编或者链接错误。 +/// +/// 另外在 `x86` 架构上当使用默认的 `Intel` 语法时候由于 `LLVM` 的问题,不应该使用只有 `0` 和 `1` 数字组成的标签,比如 `0` `11` `101010`, +/// 因为会被解析成一个二进制的数据,可以使用 `options(att_syntax)` 来避免这些歧义,但这个会影响所有的 `asm!` 语法。 +/// +fn labels() { + use std::arch::asm; + + let mut a = 0; + unsafe { + asm!( + "mov {0}, 10", + "2:", // 使用数字本地标签语法来定义标签,这里没有使用 `0` 或 `1` 因为 `LLVM` 的问题直接从数字 `2` 开始使用 + "sub {0}, 1", + "cmp {0}, 3", + "jle 2f", // 这里使用 `2f` 代表 `forward to 2`,也就是指的下面定义的 `2:` 标签 + "jmp 2b", // 这里使用 `2b` 代表 `backword to 2`,也就是指的上面定义的 `2:` 标签 + "2:", + "add {0}, 2", + out(reg) a + ); + } + assert_eq!(a, 5); +} + +/// +/// 默认情况下内联汇编的代码会被认为和 FFI 的方法是一样的, +/// 调用这些方法会做一些自定义的转换,可能会进行内存的读写,或者有可观察的一些副作用等等, +/// 所以需要为编译器提供尽可能多的一些信息来告诉编译器应该哪里能够进行优化。 +/// +/// 明确指定汇编选项可以让编译器能更好的进行代码优化。 +/// +/// 更多的选项可以[参考这里](https://doc.rust-lang.org/stable/reference/inline-assembly.html) +/// +fn options() { + use std::arch::asm; + // 使用前一个代码来举例子 + { + let mut a: u64 = 4; + let b: u64 = 4; + unsafe { + asm!( + "add {0}, {1}", + inlateout(reg) a, in(reg) b, + // options 这个参数必须要放在 `asm!` 块的最后一个参数上, + // 参数中比较常见的是下面这三个值 + // - pure 这个参数表示汇编代码中没有任何副作用,输出只和输入相关联,这样的话编译器可以最大力度的优化代码,如果代码足够简单的话,编译器甚至在保证功能一样的同时可以把这些代码直接优化掉。 + // - nomem 这个参数表示汇编代码中不需要读写内存,默认情况下编译器会认为汇编代码会读写任意内存地址。(通过(寄存器中保留的)传递的指针,或者全局变量) + // - nostack 这个参数表示汇百年代码中没有任何对于栈的操作,这个可以允许编译器使用特定平台对于栈的特殊优化手段来避免出现栈的重新调整。 + options(pure, nomem, nostack), + ); + } + assert_eq!(a, 8); + } +} + +fn main() { + // 操作原始指针 + raw_pointers(); + // 调用函数 + calling_unsafe_functions(); + + { + // 内联汇编 + inline_assembly(); + // 寄存器独占 + late_output_operands(); + // 明确指明寄存器 平台相关 + explicit_register_operands(); + // 标记寄存器的状态 + clobbered_registers(); + // 快捷标记寄存器状态,通过调用约定来标记 + symbol_operands_and_abi_clobbers(); + // 通过模版字符串来精细化操作寄存器 + register_template_modifiers(); + // 内存指针访问,平台相关 + memory_address_operands(); + // 汇编中的命名标签 + labels(); + // 汇编的优化配置项 + options(); + } +} diff --git a/23.Compatibility/Cargo.lock b/23.Compatibility/Cargo.lock new file mode 100644 index 0000000..a4753ca --- /dev/null +++ b/23.Compatibility/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "compatibility" +version = "0.1.0" diff --git a/23.Compatibility/Cargo.toml b/23.Compatibility/Cargo.toml new file mode 100644 index 0000000..4252f4d --- /dev/null +++ b/23.Compatibility/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "compatibility" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/23.Compatibility/src/main.rs b/23.Compatibility/src/main.rs new file mode 100644 index 0000000..40bc138 --- /dev/null +++ b/23.Compatibility/src/main.rs @@ -0,0 +1,41 @@ +//! +//! Rust 语言在非常快速的发展,因此可能会出现某些兼容性问题,尽管 +//! 已经在尽可能的保证向前兼容,但是依旧有些特性会出现改变。 +//! + +/// +/// Rust 和许多其他的编程语言一样也拥有关键字的概念,这些关键字都对 Rust 有特定的功能, +/// 所以你不能使用这些关键字来定义变量或者函数或者其他的一些东西,原始标记(Raw identifiers) +/// 符号可以让你使用任意的文字包括关键字来定义一些变量或者函数或者任意其他的东西。 +/// +/// 这个特性在 Rust 引入新的关键字的时候会非常有用,有些文字在现在的版本不是关键字, +/// 但是后面的发展过程中把这个文字纳入了关键字的范围就会导致原来的一些库没有办法使用了, +/// 因为这些库使用了新的关键字当做变量或者函数等等的一些对象上。 +/// +/// 举个例子,我们有一个包(crate) 叫 `foo` 是使用 2015 版本的 Rust 编译的,该包导出了一个方法 +/// 叫 `try`。这个 `try` 被 2018 版本的 Rust 中列为了关键字,如果没有原始标记的话,我们 +/// 没有任何办法去使用这个函数。 +/// +/// ``` +/// extern crate foo; +/// +/// fn main () { +/// foo::try(); // 这里会报错 因为 `try` 在 2018 版本以后是关键字 +/// // 通过原始标记 `r#` 开头来标记后面的文字是原始字符,这里实际上还是调用的 `foo:try` 方法,这里的 `r#` 只是告诉编译器我要使用关键字了。 +/// foo::r#try(); // 这里可以正常编译不会报错 +/// } +/// +/// ``` +/// +/// ```shell +/// error: expected identifier, found keyword `try` +/// --> src/main.rs:4:4 +/// | +///4 | foo::try(); +/// | ^^^ expected identifier, found keyword +/// ``` +/// + +fn main() { + println!("Hello, world!"); +} diff --git a/24.Meta/24.1 documentation/Cargo.lock b/24.Meta/24.1 documentation/Cargo.lock new file mode 100644 index 0000000..424a6a7 --- /dev/null +++ b/24.Meta/24.1 documentation/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "documentation" +version = "0.1.0" diff --git a/24.Meta/24.1 documentation/Cargo.toml b/24.Meta/24.1 documentation/Cargo.toml new file mode 100644 index 0000000..7ff3046 --- /dev/null +++ b/24.Meta/24.1 documentation/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "documentation" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/24.Meta/24.1 documentation/src/main.rs b/24.Meta/24.1 documentation/src/main.rs new file mode 100644 index 0000000..fa1952b --- /dev/null +++ b/24.Meta/24.1 documentation/src/main.rs @@ -0,0 +1,74 @@ +//! +//! 输出文档 +//! +//! `cargo doc` 可以输出当前工程的静态文档,该文档会自动保存在 `target/doc` 目录中。 +//! +//! `cargo test` 可以运行当前工程中的所有测试用例,包括文档中的测试代码。 +//! +//! `cargo test --doc` 可以只运行文档中的测试代码。 +//! +//! 这些命令会自动的调用 `rustdoc` 和 `rustc` 来完成指定的操作。 +//! +//! + +/// +/// 文档注释使用 `///` 开头,并支持 `Markdown` 语法 +/// +/// 大型的项目非常需要文档注释来解释某些具体的模块是什么样的,以及实现了什么功能, +/// 当使用 `rustdoc` 的时候,这些文档注释中的代码就会被编译成一个静态的文档, +/// +fn doc_comments() {} + +// #![crate_name = "doc"] // 当不使用 cargo 系统的时候可以使用这个宏来声明包(crate)的名称. + +/// `人` 类型的定义 +pub struct Person { + /// 一个人必须拥有一个名字 + name: String, +} + +impl Person { + /// 返回一个人的实例,这个人拥有给定的名字 + /// + /// # 参数 + /// + /// * `name` - 一个名字,这个名字会给到一个具体的人 + /// + /// # 示例 + /// + /// ``` + /// // 你可以在这里写 Rust 的代码 + /// // 如果你使用 rustdoc --test 来查看文档的话还会自动的运行这段代码来测试你的实现是否正常。 + /// use doc::Person; + /// let person = Person::new("name"); + /// ``` + pub fn new(name: &str) -> Person { + Person { + name: name.to_string(), + } + } + + /// 和朋友们说 `hello` + pub fn hello(&self) { + println!("Hello, {}!", self.name); + } +} + +#[doc(inline)] // 这个宏可以让文档嵌入到其他使用方的文档中 +pub use bar::Bar; + +/// bar docs +mod bar { + /// the docs for Bar + pub struct Bar; +} + +#[doc(no_inline)] // 这个宏禁止文档被嵌入到任何页面中 +pub use crate::mem::drop; + +#[doc(hidden)] // 这个宏会让文档隐藏不显示 +pub use self::async_await::*; + +fn main() { + println!("Hello, world!"); +} diff --git a/24.Meta/24.2 Playground/Cargo.toml b/24.Meta/24.2 Playground/Cargo.toml new file mode 100644 index 0000000..aa2c955 --- /dev/null +++ b/24.Meta/24.2 Playground/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "playground" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/24.Meta/24.2 Playground/src/main.rs b/24.Meta/24.2 Playground/src/main.rs new file mode 100644 index 0000000..1c3d964 --- /dev/null +++ b/24.Meta/24.2 Playground/src/main.rs @@ -0,0 +1,41 @@ +//! +//! [Rust Playground](https://play.rust-lang.org/) 是一个可以在线体验 Rust 代码的一个网页 +//! +//! 使用 mdbook 让你的文档可以现在交互使用。 +//! +//! [mdbook](https://github.com/rust-lang/mdBook) 可以让你的示例代码可以运行并且可以编辑 +//! +//! +//! 还可以使用 `#[doc]` 宏来指定 `html_playground_url` 一个地址这个地址对应了一些测试代码, +//! 可以让你的代码可以在线交互测试 +//! +//! 更多的相关属性可以[查看这里](https://doc.rust-lang.org/rustdoc/the-doc-attribute.html) +//! + +/// +/// +/// 通过加上额外的标记 `rust,editable` 来让 mdbook 识别要生成交互的文档测试代码块 +/// +/// ```rust,editable +/// let result = add(1, 2); +/// assert_eq!(result, 3); +/// ``` +/// +fn add(a: usize, b: usize) -> usize { + a + b +} + +/// +/// 通过添加 `ignore` 标记可以让 mdbook 来忽略这个代码块 +/// +/// ```rust,editable,ignore +/// let result = sub(10, 5); +/// assert_eq!(result, 5); +/// ``` +fn sub(a: usize, b: usize) -> usize { + a - b +} + +fn main() { + println!("Hello, world!"); +} diff --git a/readme.md b/readme.md index e69de29..b00c330 100644 --- a/readme.md +++ b/readme.md @@ -0,0 +1,19 @@ + +# 这个工程就是官方的 `Rust by Example` 的极简翻译 + +可以在这里查看[官方书](https://doc.rust-lang.org/rust-by-example/index.html) + +该工程只有一个目的,熟悉并了解 `Rust` 的基本组成和一些基本的使用, +并不包含高深的内容,其实本书并没有覆盖到所有的 `Rust` 特性,但是对于一般的引用场景下差不多都够用了, +整体的学习完这本书可以对 `Rust` 有一个比较清晰的认知,能熟悉整体的设计以及常规的使用方法。 + +该工程仅仅是做了一个简单的翻译,所有的示例都是我自己慢慢翻译过来的,和原书上肯定有一定差异,因为我是按照我的理解 +进行翻译的,可能有些地方我的理解有误,所以我并不保证这些例子里面的注释是完全正确的。 + +如果对本工程有兴趣的小伙伴可以自己克隆下来一个一个示例的看下去。 + +工程提供了一个脚本叫 `update-projects.sh` 这个脚本只做了一件事,就是动态的更新 `.vscode/config.json` 中的项目配置,但是该脚本依赖了 `shell` 的 `jq` 来处理 `json` 所以,想用该脚本的话保证电脑上有 `jq` 命令 +```shell +# 该命令会把该目录下的所有工程都添加到当前 `rust-analyzer` 的工程分析中 +./update-projects.sh "1.Hello World" +```