Loading pageā¦
Rust walkthroughs
Loading pageā¦
quote::quote! macro in procedural macro development and how does it differ from string interpolation?The quote! macro is the standard tool for generating Rust code in procedural macros. It produces TokenStream values that represent real Rust syntax, not string representations of code. This distinction is fundamental to writing correct and maintainable procedural macros.
Procedural macros receive and produce TokenStream objects, not strings:
use proc_macro::TokenStream;
// A simple derive macro
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// input is a TokenStream, not a string
// We must return a TokenStream, not a string
// This would NOT work:
// "fn hello() { println!(\"Hello\"); }".parse().unwrap()
// quote! produces a TokenStream directly
quote::quote! {
fn hello() {
println!("Hello");
}
}
}A TokenStream represents a sequence of tokensāidentifiers, literals, punctuation, and groupsāwith full fidelity to the original source code, including spans for error reporting.
The quote! macro produces TokenStream values directly:
use quote::quote;
fn basic_quote() -> proc_macro2::TokenStream {
// This generates Rust tokens, not a string
quote! {
fn greet() {
println!("Hello, world!");
}
}
}The output is a TokenStream that the compiler understands as valid Rust syntax.
The # operator interpolates values into the token stream:
use quote::quote;
fn interpolation_example() {
let name = "greet";
let message = "Hello";
let tokens = quote! {
fn #name() {
println!(#message);
}
};
// Generates:
// fn greet() {
// println!("Hello");
// }
}The # operator works with any type that implements ToTokens, not just strings.
Rust types implement ToTokens automatically:
use quote::quote;
use syn::{Ident, Type};
fn type_interpolation(fn_name: Ident, param_type: Type, return_type: Type) {
let tokens = quote! {
fn #fn_name(value: #param_type) -> #return_type {
value
}
};
// If fn_name = "process", param_type = "i32", return_type = "String"
// Generates:
// fn process(value: i32) -> String {
// value
// }
}The types are interpolated as their token representations, not as string literals.
quote! supports iteration over collections:
use quote::quote;
fn repetition_example() {
let fields = vec!["name", "age", "email"];
let tokens = quote! {
struct Person {
#( #fields: String, )*
}
};
// Generates:
// struct Person {
// name: String,
// age: String,
// email: String,
// }
}The #(...)* syntax repeats the pattern for each element in the iterator.
Multiple variables can be iterated in parallel:
use quote::quote;
use proc_macro2::Ident;
fn parallel_repetition(names: Vec<Ident>, types: Vec<Ident>) {
assert_eq!(names.len(), types.len());
let tokens = quote! {
struct Record {
#( #names: #types, )*
}
};
// If names = [id, name, active], types = [i32, String, bool]
// Generates:
// struct Record {
// id: i32,
// name: String,
// active: bool,
// }
}All iterated variables must have the same length.
String-based code generation is fragile and error-prone:
fn string_interpolation_pitfalls() {
let fn_name = "my function"; // Contains a space!
let type_name = "Vec<i32>";
// String interpolation (wrong approach)
let code = format!(
"fn {}(value: {}) {{ value }}",
fn_name, type_name
);
// Produces invalid Rust: "fn my function(value: Vec<i32>) { value }"
// This won't parse!
// quote! approach
let fn_name = syn::Ident::new("my_function", proc_macro2::Span::call_site());
let type_name: syn::Type = syn::parse_quote!(Vec<i32>);
let tokens = quote::quote! {
fn #fn_name(value: #type_name) {
value
}
};
// Produces valid tokens, caught at compile time if types are wrong
}String interpolation produces strings that must be parsed, catching errors late in the process.
quote! preserves spans, enabling accurate error messages:
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use proc_macro::TokenStream;
#[proc_macro_derive(Validate)]
pub fn validate_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// The input tokens carry span information
let name = &input.ident;
// If we generate code using name, its span is preserved
let expanded = quote! {
impl Validate for #name {
fn validate(&self) -> Result<(), String> {
if self.value < 0 {
// Error points to the struct definition, not here
return Err(format!("{}: value must be positive", stringify!(#name)));
}
Ok(())
}
}
};
expanded.into()
}When the generated code has errors, the compiler can point to the original source location.
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(BadMacro)]
pub fn bad_macro(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// String interpolation loses spans
let code = format!(
"impl {} {{ fn bad_syntax(&self) -> {{ }} }}",
name
);
// Parse error will point to this line, not the original source
code.parse().unwrap()
}Error messages from string-generated code are unhelpful because the compiler has no source location information.
use quote::quote;
fn quote_vs_format() {
let count = 5;
let name = "items";
// format! produces a String
let string = format!("let {} = {};", name, count);
// "let items = 5;"
// But this is just text, not tokens
// quote! produces a TokenStream
let tokens = quote! {
let #name = #count;
};
// Tokens: `let` `items` `=` `5` `;`
// Each token has span information
// More importantly, quote! handles Rust syntax correctly
let field_name = "type"; // Reserved keyword in Rust!
// This would create invalid Rust code as a string:
let bad_string = format!("struct S {{ {}: i32 }}", field_name);
// "struct S { type: i32 }" - syntax error!
// But quote! can handle it with proper escaping:
let field = syn::Ident::new("r#type", proc_macro2::Span::call_site());
let tokens = quote! {
struct S {
#field: i32,
}
};
// Generates: struct S { r#type: i32, }
// Valid Rust using raw identifier syntax
}When parsing input, you get tokens with proper spans:
use syn::{parse_macro_input, DeriveInput, Data, Fields};
use quote::quote;
use proc_macro::TokenStream;
#[proc_macro_derive(Builder)]
pub fn builder_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let builder_name = syn::Ident::new(
&format!("{}Builder", name),
name.span(), // Preserve span from original
);
let fields = match &input.data {
Data::Struct(data) => {
match &data.fields {
Fields::Named(fields) => {
fields.named.iter().map(|f| {
let name = &f.ident;
let ty = &f.ty;
quote! {
#name: Option<#ty>,
}
})
},
_ => panic!("Only named fields supported"),
}
},
_ => panic!("Only structs supported"),
};
let expanded = quote! {
pub struct #builder_name {
#(#fields)*
}
impl #name {
pub fn builder() -> #builder_name {
#builder_name {
// Initialize all fields to None
}
}
}
};
expanded.into()
}The quote! macro preserves all span information from the input tokens.
quote! can be nested for complex code generation:
use quote::quote;
fn nested_quote() {
let outer_name = "outer";
let inner_name = "inner";
let inner_code = quote! {
fn #inner_name() {
println!("Inner function");
}
};
let outer_code = quote! {
mod #outer_name {
#inner_code
}
};
// Generates:
// mod outer {
// fn inner() {
// println!("Inner function");
// }
// }
}For complex cases, build tokens programmatically:
use quote::{quote, quote_spanned, ToTokens};
use proc_macro2::TokenStream;
use syn::{Ident, Type};
fn dynamic_generation(fields: Vec<(Ident, Type)>) -> TokenStream {
let mut field_tokens = TokenStream::new();
for (name, ty) in fields {
let field_code = quote! {
#name: #ty,
};
field_tokens.extend(field_code);
}
quote! {
struct Generated {
#field_tokens
}
}
}use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Ident};
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn check(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
if let Some(first_field) = get_first_field(&input) {
let field_ident = &first_field.ident;
let field_ty = &first_field.ty;
// Use quote_spanned to attach error to specific field
let check = quote_spanned! {field_ty.span()=>
const _: () = {
fn _check() {
// Error will point to the field type
let _: #field_ty = String::new();
}
};
};
return quote! {
#input
#check
}.into();
}
input.into()
}
fn get_first_field(input: &DeriveInput) -> Option<&syn::Field> {
// Implementation omitted
None
}The quote_spanned! macro assigns a specific span to generated tokens, controlling where errors appear.
The standard pattern for procedural macros:
use syn::{parse_macro_input, DeriveInput, Data, Fields, Field};
use quote::quote;
use proc_macro::TokenStream;
#[proc_macro_derive(Deserialize)]
pub fn deserialize_derive(input: TokenStream) -> TokenStream {
// 1. Parse input into a syntax tree
let input = parse_macro_input!(input as DeriveInput);
// 2. Extract relevant parts
let name = &input.ident;
// 3. Process the syntax tree
let fields: Vec<_> = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => fields.named.iter().collect(),
_ => vec![],
},
_ => vec![],
};
// 4. Generate code with quote!
let field_names: Vec<_> = fields.iter()
.filter_map(|f| f.ident.as_ref())
.collect();
let expanded = quote! {
impl SomeTrait for #name {
fn deserialize(map: &mut std::collections::HashMap<String, String>) -> Self {
Self {
#(
#field_names: map.remove(stringify!(#field_names))
.and_then(|v| v.parse().ok())
.unwrap_or_default(),
)*
}
}
}
};
// 5. Return TokenStream
expanded.into()
}This patternāparse with syn, generate with quoteāis the foundation of Rust procedural macro development.
use quote::quote;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
fn token_conversions() {
// quote! produces proc_macro2::TokenStream
let tokens: TokenStream2 = quote! {
fn example() {}
};
// Convert to proc_macro::TokenStream for return
let tokens: TokenStream = tokens.into();
// Or use .into() when returning
let result: TokenStream = quote! {
fn another() {}
}.into();
}The quote! macro generates TokenStream values that represent Rust code as tokens, not strings. This is fundamentally different from string interpolation:
Token-based generation (quote!):
TokenStream directly, no parsing required#(...)*String interpolation (format!):
The quote! macro, combined with syn for parsing, provides a type-safe, span-preserving way to generate Rust code. Variable interpolation with # works with any type implementing ToTokens, not just strings, enabling composition of complex code structures. Repetition with #(...)* handles iteration patterns common in macro code generation.
For procedural macros, always use quote! (or quote_spanned! for custom error locations) instead of string interpolation. The type safety and span preservation alone prevent entire categories of bugs, and the syntax is more maintainable than string concatenation.