TL;DR
这是一个程序宏,它使用syn and quote执行您所描述的操作:
// print_caller_location/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
// Create a procedural attribute macro
//
// Notably, this must be placed alone in its own crate
#[proc_macro_attribute]
pub fn print_caller_location(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the passed item as a function
let func = syn::parse_macro_input!(item as syn::ItemFn);
// Break the function down into its parts
let syn::ItemFn {
attrs,
vis,
sig,
block,
} = func;
// Ensure that it isn't an `async fn`
if let Some(async_token) = sig.asyncness {
// Error out if so
let error = syn::Error::new(
async_token.span(),
"async functions do not support caller tracking functionality
help: consider returning `impl Future` instead",
);
return TokenStream::from(error.to_compile_error());
}
// Wrap body in a closure only if function doesn't already have #[track_caller]
let block = if attrs.iter().any(|attr| attr.path.is_ident("track_caller")) {
quote! { #block }
} else {
quote! {
(move || #block)()
}
};
// Extract function name for prettier output
let name = format!("{}", sig.ident);
// Generate the output, adding `#[track_caller]` as well as a `println!`
let output = quote! {
#[track_caller]
#(#attrs)*
#vis #sig {
println!(
"entering `fn {}`: called from `{}`",
#name,
::core::panic::Location::caller()
);
#block
}
};
// Convert the output from a `proc_macro2::TokenStream` to a `proc_macro::TokenStream`
TokenStream::from(output)
}
确保将其放入板条箱中并将这些行添加到其Cargo.toml
:
# print_caller_location/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = {version = "1.0.16", features = ["full"]}
quote = "1.0.3"
proc-macro2 = "1.0.9"
深入解释
宏只能扩展到可以手动编写的代码。知道了这一点,我在这里看到两个问题:
- How can I write a function that tracks the location of its caller?
- See How can I access a function's calling location each time it's called?
简短的回答:要获取调用函数的位置,请将其标记为#[track_caller]
并使用std::panic::Location::caller在它的身体里。
- 如何编写创建此类函数的过程宏?
初步尝试
我们想要一个程序宏
- 接受一个函数,
- 标记它
#[track_caller]
,
- 并添加一行打印Location::caller.
例如,它将转换这样的函数:
fn foo() {
// body of foo
}
into
#[track_caller]
fn foo() {
println!("{}", std::panic::Location::caller());
// body of foo
}
下面,我展示了一个精确执行该转换的过程宏——尽管,正如您将在以后的版本中看到的那样,您可能想要不同的东西。要尝试此代码,就像之前在 TL;DR 部分中一样,将其放入自己的 crate 中并将其依赖项添加到Cargo.toml
.
// print_caller_location/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
// Create a procedural attribute macro
//
// Notably, this must be placed alone in its own crate
#[proc_macro_attribute]
pub fn print_caller_location(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the passed item as a function
let func = syn::parse_macro_input!(item as syn::ItemFn);
// Break the function down into its parts
let syn::ItemFn {
attrs,
vis,
sig,
block,
} = func;
// Extract function name for prettier output
let name = format!("{}", sig.ident);
// Generate the output, adding `#[track_caller]` as well as a `println!`
let output = quote! {
#[track_caller]
#(#attrs)*
#vis #sig {
println!(
"entering `fn {}`: called from `{}`",
#name,
::core::panic::Location::caller()
);
#block
}
};
// Convert the output from a `proc_macro2::TokenStream` to a `proc_macro::TokenStream`
TokenStream::from(output)
}
用法示例:
// example1/src/main.rs
#![feature(track_caller)]
#[print_caller_location::print_caller_location]
fn add(x: u32, y: u32) -> u32 {
x + y
}
fn main() {
add(1, 5); // entering `fn add`: called from `example1/src/main.rs:11:5`
add(1, 5); // entering `fn add`: called from `example1/src/main.rs:12:5`
}
不幸的是,我们无法摆脱这个简单的版本。该版本至少存在两个问题:
-
它是如何组成的async fn
s:
- 它不是打印调用者位置,而是打印我们的宏 (
#[print_caller_location]
) 被调用。例如:
// example2/src/main.rs
#![feature(track_caller)]
#[print_caller_location::print_caller_location]
async fn foo() {}
fn main() {
let future = foo();
// ^ oops! prints nothing
futures::executor::block_on(future);
// ^ oops! prints "entering `fn foo`: called from `example2/src/main.rs:5:1`"
let future = foo();
// ^ oops! prints nothing
futures::executor::block_on(future);
// ^ oops! prints "entering `fn foo`: called from `example2/src/main.rs:5:1`"
}
-
它如何与自身的其他调用一起工作,或者一般来说,#[track_caller]
:
- 嵌套函数
#[print_caller_location]
将打印根调用者的位置,而不是给定函数的直接调用者。例如:
// example3/src/main.rs
#![feature(track_caller)]
#[print_caller_location::print_caller_location]
fn add(x: u32, y: u32) -> u32 {
x + y
}
#[print_caller_location::print_caller_location]
fn add_outer(x: u32, y: u32) -> u32 {
add(x, y)
// ^ we would expect "entering `fn add`: called from `example3/src/main.rs:12:5`"
}
fn main() {
add(1, 5);
// ^ "entering `fn add`: called from `example3/src/main.rs:17:5`"
add(1, 5);
// ^ "entering `fn add`: called from `example3/src/main.rs:19:5`"
add_outer(1, 5);
// ^ "entering `fn add_outer`: called from `example3/src/main.rs:21:5`"
// ^ oops! "entering `fn add`: called from `example3/src/main.rs:21:5`"
//
// In reality, `add` was called on line 12, from within the body of `add_outer`
add_outer(1, 5);
// ^ "entering `fn add_outer`: called from `example3/src/main.rs:26:5`"
// oops! ^ entering `fn add`: called from `example3/src/main.rs:26:5`
//
// In reality, `add` was called on line 12, from within the body of `add_outer`
}
寻址async fn
s
可以通过以下方式解决该问题async fn
s using -> impl Future
,例如,如果我们想要我们的async fn
反例要正确工作,我们可以这样写:
// example4/src/main.rs
#![feature(track_caller)]
use std::future::Future;
#[print_caller_location::print_caller_location]
fn foo() -> impl Future<Output = ()> {
async move {
// body of foo
}
}
fn main() {
let future = foo();
// ^ prints "entering `fn foo`: called from `example4/src/main.rs:15:18`"
futures::executor::block_on(future);
// ^ prints nothing
let future = foo();
// ^ prints "entering `fn foo`: called from `example4/src/main.rs:19:18`"
futures::executor::block_on(future);
// ^ prints nothing
}
我们可以添加一个特殊情况,将此转换应用于我们的宏。但是,该转换将函数的公共 API 从async fn foo()
to fn foo() -> impl Future<Output = ()>
除了影响返回的未来可以具有的自动特征之外。
因此,我建议我们允许用户根据需要使用该解决方法,并且如果我们的宏用于async fn
。我们可以通过将这些行添加到我们的宏代码中来做到这一点:
// Ensure that it isn't an `async fn`
if let Some(async_token) = sig.asyncness {
// Error out if so
let error = syn::Error::new(
async_token.span(),
"async functions do not support caller tracking functionality
help: consider returning `impl Future` instead",
);
return TokenStream::from(error.to_compile_error());
}
修复嵌套行为#[print_caller_location]
功能
有问题的行为可以归结为以下事实:当#[track_caller]
功能,foo
,直接调用另一个#[track_caller]
功能,bar
, Location::caller
将使他们都可以访问foo
的来电者。换句话说,Location::caller
在嵌套的情况下可以访问根调用者#[track_caller]
功能:
#![feature(track_caller)]
fn main() {
foo(); // prints `src/main.rs:4:5` instead of the line number in `foo`
}
#[track_caller]
fn foo() {
bar();
}
#[track_caller]
fn bar() {
println!("{}", std::panic::Location::caller());
}
游乐场链接
为了解决这个问题,我们需要打破链条#[track_caller]
来电。我们可以通过隐藏嵌套调用来打破链条bar
在闭包内:
#![feature(track_caller)]
fn main() {
foo();
}
#[track_caller]
fn foo() {
(move || {
bar(); // prints `src/main.rs:10:9`
})()
}
#[track_caller]
fn bar() {
println!("{}", std::panic::Location::caller());
}
游乐场链接
现在我们知道如何打破链条#[track_caller]
函数,我们就可以解决这个问题。我们只需要确保用户是否确实用#[track_caller]
我们故意避免插入封口并破坏链条。
我们可以将这些行添加到我们的解决方案中:
// Wrap body in a closure only if function doesn't already have #[track_caller]
let block = if attrs.iter().any(|attr| attr.path.is_ident("track_caller")) {
quote! { #block }
} else {
quote! {
(move || #block)()
}
};
最终解决方案
经过这两项更改后,我们最终得到了以下代码:
// print_caller_location/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
// Create a procedural attribute macro
//
// Notably, this must be placed alone in its own crate
#[proc_macro_attribute]
pub fn print_caller_location(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the passed item as a function
let func = syn::parse_macro_input!(item as syn::ItemFn);
// Break the function down into its parts
let syn::ItemFn {
attrs,
vis,
sig,
block,
} = func;
// Ensure that it isn't an `async fn`
if let Some(async_token) = sig.asyncness {
// Error out if so
let error = syn::Error::new(
async_token.span(),
"async functions do not support caller tracking functionality
help: consider returning `impl Future` instead",
);
return TokenStream::from(error.to_compile_error());
}
// Wrap body in a closure only if function doesn't already have #[track_caller]
let block = if attrs.iter().any(|attr| attr.path.is_ident("track_caller")) {
quote! { #block }
} else {
quote! {
(move || #block)()
}
};
// Extract function name for prettier output
let name = format!("{}", sig.ident);
// Generate the output, adding `#[track_caller]` as well as a `println!`
let output = quote! {
#[track_caller]
#(#attrs)*
#vis #sig {
println!(
"entering `fn {}`: called from `{}`",
#name,
::core::panic::Location::caller()
);
#block
}
};
// Convert the output from a `proc_macro2::TokenStream` to a `proc_macro::TokenStream`
TokenStream::from(output)
}