1. 调试工具
1.1 日志信息
需要安装log
、env_logger
2个库,可通过cargo add log
、cargo 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); } }
[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!("此错误已打印到标准输出" ); }
[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 (()) }
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::parse
以 RUST_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 ()) .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::format
和 strftime::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" ); }
2023-04-18 13:47:50 [WARN] - warn 2023-04-18 13:47:50 [INFO] - info
1.2.4 将信息记录到自定义位置
需要额外安装log4rs
和error_chain
库,可通过cargo add log4rs
、cargo add error_chain
命令安装
[dependencies] error-chain = "0.12.4" log = "0.4.17" log4rs = "1.2.0"
使用 Builder
创建自定义记录器配置。每个日志项调用 Local::now
以获取本地时区中的当前 DateTime
,并使用 DateTime::format
和 strftime::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"
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, } ); increment_patch (&mut parsed_version); assert_eq! (parsed_version.to_string (), "0.2.7" ); println! ("新的补丁发布: v{}" , parsed_version); increment_minor (&mut parsed_version); assert_eq! (parsed_version.to_string (), "0.3.0" ); println! ("新的次要版本: v{}" , parsed_version); 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; }
新的补丁发布: 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" ; 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, }, ); 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 (()) }
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" )?; assert! (!version_1.pre.is_empty ()); assert! (version_2.pre.is_empty ()); Ok (()) }
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" )?; assert! (!version_1.pre.is_empty ()); assert! (version_2.pre.is_empty ()); Ok (()) }
2.5 查询适配给定范围的最新版本
给定一个版本字符串 &str
的列表,查找最新的语义化版本 semver::Version
。semver::VersionReq
用 VersionReq::matches
过滤列表,也可以展示语义化版本 semver
的预发布参数设置。
use error_chain::error_chain;use semver::{Version, VersionReq};error_chain! { foreign_links { SemVer (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 (()) }
2.6 检查外部命令的版本兼容性
实例使用 Command
模块运行命令 git --version
,然后使用 Version::parse
将版本号解析为语义化版本 semver::Version
。VersionReq::matches
将 semver::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 (()) }
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"
fn main () { cc::Build::new () .file ("src/hello.c" ) .compile ("hello" ); }
#include <stdio.h> void hello () { printf ("Hello from C!\n" ); } void greet (const char * name) { printf ("你好, %s!\n" , name); }
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 (()) }
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++ 编译器的名称篡改。
[package] ... build = "build.rs" [build-dependencies] cc = "1"
fn main () { cc::Build::new () .cpp (true ) .file ("src/foo.cpp" ) .compile ("foo" ); }
extern "C" { int multiply (int x, int y) ; } int multiply (int x, int y) { return x*y; }
extern "C" { fn multiply (x: i32 , y: i32 ) -> i32 ; } fn main () { unsafe { println! ("乘积为:{}" , multiply (5 , 7 )); } }
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 设定了一些环境变量,这些变量可能对某些自定义设置有用。
[package] ... version = "0.1.0" build = "build.rs" [build-dependencies] cc = "1"
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" ); }
#include <stdio.h> void print_app_info () {#ifdef WELCOME printf ("欢迎:" ); #endif printf ("%s - 版本 %s\n" , APP_NAME, VERSION); }
extern "C" { fn print_app_info (); } fn main () { unsafe { print_app_info (); } }