Hi there, fellow Rustaceans! 🦀
In today’s article, we’ll dive into Rust's mechanisms for code generation, including macros, procedural macros, and code generation during build time.
We’ll see some detailed examples and explanations to help you master code generation in Rust.
Introduction to Macros in Rust
Macros in Rust allow you to write code that writes other code, which is known as metaprogramming. They are a powerful feature used to reduce code repetition and improve maintainability.
Declarative Macros
Declarative macros, defined with the macro_rules! macro, allow pattern matching on the code provided to the macro and are used to perform syntactic manipulations.
Example: Implementing a vec! macro
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
fn main() {
let a = vec![1, 2, 3];
println!("{:?}", a); // Output: [1, 2, 3]
}
In this example, $( $x:expr ),* matches zero or more expressions separated by commas. The $(...)* construct is then used to repeat the enclosed block of code for each matched expression.
Procedural Macros
Procedural macros are more powerful and flexible than declarative macros. They allow you to operate on the Rust Abstract Syntax Tree (AST), enabling transformations that are not possible with declarative macros.
Example: Implementing a simple derive macro
Let’s create a custom derive macro for a Debug trait that generates a simple debug representation for structs.
- Add Dependencies: First, add
proc-macro2,quote, andsynto yourCargo.tomlfor working with Rust's AST and generating code. - Define the Macro: In a new crate of type
proc-macro, define your procedural macro.
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(SimpleDebug)]
pub fn simple_debug_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let gen = quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, stringify!(#name))
}
}
};
gen.into()
}
In this example, #[proc_macro_derive(SimpleDebug)] defines a derive macro. The quote! macro is used to generate Rust code from the provided tokens, and stringify! converts the struct name into a string.
Code Generation During Build Time
Rust’s build script (build.rs) can be used to generate code during build time. This is useful for including generated files into your source code or for embedding resources.
Example: Generating Code with build.rs
- Create
build.rs: In your project root, create abuild.rsfile. - Write the Build Script: Generate code and write it to a file that can be included in your project.
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("hello.rs");
let mut f = File::create(&dest_path).unwrap();
f.write_all(b"
pub fn say_hello() {
println!(\"Hello, world!\");
}
").unwrap();
}
Include the Generated Code: Use the include! macro to include the generated code in your Rust file.
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
fn main() {
say_hello(); // This function is generated by build.rs
}
Advanced Code Generation Techniques in Rust
Taking our exploration of Rust’s code generation capabilities further, we delve into more advanced techniques that cater to complex scenarios and enhance the robustness and efficiency of Rust applications.
Attribute Macros
Attribute macros are similar to procedural macros but are used to annotate functions, structs, enums, or other items. They can inspect, modify, or generate additional code based on the annotated item.
Example: Logging Attribute Macro
Imagine an attribute macro #[log_args] that logs the arguments passed to a function.
Define the Macro: In your proc-macro crate, define the attribute macro.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn log_args(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_body = &input_fn.block;
let fn_args = input_fn.sig.inputs.iter().map(|arg| {
quote! { std::format!("{:?}", #arg) }
});
let gen = quote! {
#input_fn
fn #fn_name(#fn_args) {
println!("Called with args: {:?}", vec![#(#fn_args),*]);
#fn_body
}
};
gen.into()
}
In this example, the macro parses the input function, and the quote! macro generates a new function that logs its arguments before calling the original function.
Derive Macros for Custom Traits
Derive macros can be extended to implement custom traits for structs or enums, enabling automatic trait implementation based on the struct’s fields.
Example: Custom ToJson Trait
Assume you have a custom trait ToJson that converts a struct into a JSON string.
Define the Trait: In your main crate, define the ToJson trait.
pub trait ToJson {
fn to_json(&self) -> String;
}
Implement the Derive Macro: In your proc-macro crate, implement the derive macro for ToJson.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data};
#[proc_macro_derive(ToJson)]
pub fn to_json_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let fields = if let Data::Struct(data) = &ast.data {
data.fields.iter().map(|f| {
let name = &f.ident;
quote! { format!("\"{}\": \"{:?}\"", stringify!(#name), &self.#name) }
})
} else {
panic!("ToJson can only be derived for structs");
};
let gen = quote! {
impl ToJson for #name {
fn to_json(&self) -> String {
format!("{{ {} }}", vec![#(#fields),*].join(", "))
}
}
};
gen.into()
}
This macro implementation iterates over struct fields, generating a JSON string representation for each field.
Generating Code with External Tools
In some cases, you might need to generate Rust code from external tools or files, such as protocol buffers (Protobuf) or interface definition languages (IDLs).


