Implement a Custom log_execution Attribute Macro in Rust
This challenge will guide you through the creation of a procedural attribute macro in Rust. You will build a macro named log_execution that, when applied to a function, will automatically log a message to the console before the function's execution begins and after it completes. This is a common pattern for debugging and performance monitoring, allowing you to easily track when and how often functions are called.
Problem Description
Your task is to create a procedural attribute macro in Rust called log_execution. This macro should be applicable to functions.
Key Requirements:
- Macro Definition: Define a procedural attribute macro named
log_execution. - Function Wrapping: When
#[log_execution]is applied to a function, the macro should wrap the original function's body. - Logging Before Execution: Before the original function's code is executed, the macro should print a message to
stdoutindicating that the function is about to be executed. The message should include the name of the function. - Logging After Execution: After the original function's code has finished executing (regardless of whether it returns successfully or panics), the macro should print a message to
stdoutindicating that the function has completed. This message should also include the function's name. - Preserve Function Signature: The macro must preserve the original function's signature, including its arguments, return type, and visibility.
- Handle Different Return Types: The macro should correctly handle functions that return values, functions that return
()(unit type), and functions thatpanic!.
Expected Behavior:
When a function is annotated with #[log_execution], its execution should be surrounded by logging statements.
Edge Cases to Consider:
- Functions with no arguments.
- Functions with various argument types.
- Functions returning different types (including
()). - Functions that might
panic!.
Examples
Example 1: Simple Function with No Return Value
#[log_execution]
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
greet("World");
}
Expected Output:
[LOG] Entering function: greet
Hello, World!
[LOG] Exiting function: greet
Example 2: Function with a Return Value
#[log_execution]
fn add(a: i32, b: i32) -> i32 {
println!("Performing addition...");
a + b
}
fn main() {
let sum = add(5, 3);
println!("Result: {}", sum);
}
Expected Output:
[LOG] Entering function: add
Performing addition...
[LOG] Exiting function: add
Result: 8
Example 3: Function that Panics
#[log_execution]
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero!");
}
a / b
}
fn main() {
// This call will panic
let _ = std::panic::catch_unwind(|| {
divide(10, 0);
});
println!("This line might not be reached if the panic is unhandled.");
}
Expected Output (when run in a context that catches the panic, like catch_unwind or a test that allows panics):
[LOG] Entering function: divide
[LOG] Exiting function: divide (panicked: Division by zero!)
Note: The exact panic message formatting might vary slightly depending on the Rust version and how the panic is handled.
Constraints
- The macro must be implemented using Rust's procedural macro system.
- You should use the
synandquotecrates for parsing and generating Rust code. - The logging output should be directed to
stdout. - The macro should be part of a library crate (e.g.,
proc-macro = trueinCargo.toml).
Notes
- You'll need to create a separate crate for your procedural macro.
- Consider how to handle the function's return value, especially when logging after execution. A
std::panic::catch_unwindblock within the macro's generated code is a good approach to ensure the "exiting" log message is always printed. - The function name can be extracted from the
synAST. - This challenge is about understanding procedural macros, AST manipulation, and code generation. Focus on correctness and clarity of the generated code.