Rust in Everything
基于 tracing 0.1.44

宏展开

主线位置:两条主线的第一步。事件主线上,info! 在调用点展开成静态的元数据和调用点、一串判断、一次 Event::dispatch;span 主线上,info_span! 展开成同样的静态数据和判断,最后是 Span::new。判断里每一道门各自的代价在下一页 级别过滤的几道门。

flowchart TB
  info["info! 调用"] --> ev1["event!:补上 target"]
  ev1 --> ev2["event!:字段包进花括号"]
  ev2 --> body["event! 主体"]
  body --> cs["callsite2!:两个 static"]
  cs --> fs["fieldset!:字段名数组"]
  body --> le["level_enabled!:门一门二"]
  body --> vs["valueset_all!:字段值数组"]
  body --> tl["__tracing_log!:log feature"]

tracing 的这些宏(#[instrument] 是 tracing-attributes 的过程宏,不算在内)都是 macro_rules!,在 tracing/src/macros.rs 里,加上 tracing-core 里的 metadata! 和 identify_callsite! 两个(在 tracing-core 的 lib.rs,tracing-core 没有 macros.rs 这个文件)。展开后调用的函数在 tracing 的 __macro_support 模块里,它是 pub 的,因为宏展开在用户的 crate 里,但注释写明「This type, and all code contained in the __macro_support module, is a private API of tracing」,小版本之间可能不兼容。

本页先看两段真实的展开,再看字段写法,最后解释这个文件为什么这么长。展开出来的类型(Metadata、DefaultCallsite、ValueSet)在 tracing-core 一部分讲过,这里只说宏怎么把它们拼起来。

看展开的办法

下面的展开都用 cargo-expand 1.0.104 得到,依赖只有:

tracing = "=0.1.44"

注意没有开 tracing 的 log feature,开了展开会不一样(见本页最后一节)。程序:

fn main() {
  let x = 1;
  tracing::info!("x = {}", x);
  let _span = tracing::info_span!("req", id = 1);
}

cargo expand --bin expand_me 输出 123 行,下面逐段看。

info! 一路转发到 event!

info! 本身不生成代码,只是把参数改写后交给 event!。展开过程可以用 rustc 的 trace_macros! 打出来(它是不稳定特性,这里借 RUSTC_BOOTSTRAP=1 在 rustc 1.99.0 上用一次;只保留 expanding 行,跨行的只留第一行):

  = note: expanding `info! { "x = {}", x }`
  = note: expanding `event! { target : module_path! (), $crate :: Level :: INFO, "x = {}", x }`
  = note: expanding `event! { target : module_path! (), $crate :: Level :: INFO, { "x = {}", x } }`
  = note: expanding `callsite2! { name : $crate :: __macro_support :: concat!
  = note: expanding `metadata! { name :
  = note: expanding `fieldset! { "x = {}", x }`
  = note: expanding `fieldset! { @ {} "x = {}", x, }`
  = note: expanding `fieldset! { @ { "message", , } }`
  = note: expanding `identify_callsite! { & __CALLSITE }`
  = note: expanding `level_enabled! { $crate :: Level :: INFO }`
  = note: expanding `__tracing_log! { $crate :: Level :: INFO, __CALLSITE, & value_set }`
  = note: expanding `valueset_all! { __CALLSITE.metadata().fields(), "x = {}", x }`
  = note: expanding `valueset_all! { @ {}, "x = {}", x }`
  = note: expanding `valueset_all! { @
  = note: expanding `__tracing_log! { $crate :: Level :: INFO, __CALLSITE, & $crate :: valueset_all!

一条 info! 在编译期展开了 15 次宏。前三步是转发:

  1. info! 的最后一个分支 ($($arg:tt)+) 接住一切没被前面分支认出来的输入,补上 target: module_path!() 和 Level::INFO。
  2. event! 的 (target: $target:expr, $lvl:expr, $($arg:tt)+ ) 把剩下的 token 原样包进一对花括号。
  3. 带花括号的 (target: $target:expr, $lvl:expr, { $($fields:tt)* } ) 才是真正生成代码的主体。

花括号是 event! 内部的约定:花括号里是「字段加可能的格式串」,这样主体分支只需要一个 $($fields:tt)*,不用再区分各种写法。格式串 "x = {}", x 也原样进了花括号,它变成 message 字段是后面 fieldset! 和 valueset_all! 的事。

info!("x = {}", x) 的展开

主体展开成下面这样(cargo expand 原样输出,只去掉了外层的 fn main 和 let x = 1;):

    {
        use ::tracing::__macro_support::Callsite as _;
        static __CALLSITE: ::tracing::callsite::DefaultCallsite = {
            static META: ::tracing::Metadata<'static> = {
                ::tracing_core::metadata::Metadata::new(
                    "event src/bin/expand_me.rs:3",
                    "expand_me",
                    ::tracing::Level::INFO,
                    ::tracing_core::__macro_support::Option::Some(
                        "src/bin/expand_me.rs",
                    ),
                    ::tracing_core::__macro_support::Option::Some(3u32),
                    ::tracing_core::__macro_support::Option::Some("expand_me"),
                    ::tracing_core::field::FieldSet::new(
                        &["message"],
                        ::tracing_core::callsite::Identifier(&__CALLSITE),
                    ),
                    ::tracing::metadata::Kind::EVENT,
                )
            };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
        let enabled = ::tracing::Level::INFO
            <= ::tracing::level_filters::STATIC_MAX_LEVEL
            && ::tracing::Level::INFO <= ::tracing::level_filters::LevelFilter::current()
            && {
                let interest = __CALLSITE.interest();
                !interest.is_never()
                    && ::tracing::__macro_support::__is_enabled(
                        __CALLSITE.metadata(),
                        interest,
                    )
            };
        if enabled {
            (|value_set: ::tracing::field::ValueSet| {
                let meta = __CALLSITE.metadata();
                ::tracing::Event::dispatch(meta, &value_set);
            })({
                #[allow(unused_imports)]
                use ::tracing::field::{debug, display, Value};
                __CALLSITE
                    .metadata()
                    .fields()
                    .value_set_all(
                        &[
                            (::tracing::__macro_support::Option::Some(
                                &format_args!("x = {0}", x) as &dyn ::tracing::field::Value,
                            )),
                        ],
                    )
            });
        } else {
        }
    };

分四块看。

两个 static。 callsite2! 生成 static __CALLSITE 和嵌在它初始化表达式里的 static META,META 的 FieldSet 里存着 &__CALLSITE,__CALLSITE 里存着 &META。事件的 name 是 concat!("event ", file!(), ":", line!()) 拼出来的 "event src/bin/expand_me.rs:3",target 和模块路径都是 module_path!()。这两个 static 为什么能互相引用、Metadata 每个字段的含义,见 Metadata、Level 与 LevelFilter;DefaultCallsite 怎么缓存 Interest,见 Callsite 与 Interest 缓存。整块外面的花括号让每个调用点的 __CALLSITE 和 META 各在自己的作用域里,同一个函数里写一百条 info! 也不会重名。

开头的 use ::tracing::__macro_support::Callsite as _; 把 Callsite trait 引进来但不占名字,后面的 __CALLSITE.metadata() 是这个 trait 的方法,没有这行就调不到。

一串 &&。 enabled 是四道门:level_enabled! 展开出的两个级别比较,再是 interest() 和 __is_enabled。&& 短路,前一道不过,后面的都不执行。下一页逐个拆开。

一个立即调用的闭包。 门都通过后,字段值数组作为参数传给闭包,闭包里调用 Event::dispatch。字段表达式 x 写在 if enabled 里面,所以门没通过时它根本不求值。为什么不直接写 Event::dispatch(meta, &valueset_all!(...))?这个闭包来自 #1823(「fix macros "use of moved value" with log」,tracing 0.1.30):在那之前,开了 log feature 时字段的 token 会被同时代入 ValueSet 和 log 宏两处,PR 说明写的是「potentially meaning they are executed more than once」,字段表达式里有按值移动的变量时,代入两次就是移动两次,编译报 use of moved value。现在字段只代入一次,构造出的 value_set 同时交给 Event::dispatch 和闭包里的 __tracing_log!;没开 log 时后者展开为空,于是上面的闭包里只剩一行。

else {}。 空的 else 也是 __tracing_log! 留下的:开了 log feature,这里会多出「没有 subscriber 时转给 log」的代码。

info_span!("req", id = 1) 的展开

info_span! 先转给 span!,再落到 (target: $target:expr, $lvl:expr, $name:expr, $($fields:tt)*) 这个主体。展开(META 的内容和上面同构,省略):

    let _span = {
        use ::tracing::__macro_support::Callsite as _;
        static __CALLSITE: ::tracing::callsite::DefaultCallsite = {
            static META: ::tracing::Metadata<'static> = {
                // ... Metadata::new("req", "expand_me", Level::INFO, ...,
                //     FieldSet::new(&[ /* "id" */ ], Identifier(&__CALLSITE)), Kind::SPAN)
            };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
        let mut interest = ::tracing::subscriber::Interest::never();
        if ::tracing::Level::INFO <= ::tracing::level_filters::STATIC_MAX_LEVEL
            && ::tracing::Level::INFO <= ::tracing::level_filters::LevelFilter::current()
            && {
                interest = __CALLSITE.interest();
                !interest.is_never()
            }
            && ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(), interest)
        {
            let meta = __CALLSITE.metadata();
            ::tracing::Span::new(
                meta,
                &{
                    #[allow(unused_imports)]
                    use ::tracing::field::{debug, display, Value};
                    meta.fields()
                        .value_set_all(
                            &[
                                (::tracing::__macro_support::Option::Some(
                                    &1 as &dyn ::tracing::field::Value,
                                )),
                            ],
                        )
                },
            )
        } else {
            let span = ::tracing::__macro_support::__disabled_span(
                __CALLSITE.metadata(),
            );
            {};
            span
        }
    };

和事件比,有三处不同:

  • name 是用户给的 "req",kind 是 Kind::SPAN。
  • 整个块是一个表达式,两个分支都返回 Span。门都通过时调用 Span::new,由它找当前 dispatcher 的 new_span 拿 Id。不通过时调用 __disabled_span,没开 log 时它就是 Span::none(),一个什么都不做的 span(见 Span 与 Entered)。
  • 门是同样的四道,写法却不同:interest 先声明成 never,在条件里赋值,__is_enabled 是第四个 && 而不是嵌在花括号里。效果一样。span 没有 event_enabled 这一道,那是事件独有的(见下一页)。

else 分支里那个孤零零的 {}; 是 if_log_enabled! 在没开 log 时留下的空块。

字段写法各变成什么

字段名和字段值分两条路展开:fieldset! 只取名字,生成 META 里的 &[&str];valueset_all! 只取值,生成 &[Option<&dyn Value>],两个数组按位置一一对应。两个宏都是逐个吃掉 token 的递归(tt muncher),每种写法一个分支。

下面一条事件把各种写法都用上:

#[derive(Debug)]
struct User {
  name: &'static str,
}

struct Ip(&'static str);

impl fmt::Display for Ip {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(f, "ip:{}", self.0)
  }
}

const KEY: &str = "dyn.key";

fn main() {
  let user = User { name: "ann" };
  let ip = Ip("10.0.0.1");
  let n = 7;
  let r#type = "admin";
  tracing::subscriber::with_default(Print, || {
    tracing::info!(
      a = 1,
      b = ?user,
      c = %ip,
      user.name = user.name,
      "http.status" = 200u16,
      { KEY } = true,
      n,
      user.name,
      ?user,
      %ip,
      r#type,
      "done {}", n
    );
  });
}

Print 是一个最小的 Subscriber,在 event 里先打印 FieldSet,再用一个 Visit 把每个字段走了哪个 record_* 方法打出来(Visit 的分派规则见 字段:Field、Value 与 Visit)。M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:

fields = {message, a, b, c, user.name, http.status, dyn.key, n, user.name, user, ip, type}
  message      <- record_debug: done 7
  a            <- record_i64: 1
  b            <- record_debug: User { name: "ann" }
  c            <- record_debug: ip:10.0.0.1
  user.name    <- record_str: "ann"
  http.status  <- record_u64: 200
  dyn.key      <- record_bool: true
  n            <- record_i64: 7
  user.name    <- record_str: "ann"
  user         <- record_debug: User { name: "ann" }
  ip           <- record_debug: ip:10.0.0.1
  type         <- record_str: "admin"

同一条事件 cargo expand 出来的两个数组,逐项对照:

写法fieldset! 生成的名字valueset_all! 生成的值
a = 1FieldName::new("a")&1
b = ?userFieldName::new("b")&::tracing::field::debug(&user)
c = %ipFieldName::new("c")&::tracing::field::display(&ip)
user.name = user.nameFieldName::new("user.name")&user.name
"http.status" = 200u16"http.status"(字面量原样)&200u16
{ KEY } = trueKEY(常量表达式原样)&true
n(简写)FieldName::new("n")&n
user.name(简写)FieldName::new("user.name")&user.name
?user(简写)FieldName::new("user")&::tracing::field::debug(&user)
%ip(简写)FieldName::new("ip")&::tracing::field::display(&ip)
r#typeFieldName::new("r#type"),结果是 "type"&r#type
"done {}", n(剩下的)插到最前面的 "message"&format_args!("done {0}", n)

几点从这张表能读出来:

  • 标识符形式的名字用 stringify! 得到。 每个都包在 __tracing_stringify! 生成的一个 const NAME: FieldName<N> 里。FieldName 是一个 const fn 写的小工具,在编译期把 r# 去掉,所以 r#type 记成 type。user.name 这种带点的写法是 $($k:ident).+,stringify! 之后就是 "user.name",点只是名字的一部分,不代表嵌套。
  • 字面量名字和 { 常量 } 名字原样放进数组。 所以名字里可以有标识符不允许的字符("http.status"),或者来自一个 const({ KEY })。FieldSet 在 static META 里,名字必须是编译期常量,{ } 里写运行时的变量编译不过。
  • ? 和 % 只是包一层。 debug(&user)、display(&ip) 把引用包进 DebugValue / DisplayValue,不格式化。两者在 Visit 那边都走 record_debug,% 的那个用 Display 输出,所以 c 打出来是 ip:10.0.0.1。
  • 简写 n 等于 n = n。 值就是对同名变量取引用。
  • 剩下解析不了的就是格式串。 fieldset! 和 valueset_all! 各有一个兜底分支,注释是「Remainder is unparsable, but exists --- must be format args!」(fieldset! 的、valueset_all! 的)。它们把 "message" 和 format_args!(...) 插到数组最前面,所以输出里 message 总是第一个,尽管它写在最后。前面 trace_macros! 里 fieldset! { @ {} "x = {}", x, } 直接变成 fieldset! (@ { "message", , }),走的就是这个分支。
  • 重复的名字不报错。 user.name 写了两次,FieldSet 里就有两个 user.name,Visit 也收到两次。

{ KEY } = true 那一行的值是 (Some(&true as &dyn ...)),不像其他行写成 ::tracing::__macro_support::Option::Some。valueset_all! 处理常量名字的六个分支用的是裸 Some,其余分支都走 __macro_support 重新导出的 Option。后者是为了不受用户 crate 里同名条目的影响(__macro_support 重新导出 Option、format_args 等,源码注释链到 #2761,起因是用户有一个名叫 core 的 crate)。

valueset_all! 的入口(L2971)在构造数组之前还有一行 use,展开里也看得到:

    ($fields:expr, $($kvs:tt)+) => {
        {
            #[allow(unused_imports)]
            // This import statement CANNOT be removed as it will break existing use cases.
            // See #831, #2332, #3424 for the last times we tried.
            use $crate::field::{debug, display, Value};
            $fields.value_set_all($crate::valueset_all!(
                @ { },
                $($kvs)+
            ))
        }
    };

macro_rules! 的卫生性只隔离局部变量和标签,use 引进的名字对用户写进来的表达式可见。所以 info!(foo = debug(foo)) 不写 use tracing::field::debug 也能编译,有人就这么用了。

📝NOTE
冷知识:这行 use 被删过两次,两次都撤回了

2020 年的 #806 为了修宏里的名字冲突删过一次,紧接着被 #831 撤回,说明是:「This change was accidentally semver-incompatible, as user code was able to use the imported debug and display functions without adding an import outside the macro. Although this was not documented, downstream code relied on these names being available」。

2025 年的 #3382(「make valueset macro sanitary」)又删了一次,理由是反方向的问题(#3381):这行 use 让用户自己的 debug 变量被遮住。它随 tracing 0.1.42 发布,马上有人报告 tracing::info!(foo = debug(foo), bar = display(bar)) 编译不过(#3424)。CHANGELOG 记着「The previous release [0.1.42] was yanked because [#3382] was a breaking change」。#3425 把 use 恢复,只保留了改成完整路径的那部分(「This is best practice, and so worth keeping」),并加上上面那段「CANNOT be removed」注释,「because this isn't the first time that this has happened」。恢复首次发布在 tracing 0.1.43。

event! 和 span! 的入口分支

event! 的参数可以带三个可选前缀:name:、target:、parent:,顺序固定。event! 按前缀组合分成七组(源码里的注释是 // Name / target / parent.、// Name / target.、// Target / parent.、// Name / parent.、// Name.、// Target.、// Parent.,最后一组没有前缀,注释是 // ...)。前六组各有一个带花括号的主体分支,生成上面那段代码,彼此只差 name 从哪来、调 Event::dispatch 还是 Event::child_of;有 parent: 的三组调 child_of,其余三组调 dispatch。剩下的分支都在把参数改写成某个主体分支能接受的形式:

  • { 字段 }, 格式串 改写成 { message = format_args!(格式串), 字段 };
  • k = ... 开头的,整体包进花括号;
  • 其他剩下的,也整体包进花括号,留给 fieldset! 的兜底分支当格式串。

这样的分支要按顺序排,macro_rules! 从上往下试,第一个匹配的生效。这个规则也让 macros.rs 里留下了几个永远不会生效的分支:

位置模式被谁挡住
span! L85(target: $target:expr, parent: $parent:expr, $lvl:expr, $name:expr)一模一样的 L21
span! L105(target: $target:expr, $lvl:expr, $name:expr, $($fields:tt)*)一模一样的 L55
event! L1006( $lvl:expr, { $($fields:tt)* }, $($arg:tt)+ )一模一样的 L999

span! 的 L105 如果真被匹配到,会把同样的参数原样再交给 span!,无限递归;它不会出事,正是因为 L55 永远先匹配。两处 span! 的重复都来自 2019 年的同一次提交(d3577be5「tracing: fix inconsistent macros (#116)」)。event! 的两个分支只差一处:L999 写的是 $crate::__macro_support::format_args!,L1006 还是裸的 format_args!。2023 年的 #2762(「removed core imports in macros」,tracing 0.1.41)为了避免和用户名叫 core 的 crate 冲突,把宏里的 core:: 路径换成 __macro_support,只改到了生效的那一份。编译 tracing 时这几个分支没有引起任何报错,它们一直留着,也不影响行为。

callsite! 也是一个留着的旧宏:它展开后立刻调用 __CALLSITE.register()。tracing 自己的宏都改用了 callsite2!,注册推迟到第一次经过调用点、并且级别门已经放行之后。callsite! 还是 #[macro_export] 的,只是加了 #[doc(hidden)],macros.rs 里没有任何地方再调用它。

macros.rs 为什么有 3 342 行

按 macro_rules! 统计(/// 文档注释单独算):

部分行数说明
trace! debug! info! warn! error!5 × 251 = 1 255每个 49 个分支,全部转给 event!
trace_span! … error_span!5 × 46 = 230每个 8 个分支,全部转给 span!
event!43942 个分支,其中 6 个是完整的主体
valueset_all! + valueset!175 + 170两种 ValueSet 形态,分支一一对应
span!11210 个分支,2 个主体,2 个死分支
enabled!、fieldset!、callsite!、callsite2!、log 相关等共 348
/// 文档注释525
其余(空行、普通注释、属性)88

把五个级别宏的定义里级别名替换掉再逐字比较,trace!、debug!、info!、warn!、error! 完全相同;五个 *_span! 除了 trace_span! 最后一行的花括号里多两个空格,也完全相同。也就是说 1 485 行里有 1 188 行是另外四份的拷贝,占整个文件的三分之一出头。

剩下的重复在 event! 里:六个主体分支各写一遍同样的 static、门、闭包,只在 name、dispatch / child_of、__tracing_log! 放在前面还是后面这几处不同。还有 valueset_all! 和 valueset!:后者生成 (&Field, Option<&dyn Value>) 形态,只给 record_all! 用(两种形态的来历见 字段 的冷知识),两个宏的分支一个对一个地平行写了两遍。

为什么五个级别宏不用一个宏生成,源码里没有说明。从结果看,代价是改一处要改五处;好处是每个宏的文档、示例都是独立完整的,读 docs.rs 的人看到的就是自己要用的那个。上面那张死分支表说明,靠人工保持多份拷贝同步并不总能做到。

❗IMPORTANT
动脑筋:下面这段能编译吗?
tracing = "=0.1.44"
fn main() {
  let debug = false;
  tracing::info!("debug: {debug:?}");
}

format_args! 支持在格式串里直接写变量名({debug:?}),这里的 debug 是一个 bool。rustc 1.99.0 下,cargo build 是成功,还是报错?如果报错,报的是什么?

开了 log feature 展开会变

上面的展开都是在没开 tracing 的 log feature 时得到的。同一个程序,依赖改成:

tracing = { version = "=0.1.44", features = ["log"] }

cargo expand 的输出从 123 行变成 250 行,多出来的 127 行在三处:

  • 事件闭包里,Event::dispatch 之后多了一段:如果 log 的静态最大级别允许、并且 !dispatcher::has_been_set(),就构造一条 log::Record 交给 log::logger()。
  • 事件的 else {} 不再是空的:门没通过时也会走同样的判断,只要没有装过 tracing 的 dispatcher、log 那边又要这一级,字段照样求值、照样发给 log。
  • span 的 else 里,{}; 变成了一段 span.record_all(...);__disabled_span 也换成了 #[cfg(feature = "log")] 的那个版本(L1025),返回 Span::new_disabled(meta),被过滤掉的 span 仍然带着自己的元数据。

以事件的 else 分支为例,开了 log 之后是:

        } else {
            if match ::tracing::Level::INFO {
                ::tracing::Level::ERROR => ::tracing::log::Level::Error,
                // ...
                _ => ::tracing::log::Level::Trace,
            } <= ::tracing::log::STATIC_MAX_LEVEL
            {
                if !::tracing::dispatcher::has_been_set() {
                    {
                        use ::tracing::log;
                        // ...
                            if logger.enabled(&log_meta) {
                                ::tracing::__macro_support::__tracing_log(
                                    meta,
                                    logger,
                                    log_meta,
                                    &{
                                        // ... 同样的 value_set_all(&[ format_args!("x = {0}", x) ... ])
                                    },
                                )
                            }
                        // ...
                    }
                } else {
                    {}
                }
            } else {
                {}
            };
        }

这些代码什么时候真的执行、和 LogTracer 一起用会不会重复,是 和 log 互通 的内容。对本页来说要记住一点:依赖图里任何一个 crate 打开 tracing 的 log feature,feature 合并后整个程序里所有 tracing 宏都按这种方式展开。

动脑筋答案

不能编译。cargo build --release 的输出:

error[E0277]: `fn(_) -> DebugValue<_> {debug::<_>}` doesn't implement `Debug`
 --> src/bin/clash.rs:3:26
  |
3 |   tracing::info!("debug: {debug:?}");
  |                          ^^^^^^^^^ `fn(_) -> DebugValue<_> {debug::<_>}` cannot be formatted using `{:?}` because it doesn't implement `Debug`
  |
  = help: the trait `Debug` is not implemented for fn item `fn(_) -> DebugValue<_> {debug::<_>}`
  = note: this error originates in the macro `$crate::__macro_support::format_args` which comes from the expansion of the macro `tracing::info` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `exp-tracing-macros` (bin "clash") due to 1 previous error
  • 格式串 "debug: {debug:?}" 落到 valueset_all! 的兜底分支,变成 format_args!("debug: {debug:?}"),它被放进 valueset_all! 入口生成的那个块里。
  • 块的第一行是 use $crate::field::{debug, display, Value};。块里的名字查找先找到这个 use 引进的函数 tracing::field::debug,外面的 let debug = false 被遮住了。
  • {debug:?} 于是要格式化一个函数项 fn(_) -> DebugValue<_>,它没有实现 Debug,报 E0277。

这正是 #3381 报告的问题,也就是上面冷知识里 #3382 想修、又因为破坏兼容被撤回的那一面,0.1.44 里仍然存在。改成普通实参 info!("debug: {:?}", debug) 也一样报 E0277(实测),因为实参表达式同样在那个块里查名字。变量改名就好:同样写法换成 let trace = false; 和 {trace:?} 能编译通过(实测),use 只引进了 debug、display、Value 三个名字。

小结

  • info! 等级别宏不生成代码,经过两次改写交给 event! 带花括号的主体分支;一条 info!("x = {}", x) 在编译期展开 15 次宏。
  • 主体生成两个互相引用的 static(META、__CALLSITE)、一串 && 连起来的门,门都通过才在一个立即调用的闭包里构造 ValueSet 并调用 Event::dispatch;字段表达式写在 if 里,门不通过不求值。
  • info_span! 展开结构相同,两个分支分别返回 Span::new(...) 和 __disabled_span(...)(没开 log 时是 Span::none())。
  • fieldset! 取名字、valueset_all! 取值,按位置对应。标识符名字经 FieldName 去掉 r#;字面量和 { 常量 } 名字原样使用;? / % 只包一层;格式串由兜底分支变成排在最前面的 message。
  • 宏里那行 use $crate::field::{debug, display, Value} 对用户代码可见,删过两次都因为破坏兼容被撤回;它也让格式串里名叫 debug 的变量编译不过。
  • macros.rs 的 3 342 行里,五个级别宏和五个 *_span! 是逐字相同的拷贝,event! 的主体写了六遍;span! 和 event! 里有三个被前面的同名模式遮住、永远不会生效的分支。
  • 开了 tracing 的 log feature,展开从 123 行变成 250 行,门没通过的分支里也多出转给 log 的代码。

💬 关联讨论

发起讨论
加载中...