Implementing a Custom Attribute Macro in Rust
Attribute macros in Rust allow you to modify or add behavior to items like functions, structs, modules, and more. This challenge asks you to implement a custom attribute macro that automatically generates a debug print statement for a given function, logging its name and arguments when it's called. This is a common pattern for debugging and can be automated with macros.
Problem Description
You need to create a Rust attribute macro named debug_log that, when applied to a function, will wrap the function's execution with code that prints a debug message to the console. The debug message should include the function's name and its arguments. The original function's logic should still be executed as normal.
Key Requirements:
- The macro must be a function-level attribute.
- The macro should print a debug message to
std::io::stdoutbefore the function executes. - The debug message should include the function's name and a string representation of its arguments.
- The original function should be executed without modification to its core logic.
- The macro should handle functions with any number of arguments and any argument types.
Expected Behavior:
When a function is decorated with the #[debug_log] attribute, calling that function will first print a debug message to the console, followed by the function's original return value.
Edge Cases to Consider:
- Functions with no arguments.
- Functions with complex argument types (structs, enums, etc.).
- Functions returning different types.
- Error handling within the original function (the debug log should still execute even if the function panics).
Examples
Example 1:
#[debug_log]
fn add(x: i32, y: i32) -> i32 {
x + y
}
fn main() {
let result = add(5, 3);
println!("Result: {}", result);
}
Output:
Debug: add(x: 5, y: 3)
Result: 8
Explanation: The add function is decorated with #[debug_log]. When add(5, 3) is called, the macro first prints "Debug: add(x: 5, y: 3)" to the console, then executes the function, and finally prints "Result: 8".
Example 2:
#[debug_log]
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
greet("Alice");
}
Output:
Debug: greet(name: "Alice")
Hello, Alice!
Explanation: The greet function is decorated with #[debug_log]. The macro prints "Debug: greet(name: "Alice")" before printing "Hello, Alice!".
Example 3:
#[debug_log]
fn no_args() {
println!("No arguments here!");
}
fn main() {
no_args();
}
Output:
Debug: no_args()
No arguments here!
Explanation: The no_args function has no arguments. The macro correctly prints "Debug: no_args()" before executing the function.
Constraints
- The macro must be implemented using procedural macros.
- The debug message must be printed to
std::io::stdout. - The macro should be compatible with functions that return any type.
- The macro should not significantly impact the performance of the original function (avoid unnecessary allocations or complex operations within the macro itself).
- The macro should compile and run without errors.
Notes
- You'll need to create a new Rust crate with the
proc-macrofeature enabled. - Consider using the
quote!macro from theproc-macro2crate to generate the code that wraps the function. - The
TokenStreamtype represents a sequence of tokens. You'll need to parse the function's signature from theTokenStreamto extract its name and arguments. - The
syncrate can be helpful for parsing Rust code into a structured representation. - Remember to handle potential errors gracefully during parsing and code generation. A panic within the macro can lead to difficult-to-debug issues.