1. 调试工具

1.1 日志信息

  需要安装logenv_logger2个库,可通过cargo add logcargo add env_logger命令安装

[dependencies]
env_logger = "0.10.0" # 半年未更新
log = "0.4.17" # 一年未更新

1.1.1 记录调试信息到控制台

  log crate 提供了日志工具,env_logger crate 通过环境变量配置日志记录。log::debug! 宏的工作方式类似于其它 std::fmt 格式化的字符串。

fn execute_query(query: &str) {
log::debug!("执行操作: {}", query);
}

fn main() {
env_logger::init();

execute_query("删除学生表");
}
  • 运行RUST_LOG=debug cargo run输出
[2023-04-17T13:03:21Z DEBUG development] 执行操作: 删除学生表

1.1.2 记录错误信息到控制台

  正确的错误处理会将异常视为错误。下述实例中,通过 log 便捷宏 log::error!,将错误记录到 stderr

fn execute_query(_query: &str) -> Result<(), &'static str> {
Err("恐怕做不到")
}

fn main() {
env_logger::init();

let response = execute_query("DROP TABLE students");
if let Err(err) = response {
log::error!("执行操作失败: {}", err);
}
}
  • 运行cargo run输出
[2023-04-17T13:07:33Z ERROR development] 执行操作失败: 恐怕做不到

1.1.3 记录信息时,用标准输出 stdout 替换标准错误 stderr

  使用 Builder::target 创建自定义的日志记录器配置,将日志输出的目标设置为 Target::Stdout

use env_logger::{Builder, Target};

fn main() {
Builder::new().target(Target::Stdout).init();

log::error!("此错误已打印到标准输出");
}
  • 运行cargo run输出
[2023-04-17T13:10:37Z ERROR development] 此错误已打印到标准输出

1.1.4 记录信息时,用标准输出 stdout 替换标准错误 stderr

  实现一个打印到 stdout 的自定义记录器 ConsoleLogger。为了使用日志宏,ConsoleLogger实现了 log::Log trait,通过 log::set_logger 安置。

use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};

static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger;

struct ConsoleLogger;

impl log::Log for ConsoleLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Info
}

fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
println!("Rust says: {} - {}", record.level(), record.args());
}
}

fn flush(&self) {}
}

fn main() -> Result<(), SetLoggerError> {
log::set_logger(&CONSOLE_LOGGER)?;
log::set_max_level(LevelFilter::Info);

log::info!("信息日志");
log::warn!("警告日志");
log::error!("错误日志");
Ok(())
}
  • 运行cargo run输出
Rust says: INFO - 信息日志
Rust says: WARN - 警告日志
Rust says: ERROR - 错误日志

1.1.5 记录到 Unix 系统日志

  需要额外安装syslog库,可通过cargo add syslog命令安装

[dependencies]
env_logger = "0.10.0" # 半年未更新
log = "0.4.17" # 一年未更新
syslog = "6.0.1" # 一年未更新

  实现将信息记录到 UNIX syslog。使用 syslog::init 初始化记录器后端。syslog::Facility 记录提交日志项分类的程序,log::LevelFilter 表示欲记录日志的等级,Option<&str> 定义应用程序名称(可选)。

#[cfg(target_os = "linux")]
#[cfg(target_os = "linux")]
use syslog::{Error, Facility};

#[cfg(target_os = "linux")]
fn main() -> Result<(), Error> {
syslog::init(
Facility::LOG_USER,
log::LevelFilter::Debug,
Some("测试系统日志"),
)?;
log::debug!("这是debug信息 {}", "message");
log::error!("这是一个Error信息!");
Ok(())
}

#[cfg(not(target_os = "linux"))]
fn main() {
println!("So far, only Linux systems are supported.");
}
  • 运行cargo run输出到系统日志,可通过tail -f /var/log/syslog系统命令查看输出的日志内容,如下
Apr 17 14:50:34 dynasty dynasty 测试系统日志[838264]: 这是debug信息 message
Apr 17 14:50:34 dynasty dynasty 测试系统日志[838264]: 这是一个Error信息!

1.2 日志配置

1.2.1 启用每个模块的日志级别

  创建两个模块:foo 和其嵌套的 foo::bar,日志记录指令分别由 RUST_LOG 环境变量控制。

mod foo {
mod bar {
pub fn run() {
log::warn!("[bar] warn");
log::info!("[bar] info");
log::debug!("[bar] debug");
}
}

pub fn run() {
log::warn!("[foo] warn");
log::info!("[foo] info");
log::debug!("[foo] debug");
bar::run();
}
}

fn main() {
env_logger::init();
log::warn!("[root] warn");
log::info!("[root] info");
log::debug!("[root] debug");
foo::run();
}
  • 运行RUST_LOG="warn,development::foo=info,development::foo::bar=debug" cargo run(注意这块的development为项目名称)命令,输出如下:
[2023-04-17T15:00:37Z WARN  development] [root] warn
[2023-04-17T15:00:37Z WARN development::foo] [foo] warn
[2023-04-17T15:00:37Z INFO development::foo] [foo] info
[2023-04-17T15:00:37Z WARN development::foo::bar] [bar] warn
[2023-04-17T15:00:37Z INFO development::foo::bar] [bar] info
[2023-04-17T15:00:37Z DEBUG development::foo::bar] [bar] debug

1.2.2 用自定义环境变量设置日志记录

  Builder 配置日志记录。Builder::parseRUST_LOG 语法的形式解析 MY_APP_LOG 环境变量的内容。然后,Builder::init 初始化记录器。所有这些步骤通常由 env_logger::init 在内部完成。

use env_logger::Builder;
use std::env;

fn main() {
Builder::new()
.parse_filters(&env::var("MY_APP_LOG").unwrap_or_default()) //parse_filters代替parse方法
.init();
log::info!("信息消息");
log::warn!("警告消息");
log::error!("这是个错误 {}", "message");
}
  • 运行MY_APP_LOG=info cargo run命令,输出如下:
[2023-04-18T13:41:21Z INFO  development] 信息消息
[2023-04-18T13:41:21Z WARN development] 警告消息
[2023-04-18T13:41:21Z ERROR development] 这是个错误 message

1.2.3 在日志信息中包含时间戳

  需要额外安装chrono库,可通过cargo add chrono命令安装

[dependencies]
env_logger = "0.10.0" # 半年未更新
log = "0.4.17" # 一年未更新
chrono = "0.4.24"

  使用 Builder 创建自定义记录器配置。每个日志项调用 Local::now 以获取本地时区中的当前 DateTime,并使用 DateTime::formatstrftime::specifiers 来格式化最终日志中使用的时间戳。如下实例调用 Builder::format 设置一个闭包,该闭包用时间戳、Record::level 和正文(Record::args)对每个信息文本进行格式化。

use chrono::Local;
use env_logger::Builder;
use log::LevelFilter;
use std::io::Write;

fn main() {
Builder::new()
.format(|buf, record| {
writeln!(
buf,
"{} [{}] - {}",
Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.args()
)
})
.filter(None, LevelFilter::Info)
.init();

log::warn!("warn");
log::info!("info");
log::debug!("debug");
}
  • 运行cargo run命令,输出如下:
2023-04-18 13:47:50 [WARN] - warn
2023-04-18 13:47:50 [INFO] - info

1.2.4 将信息记录到自定义位置

  需要额外安装log4rserror_chain库,可通过cargo add log4rscargo add error_chain命令安装

[dependencies]
error-chain = "0.12.4"
log = "0.4.17" # 一年未更新
log4rs = "1.2.0"

  使用 Builder 创建自定义记录器配置。每个日志项调用 Local::now 以获取本地时区中的当前 DateTime,并使用 DateTime::formatstrftime::specifiers 来格式化最终日志中使用的时间戳。如下实例调用 Builder::format 设置一个闭包,该闭包用时间戳、Record::level 和正文(Record::args)对每个信息文本进行格式化。

use error_chain::error_chain;

use log::LevelFilter;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Config, Root};
use log4rs::encode::pattern::PatternEncoder;

error_chain! {
foreign_links {
Io(std::io::Error);
LogConfig(log4rs::config::FormatError);
SetLogger(log::SetLoggerError);
}
}

fn main() -> Result<()> {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} -[{l}]: {m}{n}")))
.build("log/output.log")
.unwrap();

let config = Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.build(Root::builder().appender("logfile").build(LevelFilter::Info))
.unwrap();

log4rs::init_config(config)?;

log::info!("输出日志");

Ok(())
}
  • 运行cargo run命令生成log日志,可通过cat log/output.log命令查看输出的日志:
2023-04-18T14:03:39.703966199+00:00 -[INFO]: 输出日志

2. 版本控制

  需要安装semver库,可通过cargo add semver命令安装

[dependencies]
semver = "1.0.17" # api 变化大

2.1 解析并递增版本字符串

  使用 Version::parse 从字符串字面量构造语义化版本 semver::Version,然后逐个递增补丁(修订)版本号、副(次要)版本号和主版本号。**注意:**根据语义化版本控制规范,增加副(次要)版本号时会将补丁(修订)版本号重置为 0,增加主版本号时会将副(次要)版本号和补丁(修订)版本号都重置为 0

use semver::{BuildMetadata, Error, Prerelease, Version};

fn main() -> Result<(), Error> {
let mut parsed_version = Version::parse("0.2.6")?;

assert_eq!(
parsed_version,
Version {
major: 0,
minor: 2,
patch: 6,
pre: Prerelease::EMPTY,
build: BuildMetadata::EMPTY,
}
);

// parsed_version.increment_patch(); // 开发者删除该实现,需要用户自己实现
increment_patch(&mut parsed_version);
assert_eq!(parsed_version.to_string(), "0.2.7");
println!("新的补丁发布: v{}", parsed_version);

// parsed_version.increment_minor(); // 开发者删除该实现,需要用户自己实现
increment_minor(&mut parsed_version);
assert_eq!(parsed_version.to_string(), "0.3.0");
println!("新的次要版本: v{}", parsed_version);

// parsed_version.increment_major(); // 开发者删除该实现,需要用户自己实现
increment_major(&mut parsed_version);
assert_eq!(parsed_version.to_string(), "1.0.0");
println!("新的主要版本: v{}", parsed_version);

Ok(())
}

fn increment_patch(v: &mut Version) {
v.patch += 1;
v.pre = Prerelease::EMPTY;
v.build = BuildMetadata::EMPTY;
}

fn increment_minor(v: &mut Version) {
v.minor += 1;
v.patch = 0;
v.pre = Prerelease::EMPTY;
v.build = BuildMetadata::EMPTY;
}

fn increment_major(v: &mut Version) {
v.major += 1;
v.minor = 0;
v.patch = 0;
v.pre = Prerelease::EMPTY;
v.build = BuildMetadata::EMPTY;
}
  • 运行cargo run输出
新的补丁发布: v0.2.7
新的次要版本: v0.3.0
新的主要版本: v1.0.0

2.2 解析复杂的版本字符串

  使用 Version::parse 从复杂的版本字符串构造语义化版本 semver::Version。该字符串包含语义化版本控制规范中定义的预发布和构建元数据。需要注意的是:根据语义化版本控制规范,构建元数据是虽然被解析,但在比较版本时不考虑。换句话说,即使两个版本的构建字符串不同,但它们的版本可能是相等的。

use semver::{BuildMetadata, Comparator, Error, Prerelease, Version};

fn main() -> Result<(), Error> {
let version_str = "1.0.49-125+g72ee7853";

// 不考虑build版本的比较
let comparator_version = Comparator::parse(version_str)?;

Comparator::matches(
&comparator_version,
&Version {
major: 1,
minor: 0,
patch: 49,
pre: Prerelease::new("125")?,
build: BuildMetadata::EMPTY, // 不考虑build版本的比较
},
);

// // 将版本号字符串解析为完整的版本号对象,考虑build版本的比较
let parsed_version = Version::parse(version_str)?;
assert_eq!(parsed_version.build, BuildMetadata::new("g72ee7853")?);

let serialized_version = parsed_version.to_string();
assert_eq!(&serialized_version, version_str);

Ok(())
}
  • 运行cargo run校验

2.3 检查给定版本是否为预发布版本

  给定两个版本,使用 is_prerelease 断言一个是预发布,另一个不是。

use semver::{Error, Version};

fn main() -> Result<(), Error> {
let version_1 = Version::parse("1.0.0-alpha")?;
let version_2 = Version::parse("1.0.0")?;

// 原函数is_prerelease已删除通过version_1.pre判断
assert!(!version_1.pre.is_empty()); // 断言version1为预发布版本
assert!(version_2.pre.is_empty()); // 断言version2不是预发布版本

Ok(())
}
  • 运行cargo run校验

2.4 查询适配给定范围的最新版本

  给定两个版本,使用 is_prerelease 断言一个是预发布,另一个不是。

use semver::{Error, Version};

fn main() -> Result<(), Error> {
let version_1 = Version::parse("1.0.0-alpha")?;
let version_2 = Version::parse("1.0.0")?;

// 原函数is_prerelease已删除通过version_1.pre判断
assert!(!version_1.pre.is_empty()); // 断言version1为预发布版本
assert!(version_2.pre.is_empty()); // 断言version2不是预发布版本

Ok(())
}
  • 运行cargo run校验

2.5 查询适配给定范围的最新版本

  给定一个版本字符串 &str 的列表,查找最新的语义化版本 semver::Versionsemver::VersionReqVersionReq::matches 过滤列表,也可以展示语义化版本 semver 的预发布参数设置。

use error_chain::error_chain;

use semver::{Version, VersionReq};

error_chain! {
foreign_links {
SemVer(semver::Error);
// SemVerReq(semver::Error);
}
}

fn find_max_matching_version<'a, I>(version_req_str: &str, iterable: I) -> Result<Option<Version>>
where
I: IntoIterator<Item = &'a str>,
{
let vreq = VersionReq::parse(version_req_str)?;

Ok(iterable
.into_iter()
.filter_map(|s| Version::parse(s).ok())
.filter(|s| vreq.matches(s))
.max())
}

fn main() -> Result<()> {
assert_eq!(
find_max_matching_version("<= 1.0.0", vec!["0.9.0", "1.0.0", "1.0.1"])?,
Some(Version::parse("1.0.0")?)
);

assert_eq!(
find_max_matching_version(
">1.2.3-alpha.3",
vec![
"1.2.3-alpha.3",
"1.2.3-alpha.4",
"1.2.3-alpha.10",
"1.2.3-beta.4",
"3.4.5-alpha.9",
]
)?,
Some(Version::parse("1.2.3-beta.4")?)
);

Ok(())
}
  • 运行cargo run校验

2.6 检查外部命令的版本兼容性

  实例使用 Command 模块运行命令 git --version,然后使用 Version::parse 将版本号解析为语义化版本 semver::VersionVersionReq::matchessemver::VersionReq 与解析的语义化版本进行比较。最终,命令输出类似于“git version x.y.z

use error_chain::error_chain;

use semver::{Version, VersionReq};
use std::process::Command;

error_chain! {
foreign_links {
Io(std::io::Error);
Utf8(std::string::FromUtf8Error);
SemVer(semver::Error);
}
}

fn main() -> Result<()> {
let version_constraint = "> 1.12.0";
let version_test = VersionReq::parse(version_constraint)?;
let output = Command::new("git").arg("--version").output()?;

if !output.status.success() {
error_chain::bail!("执行命令失败错误代码");
}

let stdout = String::from_utf8(output.stdout)?;
let version = stdout
.trim() //处理多余的回车符号
.split(" ")
.last()
.ok_or_else(|| "命令输出无效")?;
println!("{}", version);
let parsed_version = Version::parse(version)?;

if !version_test.matches(&parsed_version) {
error_chain::bail!(
"命令版本低于最低支持版本 (找到 {}, 需要 {})",
parsed_version,
version_constraint
);
}

Ok(())
}
  • 运行cargo run输出
2.34.1

3. 构建工具

  本节介绍在编译 crate 源代码之前运行的“构建时”工具或代码。按照惯例,构建时代码存放在 build.rs 文件,通常称为“构建脚本”。常见的用例包括:Rust 代码生成、绑定的 C/C++/asm代码的编译。要获取更多信息,请阅读(Cargo 手册 中文版) 的构建脚本文档。

3.1 编译并静态链接到绑定的 C 语言库

  为了适应项目中需要混合 C、C++,或 asm等语言的场景,cc crate 提供了一个简单的 API,用于将绑定的 C/C++/asm 代码编译成静态库(.a),静态库可以通过 rustc 静态链接。下面的实例有一些绑定的 C 语言代码(src/hello.c),将从 rust 中调用它们。在编译 rust 源代码之前,Cargo.toml中指定的“构建”文件(build.rs)预先运行。使用 cc crate,将生成一个静态库文件(本实例中为 libhello.a,请参阅 compile 文档),通过在 extern 代码块中声明外部函数签名,然后就可以从 rust 中调用该静态库。
  本实例中绑定的 C 语言文件非常简单,只需要将一个源文件传递给 cc::Build。对于更复杂的构建需求,cc::Build 提供了一整套构建器方法,用于指定(包含)include路径和扩展编译器标志(flag)。

  • 需要安装cc库,Cargo.toml文件内容配置如下:
[package]
...
build = "build.rs"

[build-dependencies]
cc = "1"

[dependencies]
error-chain = "0.12.4"
  • build.rs文件内容如下:
fn main() {
cc::Build::new()
.file("src/hello.c")
.compile("hello"); // 输出 `libhello.a`
}
  • src/hello.c文件内容如下:
#include <stdio.h>

void hello() {
printf("Hello from C!\n");
}

void greet(const char* name) {
printf("你好, %s!\n", name);
}
  • src/main.rs文件内容如下:
use error_chain::error_chain;
use std::ffi::CString;
use std::os::raw::c_char;

error_chain! {
foreign_links {
NulError(::std::ffi::NulError);
Io(::std::io::Error);
}
}
fn prompt(s: &str) -> Result<String> {
use std::io::Write;
print!("{}", s);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}

extern "C" {
fn hello();
fn greet(name: *const c_char);
}

fn main() -> Result<()> {
unsafe { hello() }
let name = prompt("你娃叫啥? ")?;
let c_name = CString::new(name)?;
unsafe { greet(c_name.as_ptr()) }
Ok(())
}
  • 运行cargo run,输入Rust。输出如下:
Hello from C!
你娃叫啥? Rust
你好, Rust!
  • 注:这里修改src/hello.c文件内容后,需要同时修改build.rs中的内容(可随意修改后撤回并Ctrl+S保存),否则运行时无法获取最新修改后的信息。

3.2 编译并静态链接到绑定的 C++ 语言库

  链接绑定的 C++ 语言库非常类似于链接绑定的 C 语言库。编译并静态链接绑定的 C++ 库时,与链接绑定的 C 语言库相比有两个核心区别:一是通过构造器方法 cpp(true) 指定 C++ 编译器;二是通过在 C++ 源文件顶部添加 extern "C" 代码段,以防止 C++ 编译器的名称篡改。

  • Cargo.toml文件内容配置如下:
[package]
...
build = "build.rs"

[build-dependencies]
cc = "1"
  • build.rs文件内容如下:
fn main() {
cc::Build::new()
.cpp(true)
.file("src/foo.cpp")
.compile("foo");
}
  • src/foo.cpp文件内容如下:
extern "C" {
int multiply(int x, int y);
}

int multiply(int x, int y) {
return x*y;
}
  • src/main.rs文件内容如下:
extern "C" {
fn multiply(x: i32, y: i32) -> i32;
}

fn main() {
unsafe {
println!("乘积为:{}", multiply(5, 7));
}
}
  • 运行cargo run输出
乘积为:35

3.3 编译 C 语言库时自定义设置

  使用 cc::Build::define自定义构建绑定的 C 语言代码非常简单。该方法接受 Option 值,因此可以创建这样的定义:#define APP_NAME "foo"#define WELCOME(将 None 作为不确定值传递)。如下实例构建了一个绑定的 C 语言文件,其在 build.rs 中设置了动态定义,并在运行时打印 “Welcome to foo - version 1.0.2”。Cargo 设定了一些环境变量,这些变量可能对某些自定义设置有用。

  • Cargo.toml文件内容配置如下:
[package]
...
version = "0.1.0"
build = "build.rs"

[build-dependencies]
cc = "1"
  • build.rs文件内容如下:
fn main() {
cc::Build::new()
.define("APP_NAME", "\"无名\"")
.define(
"VERSION",
format!("\"{}\"", env!("CARGO_PKG_VERSION")).as_str(),
)
.define("WELCOME", None)
.file("src/foo.c")
.compile("foo");
}
  • src/foo.c文件内容如下:
#include <stdio.h>

void print_app_info() {
#ifdef WELCOME
printf("欢迎:");
#endif
printf("%s - 版本 %s\n", APP_NAME, VERSION);
}
  • src/main.rs文件内容如下:
extern "C" {
fn print_app_info();
}

fn main() {
unsafe {
print_app_info();
}
}
  • 运行cargo run输出
欢迎:无名 - 版本 0.1.0