我们可以在过程宏属性中获取调用者的源代码位置吗?

Ayu*_*hra 6 rust rust-macros rust-proc-macros

我需要获取每个方法的调用者的源位置。我正在尝试创建一个proc_macro_attribute来捕获位置并打印它。

#[proc_macro_attribute]
pub fn get_location(attr: TokenStream, item: TokenStream) -> TokenStream {
    // Get and print file!(), line!() of source
    // Should print line no. 11
    item
}
Run Code Online (Sandbox Code Playgroud)
#[get_location]
fn add(x: u32, y: u32) -> u32 {
    x + y
}

fn main() {
    add(1, 5); // Line No. 11
}
Run Code Online (Sandbox Code Playgroud)

tim*_*ree 6

长话短说

\n\n

这是一个使用syn和的程序宏quote执行您所描述的操作:

\n\n
// print_caller_location/src/lib.rs\n\nuse proc_macro::TokenStream;\nuse quote::quote;\nuse syn::spanned::Spanned;\n\n// Create a procedural attribute macro\n//\n// Notably, this must be placed alone in its own crate\n#[proc_macro_attribute]\npub fn print_caller_location(_attr: TokenStream, item: TokenStream) -> TokenStream {\n    // Parse the passed item as a function\n    let func = syn::parse_macro_input!(item as syn::ItemFn);\n\n    // Break the function down into its parts\n    let syn::ItemFn {\n        attrs,\n        vis,\n        sig,\n        block,\n    } = func;\n\n    // Ensure that it isn\'t an `async fn`\n    if let Some(async_token) = sig.asyncness {\n        // Error out if so\n        let error = syn::Error::new(\n            async_token.span(),\n            "async functions do not support caller tracking functionality\n    help: consider returning `impl Future` instead",\n        );\n\n        return TokenStream::from(error.to_compile_error());\n    }\n\n    // Wrap body in a closure only if function doesn\'t already have #[track_caller]\n    let block = if attrs.iter().any(|attr| attr.path.is_ident("track_caller")) {\n        quote! { #block }\n    } else {\n        quote! {\n            (move || #block)()\n        }\n    };\n\n    // Extract function name for prettier output\n    let name = format!("{}", sig.ident);\n\n    // Generate the output, adding `#[track_caller]` as well as a `println!`\n    let output = quote! {\n        #[track_caller]\n        #(#attrs)*\n        #vis #sig {\n            println!(\n                "entering `fn {}`: called from `{}`",\n                #name,\n                ::core::panic::Location::caller()\n            );\n            #block\n        }\n    };\n\n    // Convert the output from a `proc_macro2::TokenStream` to a `proc_macro::TokenStream`\n    TokenStream::from(output)\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

确保将其放入板条箱中并将这些行添加到其Cargo.toml

\n\n
# print_caller_location/Cargo.toml\n\n[lib]\nproc-macro = true\n\n[dependencies]\nsyn = {version = "1.0.16", features = ["full"]}\nquote = "1.0.3"\nproc-macro2 = "1.0.9"\n
Run Code Online (Sandbox Code Playgroud)\n\n

深入解释

\n\n

宏只能扩展到可以手动编写的代码。知道了这一点,我在这里看到两个问题:

\n\n
    \n
  1. 如何编写一个函数来跟踪其调用者的位置?\n\n
  2. \n
  3. 如何编写一个过程宏来创建此类函数?
  4. \n
\n\n

初步尝试

\n\n

我们想要一个程序宏

\n\n
    \n
  • 接受一个函数,
  • \n
  • 标记它#[track_caller]
  • \n
  • 并添加一行打印Location::caller.
  • \n
\n\n

例如,它将转换这样的函数:

\n\n
fn foo() {\n    // body of foo\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

进入

\n\n
#[track_caller]\nfn foo() {\n    println!("{}", std::panic::Location::caller());\n    // body of foo\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

下面,我提出了一个程序宏,它精确地执行该转换 \xe2\x80\x94 尽管,正如您将在以后的版本中看到的那样,您可能想要不同的东西。要尝试此代码,就像之前在 TL;DR 部分中一样,将其放入自己的 crate 中并将其依赖项添加到Cargo.toml.

\n\n
// print_caller_location/src/lib.rs\n\nuse proc_macro::TokenStream;\nuse quote::quote;\n\n// Create a procedural attribute macro\n//\n// Notably, this must be placed alone in its own crate\n#[proc_macro_attribute]\npub fn print_caller_location(_attr: TokenStream, item: TokenStream) -> TokenStream {\n    // Parse the passed item as a function\n    let func = syn::parse_macro_input!(item as syn::ItemFn);\n\n    // Break the function down into its parts\n    let syn::ItemFn {\n        attrs,\n        vis,\n        sig,\n        block,\n    } = func;\n\n    // Extract function name for prettier output\n    let name = format!("{}", sig.ident);\n\n    // Generate the output, adding `#[track_caller]` as well as a `println!`\n    let output = quote! {\n        #[track_caller]\n        #(#attrs)*\n        #vis #sig {\n            println!(\n                "entering `fn {}`: called from `{}`",\n                #name,\n                ::core::panic::Location::caller()\n            );\n            #block\n        }\n    };\n\n    // Convert the output from a `proc_macro2::TokenStream` to a `proc_macro::TokenStream`\n    TokenStream::from(output)\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

用法示例:

\n\n
// example1/src/main.rs\n\n#![feature(track_caller)]\n\n#[print_caller_location::print_caller_location]\nfn add(x: u32, y: u32) -> u32 {\n    x + y\n}\n\nfn main() {\n    add(1, 5); // entering `fn add`: called from `example1/src/main.rs:11:5`\n    add(1, 5); // entering `fn add`: called from `example1/src/main.rs:12:5`\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

不幸的是,我们无法摆脱这个简单的版本。该版本至少存在两个问题:

\n\n
    \n
  • 它如何与 s 组成async fn

    \n\n
      \n
    • 它不是打印调用者位置,而是打印调用宏 ( ) 的位置#[print_caller_location]。例如:
    • \n
    \n\n

    \n\n

    // example2/src/main.rs\n\n#![feature(track_caller)]\n\n#[print_caller_location::print_caller_location]\nasync fn foo() {}\n\nfn main() {\n    let future = foo();\n    // ^ oops! prints nothing\n    futures::executor::block_on(future);\n    // ^ oops! prints "entering `fn foo`: called from `example2/src/main.rs:5:1`"\n    let future = foo();\n    // ^ oops! prints nothing\n    futures::executor::block_on(future);\n    // ^ oops! prints "entering `fn foo`: called from `example2/src/main.rs:5:1`"\n}\n
    Run Code Online (Sandbox Code Playgroud)
  • \n
  • 它如何与自身的其他调用一起工作,或者一般来说#[track_caller]

    \n\n
      \n
    • 嵌套函数#[print_caller_location]将打印根调用者的位置,而不是给定函数的直接调用者。例如:
    • \n
    \n\n

    \n\n

    // example3/src/main.rs\n\n#![feature(track_caller)]\n\n#[print_caller_location::print_caller_location]\nfn add(x: u32, y: u32) -> u32 {\n    x + y\n}\n\n#[print_caller_location::print_caller_location]\nfn add_outer(x: u32, y: u32) -> u32 {\n    add(x, y)\n    // ^ we would expect "entering `fn add`: called from `example3/src/main.rs:12:5`"\n}\n\nfn main() {\n    add(1, 5);\n    // ^ "entering `fn add`: called from `example3/src/main.rs:17:5`"\n    add(1, 5);\n    // ^ "entering `fn add`: called from `example3/src/main.rs:19:5`"\n    add_outer(1, 5);\n    // ^ "entering `fn add_outer`: called from `example3/src/main.rs:21:5`"\n    // ^ oops! "entering `fn add`: called from `example3/src/main.rs:21:5`"\n    //\n    // In reality, `add` was called on line 12, from within the body of `add_outer`\n    add_outer(1, 5);\n    // ^ "entering `fn add_outer`: called from `example3/src/main.rs:26:5`"\n    // oops! ^ entering `fn add`: called from `example3/src/main.rs:26:5`\n    //\n    // In reality, `add` was called on line 12, from within the body of `add_outer`\n}\n
    Run Code Online (Sandbox Code Playgroud)
  • \n
\n\n

寻址async fn

\n\n

可以async fn使用 s来解决这个问题-> impl Future,例如,如果我们希望async fn反例正常工作,我们可以写:

\n\n
// example2/src/main.rs\n\n#![feature(track_caller)]\n\n#[print_caller_location::print_caller_location]\nasync fn foo() {}\n\nfn main() {\n    let future = foo();\n    // ^ oops! prints nothing\n    futures::executor::block_on(future);\n    // ^ oops! prints "entering `fn foo`: called from `example2/src/main.rs:5:1`"\n    let future = foo();\n    // ^ oops! prints nothing\n    futures::executor::block_on(future);\n    // ^ oops! prints "entering `fn foo`: called from `example2/src/main.rs:5:1`"\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们可以添加一个特殊情况,将此转换应用于我们的宏。async fn foo()然而,除了fn foo() -> impl Future<Output = ()>影响返回的 future 可能具有的 auto 特征之外,该转换还将函数的公共 API 从 更改为。

\n\n

因此,我建议我们允许用户根据需要使用该解决方法,并且如果我们的宏在async fn. 我们可以通过将这些行添加到我们的宏代码中来做到这一点:

\n\n
// example3/src/main.rs\n\n#![feature(track_caller)]\n\n#[print_caller_location::print_caller_location]\nfn add(x: u32, y: u32) -> u32 {\n    x + y\n}\n\n#[print_caller_location::print_caller_location]\nfn add_outer(x: u32, y: u32) -> u32 {\n    add(x, y)\n    // ^ we would expect "entering `fn add`: called from `example3/src/main.rs:12:5`"\n}\n\nfn main() {\n    add(1, 5);\n    // ^ "entering `fn add`: called from `example3/src/main.rs:17:5`"\n    add(1, 5);\n    // ^ "entering `fn add`: called from `example3/src/main.rs:19:5`"\n    add_outer(1, 5);\n    // ^ "entering `fn add_outer`: called from `example3/src/main.rs:21:5`"\n    // ^ oops! "entering `fn add`: called from `example3/src/main.rs:21:5`"\n    //\n    // In reality, `add` was called on line 12, from within the body of `add_outer`\n    add_outer(1, 5);\n    // ^ "entering `fn add_outer`: called from `example3/src/main.rs:26:5`"\n    // oops! ^ entering `fn add`: called from `example3/src/main.rs:26:5`\n    //\n    // In reality, `add` was called on line 12, from within the body of `add_outer`\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

修复#[print_caller_location]函数的嵌套行为

\n\n

有问题的行为可以最小化为这样一个事实:当一个#[track_caller]函数foo直接调用另一个#[track_caller]函数时barLocation::caller它们都可以访问foo\ 的调用者。换句话说,Location::caller在嵌套函数的情况下,可以访问根调用者#[track_caller]

\n\n
// example4/src/main.rs\n\n#![feature(track_caller)]\n\nuse std::future::Future;\n\n#[print_caller_location::print_caller_location]\nfn foo() -> impl Future<Output = ()> {\n    async move {\n        // body of foo\n    }\n}\n\nfn main() {\n    let future = foo();\n    // ^ prints "entering `fn foo`: called from `example4/src/main.rs:15:18`"\n    futures::executor::block_on(future);\n    // ^ prints nothing\n    let future = foo();\n    // ^ prints "entering `fn foo`: called from `example4/src/main.rs:19:18`"\n    futures::executor::block_on(future);\n    // ^ prints nothing\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

游乐场链接

\n\n

为了解决这个问题,我们需要打破调用链#[track_caller]bar我们可以通过隐藏闭包内的嵌套调用来打破链条:

\n\n
// Ensure that it isn\'t an `async fn`\nif let Some(async_token) = sig.asyncness {\n    // Error out if so\n    let error = syn::Error::new(\n        async_token.span(),\n        "async functions do not support caller tracking functionality\n    help: consider returning `impl Future` instead",\n    );\n\n    return TokenStream::from(error.to_compile_error());\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

游乐场链接

\n\n

现在我们知道了如何打破函数链#[track_caller],我们就可以解决这个问题了。我们只需要确保,如果用户确实#[track_caller]有意标记他们的函数,我们就不会插入闭包并破坏链条。

\n\n

我们可以将这些行添加到我们的解决方案中:

\n\n
#![feature(track_caller)]\n\nfn main() {\n    foo(); // prints `src/main.rs:4:5` instead of the line number in `foo`\n}\n\n#[track_caller]\nfn foo() {\n   bar();\n}\n\n#[track_caller]\nfn bar() {\n    println!("{}", std::panic::Location::caller());\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

最终解决方案

\n\n

经过这两项更改后,我们最终得到了以下代码:

\n\n
#![feature(track_caller)]\n\nfn main() {\n    foo();\n}\n\n#[track_caller]\nfn foo() {\n    (move || {\n        bar(); // prints `src/main.rs:10:9`\n    })()\n}\n\n#[track_caller]\nfn bar() {\n    println!("{}", std::panic::Location::caller());\n}\n
Run Code Online (Sandbox Code Playgroud)\n


Vic*_*eau 2

可以使用现成的解决方案(请参阅@timotree 的评论)。如果您想自己执行此操作,具有更大的灵活性或学习,您可以编写一个过程宏来解析回溯(从调用的函数内部获取)并打印您需要的信息。这是 a 中的程序宏lib.rs

extern crate proc_macro;
use proc_macro::{TokenStream, TokenTree};

#[proc_macro_attribute]
pub fn get_location(_attr: TokenStream, item: TokenStream) -> TokenStream {

    // prefix code to be added to the function's body
    let mut prefix: TokenStream = "
        // find earliest symbol in source file using backtrace
        let ps = Backtrace::new().frames().iter()
            .flat_map(BacktraceFrame::symbols)
            .skip_while(|s| s.filename()
                .map(|p|!p.ends_with(file!())).unwrap_or(true))
            .nth(1 as usize).unwrap();

        println!(\"Called from {:?} at line {:?}\",
            ps.filename().unwrap(), ps.lineno().unwrap());
    ".parse().unwrap(); // parse string into TokenStream

    item.into_iter().map(|tt| { // edit input TokenStream
        match tt { 
            TokenTree::Group(ref g) // match the function's body
                if g.delimiter() == proc_macro::Delimiter::Brace => { 

                    prefix.extend(g.stream()); // add parsed string

                    TokenTree::Group(proc_macro::Group::new(
                        proc_macro::Delimiter::Brace, prefix.clone()))
            },
            other => other, // else just forward TokenTree
        }
    }).collect()
} 
Run Code Online (Sandbox Code Playgroud)

解析回溯以查找源文件中最早的符号(使用file!()另一个宏检索)。我们需要添加到函数中的代码在字符串中定义,然后将其解析为 aTokenStream并添加到函数体的开头。我们可以在最后添加这个逻辑,但是返回一个没有分号的值就不再起作用了。然后,您可以在您的程序中使用程序宏,main.rs如下所示:

extern crate backtrace;
use backtrace::{Backtrace, BacktraceFrame};
use mylib::get_location;

#[get_location]
fn add(x: u32, y: u32) -> u32 { x + y }

fn main() { 
    add(1, 41);
    add(41, 1);
}
Run Code Online (Sandbox Code Playgroud)

输出是:

> Called from "src/main.rs" at line 10
> Called from "src/main.rs" at line 11
Run Code Online (Sandbox Code Playgroud)

不要忘记lib通过将这两行添加到您的Cargo.toml

[lib]
proc-macro = true
Run Code Online (Sandbox Code Playgroud)