I have requirement to get the source location of the caller of every method. I am trying to create a proc_macro_attribute
to capture the location and print it.
#[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
}
TL;DR
Here is a procedural macro that uses
syn
andquote
to do what you've described:Make sure to put it in its on crate and add these lines to its
Cargo.toml
:In-depth explanation
A macro can only expand to code that's possible to write by hand to begin with. Knowing this, I see two questions here:
Initial attempt
We want a procedural macro that
#[track_caller]
,Location::caller
.For example, it would transform a function like this:
into
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
.Example usage:
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:#[print_caller_location]
) is invoked. For example:How it works with other invocations of itself, or generally, of
#[track_caller]
:#[print_caller_location]
will print the location of the root caller, rather than the direct caller of a given function. For example:Addressing
async fn
sIt is possible to work around the problem with
async fn
s using-> impl Future
, for example, if we wanted ourasync fn
counter-example to work correctly, we could instead write:We could add a special case that applies this transformation to our macro. However, that transformation changes the public API of the function from
async fn foo()
tofn foo() -> impl Future<Output = ()>
in addition to affecting the auto traits that the returned future can have.Therefore I recommend that we allow users to use that workaround if they desire, and simply emit an error if our macro is used on an
async fn
. We can do this by adding these lines to our macro code:Fixing nested behavior of
#[print_caller_location]
functionsThe problematic behaviour minimizes down to this fact: When a
#[track_caller]
function,foo
, directly calls into another#[track_caller]
function,bar
,Location::caller
will give both of them access tofoo
's caller. In other words,Location::caller
gives access to the root caller in the case of nested#[track_caller]
functions:playground link
To remedy this, we need to break the chain of
#[track_caller]
calls. We can break the chain by hiding the nested call tobar
within a closure:playground link
Now that we know how to break the chain of
#[track_caller]
functions, we can address this problem. We just need to make sure that if the user actually marks their function with#[track_caller]
on purpose, we refrain from inserting the closure and breaking the chain.We can add these lines to our solution:
Final solution
After those two changes, we've ended up with this code:
Ready to use solutions are available (see @timotree 's comment). If you want to do this yourself, have more flexibility or learn, you can write a procedural macro that will parse a backtrace (obtained from inside the function that is called) and print the information that you need. Here is a procedural macro inside a
lib.rs
:The backtrace is parsed to find the earliest symbol inside the source file (retrieved using
file!()
, another macro). The code we need to add to the function is defined in a string, that is then parsed as aTokenStream
and added at the beginning of the function's body. We could have added this logic at the end, but then returning a value without a semicolon wouldn't work anymore. You can then use the procedural macro in yourmain.rs
as follow:The output is:
Don't forget to specify that your
lib
crate is providing procedural macros by adding these two lines to yourCargo.toml
: