Можем ли мы получить местоположение исходного кода вызывающей стороны в атрибуте процедурного макроса?
У меня есть требование получить исходное местоположение вызывающего абонента каждого метода. Я пытаюсь создать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
}
#[get_location]
fn add(x: u32, y: u32) -> u32 {
x + y
}
fn main() {
add(1, 5); // Line No. 11
}
2 ответа
Доступны готовые к использованию решения (см. Комментарий @timotree). Если вы хотите сделать это самостоятельно, проявите больше гибкости или научитесь, вы можете написать процедурный макрос, который будет анализировать обратную трассировку (полученную изнутри вызываемой функции) и распечатывать необходимую вам информацию. Вот процедурный макрос внутри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()
}
Трассировка анализируется, чтобы найти самый ранний символ внутри исходного файла (полученного с помощью file!()
, другой макрос). Код, который нам нужно добавить к функции, определяется в строке, которая затем анализируется какTokenStream
и добавлен в начало тела функции. Мы могли бы добавить эту логику в конце, но тогда возврат значения без точки с запятой больше не будет работать. Затем вы можете использовать процедурный макрос в своем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);
}
Результат:
> Called from "src/main.rs" at line 10
> Called from "src/main.rs" at line 11
Не забудьте указать, что ваш lib
crate предоставляет процедурные макросы, добавляя эти две строки в ваш Cargo.toml
:
[lib]
proc-macro = true
TL;DR
Вот процедурный макрос, который использует syn
а также 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"
Подробное объяснение
Макрос может расширяться только до кода, который с самого начала можно написать вручную. Зная это, я вижу здесь два вопроса:
- Как я могу написать функцию, которая отслеживает местоположение вызывающей стороны?
- См. Как я могу получить доступ к месту вызова функции каждый раз, когда она вызывается?
Краткий ответ: чтобы получить место, в котором вызывается ваша функция, отметьте его
#[track_caller]
и использоватьstd::panic::Location::caller
в его теле.
- См. Как я могу получить доступ к месту вызова функции каждый раз, когда она вызывается?
- Как я могу написать процедурный макрос, который создает такие функции?
Первая попытка
Нам нужен процедурный макрос, который
- берет на себя функцию,
- отмечает это
#[track_caller]
, - и добавляет строку, которая печатает
Location::caller
.
Например, он преобразует такую функцию:
fn foo() {
// body of foo
}
в
#[track_caller]
fn foo() {
println!("{}", std::panic::Location::caller());
// body of foo
}
Below, I present a procedural macro that executes that transformation exactly — although, as you'll see in later versions, you probably want something different. To try this code, like before in the TL;DR section, put it into its own crate and add its dependencies to the 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)
}
Example usage:
// 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`
}
Unfortunately, we won't be able to get away with that simple version. There are at least two problems with that version:
How it composes with
async fn
s:- Instead of printing the caller location, it prints the location in which our macro (
#[print_caller_location]
) is invoked. For example:
// 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`" }
- Instead of printing the caller location, it prints the location in which our macro (
How it works with other invocations of itself, or generally, of
#[track_caller]
:- Nested functions with
#[print_caller_location]
will print the location of the root caller, rather than the direct caller of a given function. For example:
// 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` }
- Nested functions with
Addressing async fn
s
It is possible to work around the problem with async fn
s using -> impl Future
, for example, if we wanted our 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()
к 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)
}