Можем ли мы получить исходный код местоположения вызывающей стороны в процедурном макро-атрибуте? - PullRequest
2 голосов
/ 15 марта 2020

У меня есть требование получить исходное местоположение вызывающей стороны каждого метода. Я пытаюсь создать 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 ]

2 голосов
/ 18 марта 2020

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"

Подробное объяснение

A макрос может расширяться только до кода, который можно написать вручную для начала. Зная это, я вижу здесь два вопроса:

  1. Как мне написать функцию, которая отслеживает местоположение вызывающего абонента?
  2. Как мне написать процедурный макрос, который создает такие функции?

Начальная попытка

Нам нужен процедурный макрос, который

  • принимает функцию,
  • отмечает ее #[track_caller],
  • и добавляет строку, которая печатает Location::caller.

Например, она преобразует такую ​​функцию:

fn foo() {
    // body of foo
}

в

#[track_caller]
fn foo() {
    println!("{}", std::panic::Location::caller());
    // body of foo
}

Ниже я представляю процедурный макрос, который точно выполняет это преобразование - хотя, как вы увидите в более поздних версиях, вы, вероятно, захотите что-то другое. Чтобы попробовать этот код, как и раньше в разделе TL; DR, поместите его в свой ящик и добавьте его зависимости в 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] будут печатать местоположение вызывающей стороны root, а не прямой вызывающей стороны данной функции. Например:

    // 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 Например, используя -> 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-интерфейс publi c функции с 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] функций

Проблемное поведение c сводит к минимуму этот факт: когда Функция #[track_caller], foo, непосредственно вызывает другую функцию #[track_caller], bar, Location::caller предоставит им обоим доступ к вызывающей стороне foo. Другими словами, Location::caller предоставляет доступ к вызывающему root в случае вложенных функций #[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)
}
1 голос
/ 18 марта 2020

Доступны готовые решения (см. Комментарий @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 и добавляется в начале тела функции. Мы могли бы добавить этот лог c в конце, но тогда возвращение значения без точки с запятой больше не будет работать. Затем вы можете использовать процедурный макрос в вашем 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 ящик предоставление процедурных макросов путем добавления этих двух строк к вашему Cargo.toml:

[lib]
proc-macro = true
...