这是一遍阅读 rust 第三方库 human-panic 的源码,这个库是用来美化 panic 信息的
项目 stars 数:1.7k,代码量四千多行,这种比较小的库是比较入门学习的,可以了解 rust 的一些特性和最佳实践
这是我阅读的第一个 rust 项目,所以会记录一些 rust 的基础知识,正式的源码阅读在下面的源码解读章节中
文档测试
在 rust 中,文档注释可用于生成文档,同时也可以用于测试:
cargo test运行文档测试cargo doc自动生成文档:cargo doc --open自动生成文档并打开浏览器
运行完命令之后,会在 target/doc 目录下生成文档,其中 index.html 是文档的入口
文档测试主要是针对库代码的,不是针对二进制执行文件,所以文档注释的入口默认是 src/lib,在 cargo.toml 中可以修改
比如你想修改为 src/vender.rs,则在 cargo.toml 中添加:
[lib]
path = "src/vender.rs"
文档注释分为:行文档注释和块文档注释
行文档注释用 /// 开头,用于为以下的函数、模块、结构体等生成文档,注释的内容应该是对该项的描述,并且会在文档中显示
/// This function adds two numbers and returns the result.
///
/// # Examples
///
/// ```
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
块文档注释用 //! 开头,用于对当前模块、库或文件的整体进行描述,这种注释一般出现在文件或模块的开头,用来描述文件的功能或模块的整体情况。
//! This module provides basic arithmetic functions.
pub mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
如果你的项目名是 example-project,那么在使用 exmaple-project 作为模块名名时,要以 example_project 为模块名,rust 会自动将 - 替换为 _
如果想要在文档中隐藏某个函数,可以在函数前加上 #[doc(hidden)] 注解
#[doc(hidden)]
pub fn hidden_function() {
println!("You can't see me!");
}
宏
宏可以实现函数重载,但宏是在编译时展开的,所以宏的参数类型必须是确定的,不能是泛型
宏声明有三种方式:
- 带
#[macro_export]- 可被其他crate使用 - 不带
#[macro_export]- 仅在当前crate内使用 - 带可见性控制
// 私有宏
macro_rules! private_macro {}
// 模块级可见
#[macro_use]
mod macros {}
// 公开但仅限当前 crate
#[macro_export(local_inner_macros)]
macro_rules! public_crate_only {}
// 完全公开
#[macro_export]
macro_rules! fully_public {}
宏可以使用 $crate 来引用当前 crate,这在跨 crate 使用时很有用
#[macro_export]
macro_rules! setup_panic {
() => {
$crate::setup_panic();
};
}
pub fn setup_panic() {
println!("uccs");
}
在宏中,$meta 是一个特殊的语法:
$用来表示这是一个宏变量- 它告诉编译器这不是普通的变量名,而是宏系统中的一个占位符
// 在宏中:
macro_rules! setup_panic {
($meta:expr) => {{
$crate::setup_panic(|| $meta); // 是一个无参数闭包
}};
}
其他常见的类型说明符:
macro_rules! example {
($i:ident) => { /* 标识符 */ };
($s:stmt) => { /* 语句 */ };
($e:expr) => { /* 表达式 */ };
($t:ty) => { /* 类型 */ };
($p:pat) => { /* 模式 */ };
($m:meta) => { /* 元数据 */ };
}
inline(always)
#[inline(always)] 作用是:注解强制函数内联,这会影响最终的 backtrace
这是正常调用
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let result = add_one(5); // 这里会有函数调用的开销
}
这是内联后的效果
fn main() {
let result = 5 + 1; // 编译器直接把函数体放在调用处
}
Cow
Cow 是 "Clone on Write"(写时克隆)的缩写,它是 rust 标准库中一个很巧妙的类型
Cow<'a, T> 是一个智能指针,它可以包含两种数据:
- 借用的数据
(&T) - 拥有的数据
(T)
fn main() {
let input = "hello";
let a = process_string(input);
println!("{}", a); // HELLO
let input = "HELLO";
let a = process_string(input);
println!("{}", a); // HELLO
}
fn process_string<'a>(input: &'a str) -> Cow<'a, str> {
if input.chars().all(char::is_uppercase) {
// 如果已经是大写,直接返回借用
Cow::Borrowed(input)
} else {
// 需要修改时,会创建一个并返回一个新的 String
Cow::Owned(input.to_uppercase())
}
}
如果不是用 Cow,则需要返回一个 String,这样会导致每次都需要创建一个新的 String,而 Cow 可以避免这种情况
fn main() {
let input = "hello";
let a = process_string(input);
println!("{}", a); // HELLO
let input = "HELLO";
let a = process_string(input);
println!("{}", a); // HELLO
}
fn process_string(input: &str) -> String {
if input.chars().all(char::is_uppercase) {
input.to_string()
} else {
input.to_uppercase()
}
}
Into
Into 是 rust 标准库中的一个 trait,它的主要作用是提供类型转换的功能
// String 实现了 From<&str>,所以 &str 可以转换为 String
let string: String = "hello".into(); // &str -> String
// 等价于:
let string: String = String::from("hello");
函数的参数如果不使用 Into,则需要传入具体的类型
// 不使用 Into
fn process_string(s: String) {
println!("{}", s);
}
// 调用时必须传入 String
process_string(String::from("hello")); // 不能直接传 &str
使用 Into 可以传入多种类型
// 使用 Into
fn process_string(s: impl Into<String>) {
let s = s.into();
println!("{}", s);
}
// 现在可以传入任何能转换为 String 的类型
process_string("hello"); // &str 可以
process_string(String::from("hello")); // String 也可以
impl Into<String> 是一个泛型语法糖,它等价于 T: Into<String>
fn process_string<T: Into<String>>(s: T) {
let s = s.into();
println!("{}", s);
}
通常 Into 搭配 From 使用,这样就可以实现双向转换,实现了 From trait,会自动实现 Into trait
#[derive(Debug)]
struct Person {
name: String,
}
impl From<&str> for Person {
fn from(name: &str) -> Self {
Self {
name: name.to_string(),
}
}
}
fn main() {
let p: Person = "Alice".into();
println!("{:?}", p); // Person { name: "Alice" }
}
Into 用于函数参数,From 用于函数返回值
环境变量
常用的 Cargo 环境变量
env!("CARGO_PKG_NAME") // 包名,来自 name = "your-package-name"
env!("CARGO_PKG_VERSION") // 版本,来自 version = "0.1.0"
env!("CARGO_PKG_AUTHORS") // 作者,来自 authors = ["Your Name"]
env!("CARGO_PKG_DESCRIPTION")// 描述,来自 description = "Your description"
env!("CARGO_PKG_HOMEPAGE") // 主页,来自 homepage = "https://..."
来自的 Cargo.toml 文件中的配置
[package]
name = "my-awesome-app"
version = "0.1.0"
authors = ["Alice <alice@example.com>"]
description = "A really awesome app"
homepage = "https://example.com"
需要注意的是,env! 是编译时宏,在编译时求值,所以只能获取到编译时的环墋变量,不能获取运行时的环境变量
获取运行时环境变量,使用 std::env::var
use std::env;
fn main() {
let key = "HOME";
match env::var(key) {
Ok(val) => println!("{}: {:?}", key, val),
Err(e) => eprintln!("couldn't interpret {}: {}", key, e),
}
}
属性标记
属性标记是一种向编译器提供额外信息的元数据
// 内部属性 (作用于包含它的整个模块或项)
#![attribute_name]
// 外部属性 (作用于紧随其后的项)
#[attribute_name]
常见的属性类型:
// 1. 条件编译
#[cfg(target_os = "windows")]
fn windows_only() {
// 仅在 Windows 系统下编译
}
// 2. 衍生宏
#[derive(Debug, Clone)]
struct Point {
x: i32,
y: i32,
}
// 3. 编译器控制
#[allow(dead_code)]
fn unused_function() {}
// 4. 测试标记
#[test]
fn test_function() {
assert_eq!(2 + 2, 4);
}
// 5. 文档注释
#[doc = "这是一个函数的文档"]
fn documented_function() {}
属性标记的参数形式:
// 无参数
#[test]
// 单个参数
#[path = "foo.rs"]
// 多个参数
#[allow(unused, deprecated)]
// 键值对
#[cfg(target_os = "windows")]
// 结构化参数
#[deprecated(
since = "1.0.0",
note = "请使用新的 API"
)]
序列化
rust 中序列化还是比较简单的,使用 serde 就可以实现
serde_derive 是过程宏 (procedural macros) 库,提供 #[derive(Serialize, Deserialize)] 派生宏
#[derive(Debug, Serialize)]
struct Person {
name: String,
age: u8,
}
impl Person {
fn new(name: String, age: u8) -> Self {
Self { name, age }
}
pub fn serialize(&self) -> Option<String> {
toml::to_string_pretty(&self).ok()
}
}
fn main() {
let person = Person::new("Alice".to_string(), 30);
println!("{:?}", person.serialize().unwrap());
}
调试
cargo run默认是进入Debug模式,可通过cfg!(debug_assertions)判断cargo run --release进入Release模式,通过std::env::var("RUST_BACKTRACE")获取环境变量RUST_BACKTRACE=1 cargo run进入Debug模式,通过std::env::var("RUST_BACKTRACE")获取环境变量
#[non_exhaustive]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum PanicStyle {
/// Normal panic
Debug,
/// Human-formatted panic
Human,
}
impl Default for PanicStyle {
fn default() -> Self {
if cfg!(debug_assertions) {
PanicStyle::Debug
} else {
match ::std::env::var("RUST_BACKTRACE") {
Ok(_) => PanicStyle::Debug,
Err(_) => PanicStyle::Human,
}
}
}
}
模块
pub mod report; 是声明和引入模块,mod 是关键字,report 是模块名,report.rs 是模块文件
use report::{Method, Report}; 是将模块中的具体项引入到当前作用域
pub mod report;
use report::{Method, Report};
源码解读
从这里开始正式进入源码阅读环节,我们先从 README.md 开始
在 README.md 中,我们看到作者有些 Usage 的例子,我们就从这个例子开始
use human_panic::setup_panic;
fn main() {
setup_panic!();
println!("A normal log message");
panic!("OMG EVERYTHING IS ON FIRE!!!")
}
这个库的入口函数是 setup_panic!,这个是一个宏,我们先找到这个宏的测试用例
通过搜索我们在 tests/single-panic/src/main.rs 中找到了这个宏的测试用例
通过跳转进入 src/lib.rs 文件,我们找到了这个宏的定义
setup_panic!
#[macro_export] 是一个属性标记,表示这个宏可以被其他 crate 使用
macro_rules! 是一个宏定义,setup_panic! 是宏的名称,因为宏可以实现函数重载,所以这个宏可以有多个变体
默认情况下,进入变体 2,也就是无需传递任何参数,作者会调用 metadata! 宏获取默认参数,然后调用 setup_panic 函数
#[macro_export]
macro_rules! setup_panic {
// 变体 1
($meta:expr) => {{
$crate::setup_panic(|| $meta);
}};
// 变体 2
() => {
$crate::setup_panic!($crate::metadata!());
};
}
metadata
在调用 setup_panic 时会传入一个元数据 metadata,metadata 是用于描述 crate 基本信息,属性有以下几个
pub struct Metadata {
name: Cow<'static, str>,
version: Cow<'static, str>,
authors: Option<Cow<'static, str>>,
homepage: Option<Cow<'static, str>>,
support: Option<Cow<'static, str>>,
}
然后给这个结构体实现关联方法
impl Metadata {
pub fn new(name: impl Into<Cow<'static, str>>, version: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
version: version.into(),
authors: None,
homepage: None,
support: None,
}
}
pub fn authors(mut self, value: impl Into<Cow<'static, str>>) -> Self {
let value = value.into();
if !value.is_empty() {
self.authors = value.into();
}
self
}
pub fn homepage(mut self, value: impl Into<Cow<'static, str>>) -> Self {
let value = value.into();
if !value.is_empty() {
self.homepage = value.into();
}
self
}
pub fn support(mut self, value: impl Into<Cow<'static, str>>) -> Self {
let value = value.into();
if !value.is_empty() {
self.support = value.into();
}
self
}
}
在宏里面调用方法,需要给那个方法实现宏
#[macro_export]
macro_rules! metadata {
() => {{
$crate::Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.authors(env!("CARGO_PKG_AUTHORS").replace(":", ", "))
.homepage(env!("CARGO_PKG_HOMEPAGE"))
}};
}
setup_panic
setup_panic 函数用模式匹配 PanicStyle,如果是 Debug 模式啥也不干,如果是Human 模式,就进入Human 分支,进行美化 panic
#[doc(hidden)]
pub fn setup_panic(meta: impl Fn() -> Metadata) {
use std::panic;
match PanicStyle::default() {
PanicStyle::Debug => {}
PanicStyle::Human => {
let meta = meta();
panic::set_hook(Box::new(move |info: &PanicInfo<'_>| {
let file_path = handle_dump(&meta, info);
print_msg(file_path, &meta)
.expect("human-panic: printing error message to console failed");
}));
}
}
}
当程序发生 panic,会调用 panic hook,来打印错误信息,我们通过这些 hook 才实现自定义处理函数
set_hook 就是其中之一
fn main() {
std::panic::set_hook(Box::new(|info| {
// 自定义处理逻辑
println!("这是自定义 panic 处理逻辑");
}));
panic!();
}
还有闭包前面的 move 关键字的作用
move 的作用是把闭包捕获的所有外部变量的所有权转移到闭包内部,这里是 meta 变量
PanicStyle
PanicStyle 是一个枚举类型,用于控制 panic 时的输出风格
它上面定义了两个属性标记:
#[non_exhaustive]- 表示这个枚举是非穷尽的,即未来可能会添加新的变体,强制用户处理未知情况,通常使用_通配符#[derive(Copy, Clone, PartialEq, Eq)]- 表示这个枚举可以进行复制、克隆、比较
接着为 PanicStyle 实现了 Default trait,这样就可以使用 PanicStyle::default() 来获取默认值
在 Default trait 的实现中,首先判断是否是 debug 模式,如果是则返回 PanicStyle::Debug,否则判断环境变量 RUST_BACKTRACE 是否存在,如果存在则返回 PanicStyle::Debug,否则返回 PanicStyle::Human
#[non_exhaustive]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum PanicStyle {
Debug,
Human,
}
impl Default for PanicStyle {
fn default() -> Self {
if cfg!(debug_assertions) {
PanicStyle::Debug
} else {
match ::std::env::var("RUST_BACKTRACE") {
Ok(_) => PanicStyle::Debug,
Err(_) => PanicStyle::Human,
}
}
}
}
handle_dump
handle_dump 第一个代码段
#[cfg(feature = "nightly")]和#[cfg(not(feature = "nightly"))]是条件编译,针对不同版本做的兼容,nightly是一个特性,表示这段代码只在nightly版本下编译,否则编译另一段代码- 模式匹配可以匹配多个值
let (x, y) = (1, 2); match (x, y) { (1, 2) => println!("匹配到 1,2"), (1, _) => println!("x 是 1,y 是任意值"), (_, 2) => println!("x 是任意值,y 是 2"), _ => println!("其他情况"), } downcast_ref是一个trait,用于将Any类型的对象转换为具体类型的引用,如果转换失败则返回None
#[cfg(feature = "nightly")]
let message = panic_info.message().map(|m| format!("{}", m));
#[cfg(not(feature = "nightly"))]
let message = match (
panic_info.payload().downcast_ref::<&str>(),
panic_info.payload().downcast_ref::<String>(),
) {
(Some(s), _) => Some((*s).to_owned()),
(_, Some(s)) => Some(s.to_owned()),
(None, None) => None,
};
再来看 handle_dump 的第二个代码段
panic_info.location() 返回一个 Option,如果 Some 则返回文件名和行号,否则返回 None
location.file()获取发生panic的文件名location.line()获取发生panic的行号
然后格式化后添加到 expl 字符串中
match panic_info.location() {
Some(location) => expl.push_str(&format!(
"Panic occurred in file '{}' at line {}\n",
location.file(),
location.line()
)),
None => expl.push_str("Panic location unknown.\n"),
}
第三个代码段是输出 backtrace,通过调用 Report::new 创建一个 Report 实例,然后调用 persist 方法持久化
let report = Report::new(&meta.name, &meta.version, Method::Panic, expl, cause);
if let Ok(f) = report.persist() {
Some(f)
} else {
use std::io::Write as _;
let stderr = std::io::stderr();
let mut stderr = stderr.lock();
let _ = writeln!(
stderr,
"{}",
report
.serialize()
.expect("only doing toml compatible types")
);
None
}
Report
Report 用于 panic 信息的序列化和持久化,或打印给用户
Report 包含下面这些属性
pub struct Report {
name: String,
operating_system: String,
crate_version: String,
explanation: String,
cause: String,
method: Method,
backtrace: String,
}
提供三个关联方法:
new- 创建一个新的Report实例serialize- 序列化Report实例persist- 持久化Report实例
impl Report {
pub fn new(
name: &str,
version: &str,
method: Method,
explanation: String,
cause: String,
) -> Self {
let operating_system = os_info::get().to_string();
let backtrace = render_backtrace();
Self {
crate_version: version.into(),
name: name.into(),
operating_system,
method,
explanation,
cause,
backtrace,
}
}
pub fn serialize(&self) -> Option<String> {
toml::to_string_pretty(&self).ok()
}
pub fn persist(&self) -> Result<PathBuf, Box<dyn Error + 'static>> {
let uuid = Uuid::new_v4().hyphenated().to_string();
let tmp_dir = env::temp_dir();
let file_name = format!("report-{}.toml", &uuid);
let file_path = Path::new(&tmp_dir).join(file_name);
let toml = self.serialize().expect("only using toml-compatible types");
std::fs::write(&file_path, toml.as_bytes())?;
Ok(file_path)
}
}
print_msg
print_msg 用于打印 panic 信息,如果开启了 color 特性,则打印红色,否则不打印颜色
#[cfg(feature = "color")] 和 #[cfg(not(feature = "color"))] 是条件编译,针对不同特性做的兼容
feature 是 Cargo.toml 中的一个配置,如果用户启用了 color 特性,则会去 Cargo.toml 中查找 features 配置
[features]
default = ["color"]
nightly = []
color = ["dep:anstyle", "dep:anstream"]
其中方式:
// 默认使用 color feature
cargo build
// 禁用 color feature
cargo build --no-default-features
// 显式启用 color feature
cargo build --features color
写入信息交给 write_msg 函数
#[cfg(feature = "color")]
pub fn print_msg<P: AsRef<Path>>(file_path: Option<P>, meta: &Metadata) -> IoResult<()> {
use std::io::Write as _;
let stderr = anstream::stderr();
let mut stderr = stderr.lock();
write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg())?;
write_msg(&mut stderr, file_path, meta)?;
write!(stderr, "{}", anstyle::Reset.render())?;
Ok(())
}
#[cfg(not(feature = "color"))]
pub fn print_msg<P: AsRef<Path>>(file_path: Option<P>, meta: &Metadata) -> IoResult<()> {
let stderr = std::io::stderr();
let mut stderr = stderr.lock();
write_msg(&mut stderr, file_path, meta)?;
Ok(())
}
write_msg
write_msg 用于写入 panic,在结构结构体时 .. 表示忽略剩余字段
fn write_msg<P: AsRef<Path>>(
buffer: &mut impl std::io::Write,
file_path: Option<P>,
meta: &Metadata,
) -> IoResult<()> {
let Metadata {
name,
authors,
homepage,
support,
..
} = meta;
writeln!(buffer, "Well, this is embarrassing.\n")?;
writeln!(
buffer,
"{name} had a problem and crashed. To help us diagnose the \
problem you can send us a crash report.\n"
)?;
writeln!(
buffer,
"We have generated a report file at \"{}\". Submit an \
issue or email with the subject of \"{} Crash Report\" and include the \
report as an attachment.\n",
match file_path {
Some(fp) => format!("{}", fp.as_ref().display()),
None => "<Failed to store file to disk>".to_owned(),
},
name
)?;
if let Some(homepage) = homepage {
writeln!(buffer, "- Homepage: {homepage}")?;
}
if let Some(authors) = authors {
writeln!(buffer, "- Authors: {authors}")?;
}
if let Some(support) = support {
writeln!(buffer, "\nTo submit the crash report:\n\n{support}")?;
}
writeln!(
buffer,
"\nWe take privacy seriously, and do not perform any \
automated error collection. In order to improve the software, we rely on \
people to submit reports.\n"
)?;
writeln!(buffer, "Thank you kindly!")?;
Ok(())
}
源码到此基本就结束了
测试
snapbox 是 rust 测试工具库
snapbox = { version = "0.6.4", features = ["cmd", "dir"] }
features = ["cmd", "dir"]:表示启用两个可选功能:
cmd:启用命令执行相关功能dir:启用目录和文件操作相关功能
// 创建一个临时目录
let root = snapbox::dir::DirRoot::mutable_temp().unwrap();
// ...
// 测试结束时显式清理临时目录
root.close().unwrap();
#[cfg(unix] 表示只在 unix 系统下编译,#[cfg(not(unix))] 表示在非 unix 系统下编译
因为 Unix 和 Windows 系统处理临时文件的方式不同,Unix 系统依赖 TMPDIR 环境变量,Windows 系统有其他机制来处理临时目录
#[cfg(unix)]
let envs = [("TMPDIR", root_path)];
#[cfg(not(unix))]
let envs: [(&str, &str); 0] = [];
cargo_bin!("single-panic-test") 用于获取 cargo 二进制文件的路径,它会在 Cargo 的构建目录(通常是 target/debug/ 或 target/release/)中查找名为 "single-panic-test" 的可执行文件
snapbox::cmd::Command::new(snapbox::cmd::cargo_bin!("single-panic-test"))
snapbox::str! 是一个宏,用于定义期望的输出格式,r#"..."# 是 rust 的原始字符串语法,可以包含多行和特殊字符,[..] 是通配符,表示这部分内容可以变化
snapbox::str![[r#"
"name" = "single-panic-test" // 测试名称
"operating_system" = "[..]" // [..] 表示匹配任何操作系统
"crate_version" = "0.1.0" // crate 版本
"explanation" = """ // panic 发生的位置说明
Panic occurred in file 'tests/single-panic/src/main.rs' at line [..]
"""
"cause" = "OMG EVERYTHING IS ON FIRE!!!" // panic 的具体消息
"method" = "Panic" // panic 的方法
"backtrace" = """ // 调用栈信息
...
"""
"#]
总结
学 rust 语言看的第一个库,学到的东西有很多,除了 rust 基本语法外,所有的都是新知识,每遇到一个都要问去搜索一下