Remove Boilerplate Code from src/lib.rs
Mục tiêu: Delete the default boilerplate code generated by cargo new --lib in the src/lib.rs file. This prepares the crate for writing the actual procedural macro logic by starting with a clean, empty file.
With the project configuration in Cargo.toml now complete, our foundation is set. You’ve officially told the Rust compiler that you’re building a procedural macro and have supplied it with the necessary tools, syn and quote. It’s time to shift our focus from the project’s manifest to its heart: the source code. Our journey into writing the macro logic begins in the src/lib.rs file.
Examining the Default Library Code
When you ran cargo new my_builder_macro --lib, Cargo helpfully generated a src/lib.rs file with some boilerplate code. This default code is a great starting point for a standard library crate, as it demonstrates a simple public function and a corresponding unit test.
Open your src/lib.rs file, and you will see the following content:
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Let’s briefly break down what this code does and, more importantly, why it’s not what we need for our procedural macro.
pub fn add(...): This is a simple, publicly available function that any other crate depending on this library could call.#[cfg(test)] mod tests { ... }: This is an inline test module. The#[cfg(test)]attribute is a conditional compilation flag. It tells the Rust compiler to only compile and include thistestsmodule when you runcargo test. It will be completely ignored during a normalcargo build. This is Rust’s idiomatic way to keep unit tests alongside the code they are testing.
Why We Remove It: The Nature of a Proc-Macro Crate
A procedural macro crate is a special kind of library. Its primary purpose is not to export functions that are called at runtime, but rather to export macros that are executed by the compiler at compile time to generate code.
Because of this fundamental difference:
- We won’t be exporting regular functions like
add. The only public items in our crate will be the macro functions themselves. - Our testing strategy will be different. While unit tests are fantastic, the most effective way to test a procedural macro is to see if the code it generates actually compiles and works as expected. We will use a dedicated testing library called
trybuildfor this later, which involves creating separate test cases that the compiler attempts to build. The defaultmod testsstructure is therefore not suitable for our main testing approach.
For these reasons, the best practice is to start with a completely clean slate. Your task now is to delete all the boilerplate code inside src/lib.rs, leaving you with an empty file. This ensures that our crate contains only the logic specific to our Builder macro.
After you have removed the code, your src/lib.rs file should be completely empty.
We now have a blank canvas, ready for us to paint our metaprogramming masterpiece. In the very next task, we will write our first lines of code by importing the essential types we’ll need from the proc_macro, syn, and quote crates.
Further Reading
To better understand the concepts demonstrated in the boilerplate code you just removed, you can explore the following official resources:
- The Rust Programming Language: How to Write Tests: A comprehensive guide to Rust’s built-in testing framework. https://doc.rust-lang.org/book/ch11-01-writing-tests.html
- The Rust Programming Language: Bringing Paths into Scope with the
useKeyword: An explanation of howmodandusework together. https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html
Set Up a Rust Procedural Macro: Essential Imports
Mục tiêu: Add the three foundational use statements (proc\_macro::TokenStream, syn, and quote) to your Rust procedural macro crate. This step brings the core tools for parsing, code generation, and token stream manipulation into scope.
Having cleared the boilerplate in the previous step, you are now looking at a blank canvas in your src/lib.rs file. This is the perfect place to start building our procedural macro. The very first step in writing any Rust code is to bring the necessary types, traits, and functions into scope with use statements. For a procedural macro, there are three indispensable imports that form the foundation of our work.
The Holy Trinity of Procedural Macros
Every procedural macro you write will begin with importing three key components. Think of them as your specialized toolkit: one for handling the raw code, one for understanding its structure, and one for building new code.
Let’s add them to the top of your src/lib.rs file and then break down what each one does in detail.
// This line brings the TokenStream type into scope. TokenStream is the fundamental
// data structure that represents the input and output of a procedural macro.
// It's essentially a sequence of Rust code tokens.
use proc_macro::TokenStream;
// The quote macro is the primary tool for generating Rust code. It allows you to
// write code templates that can be filled with variables, producing a new TokenStream.
use quote::quote;
// syn is the parsing library we use to interpret the input TokenStream. It can
// parse Rust code into a structured data format called an Abstract Syntax Tree (AST),
// making it easy to inspect and analyze the code our macro is attached to.
use syn;
Now, let’s explore the role of each of these imports in our project.
use proc_macro::TokenStream; - The Currency of Macros
The proc_macro crate is special. Unlike syn and quote, you didn’t add this to your Cargo.toml. That’s because it’s a “compiler-provided” crate. When you set proc-macro = true in your manifest, the Rust compiler automatically makes this crate available to your code.
The most important type from this crate is TokenStream. This is the single, fundamental data type that is passed into your macro function and is also the type that your macro function must return.
- Input: When a user writes
#[derive(Builder)]on their struct, the Rust compiler takes the entire struct definition, converts it into aTokenStream, and passes it to your macro as an argument. - Output: Your macro’s job is to process this input and generate a new
TokenStreamcontaining the implementation of the Builder pattern. The compiler then takes this output and inserts it into the user’s code.
You can think of TokenStream as the raw, unrefined material of Rust code. It’s more structured than a plain string of text, but it’s not yet easy to work with logically. That’s where our next import comes in.
use syn; - The Code Parser
The syn crate (which we did add to Cargo.toml) is the premier parsing library for Rust procedural macros. Its name is short for “syntax,” and its core purpose is to take the raw TokenStream and turn it into a highly structured and easy-to-navigate data structure known as an Abstract Syntax Tree (AST).
For our derive macro, syn will parse the input TokenStream representing a struct into a syn::DeriveInput struct. This Rust struct will give us convenient access to everything we need to know: * The name of the struct (e.g., Command). * Its visibility (e.g., pub). * A list of its fields, where for each field we can access its name and its type.
Without syn, we would have to manually iterate through the tokens (pub, struct, Command, {, executable, :, String, ,, ...) and try to make sense of them, which would be incredibly difficult and fragile. syn handles all this complexity for us.
use quote::quote; - The Code Generator
Once syn has helped us understand the input struct, our next job is to generate the new code for the builder. This is the job of the quote crate.
The quote! macro it provides is a form of “quasi-quoting.” This is a fancy term for a simple and powerful idea: you can write out Rust code almost exactly as it should appear, and then “unquote” or splice variables from your program directly into it.
For example, we’ll be able to write something like this:
// (This is a preview, not for the current task)
let builder_name = /* ... */;
let builder_fields = /* ... */;
let generated_code = quote! {
pub struct #builder_name {
#(#builder_fields),*
}
};
The quote! macro will take our variables (builder_name, builder_fields) and seamlessly integrate them into the code template, handling all the tedious details of converting everything back into a valid TokenStream. It is the engine that will construct our final output.
With these three use statements in place, we have successfully equipped our macro with the essential tools for its mission. We are now ready to define the function that will act as the entry point for our #[derive(Builder)] logic.
Further Reading
To deepen your understanding of these foundational components, exploring their official documentation is highly recommended.
proc_macroCrate Documentation: https://doc.rust-lang.org/proc_macro/synCrate Documentation: https://docs.rs/syn/quoteCrate Documentation: https://docs.rs/quote/
Define the Macro’s Entry Point Function
Mục tiêu: Create the main derive\_builder function with the required pub fn(TokenStream) -> TokenStream signature. This function will serve as the entry point for the Builder derive macro and will initially be scaffolded with an unimplemented!() body.
Excellent! With the essential use statements now in place, you’ve set the stage for the core logic of our procedural macro. The next logical step is to define the function that will serve as the main entry point for our code generation. This function is where the magic happens: it receives the user’s code as input and produces the builder implementation as output.
The Macro’s Entry Point: The Function Signature
Every procedural macro needs a public function that the Rust compiler can call. The signature of this function is strictly defined. It must accept one argument of type proc_macro::TokenStream and must return a value of the same type. Let’s break down the structure of the function we’re about to write: pub fn derive_builder(input: TokenStream) -> TokenStream.
pub(public visibility): The function must be public so that it’s visible outside of our crate. A procedural macro crate’s public API is precisely its collection of macro-defining functions. The Rust compiler needs to be able to “see” and call this function when it encounters our#[derive(Builder)]attribute in another crate.fn derive_builder(the function name): The name itself is a convention, but it’s good practice to make it descriptive. A name likederive_builderclearly communicates its purpose. This is the internal name of our function; the name the user will see in#[derive(Builder)]is defined in the attribute we will add in the next task.(input: TokenStream)(the input parameter): This is the heart of the macro’s input. Theinputparameter will contain the entire block of code that the#[derive]attribute is attached to. For example, if a user writes:rust #[derive(Builder)] pub struct Command { executable: String, args: Vec<String>, }TheinputTokenStreamwill contain the tokens forpub struct Command { ... }. We will later use thesyncrate to parse this stream into a structured format we can easily work with.-> TokenStream(the return type): Just as the macro receives code as aTokenStream, it must also produce code in the same format. Our function’s job is to build up a newTokenStreamcontaining the generatedCommandBuilderstruct, itsnewfunction, its setter methods, and itsbuildmethod. The compiler will then take this returnedTokenStreamand insert it into the user’s code.
Writing the Function Skeleton
For now, we will simply define the function with an empty body. This creates the necessary structure that we will fill in during the subsequent steps. Your src/lib.rs file should now contain the imports from the previous task followed by this new function definition.
// This line brings the TokenStream type into scope. TokenStream is the fundamental
// data structure that represents the input and output of a procedural macro.
// It's essentially a sequence of Rust code tokens.
use proc_macro::TokenStream;
// The quote macro is the primary tool for generating Rust code. It allows you to
// write code templates that can be filled with variables, producing a new TokenStream.
use quote::quote;
// syn is the parsing library we use to interpret the input TokenStream. It can
// parse Rust code into a structured data format called an Abstract Syntax Tree (AST),
// making it easy to inspect and analyze the code our macro is attached to.
use syn;
// This is the main function for our procedural macro.
// It takes a TokenStream as input (the struct we're deriving the builder for)
// and returns a TokenStream (the generated builder code).
pub fn derive_builder(input: TokenStream) -> TokenStream {
// The body is currently empty. We will implement the parsing and code generation
// logic in the upcoming steps.
unimplemented!()
}
We’ve used the unimplemented!() macro in the function body. This is a standard Rust macro that will cause the program to panic if it’s ever executed. It’s a useful placeholder that satisfies the compiler’s requirement for the function to return a TokenStream, while clearly indicating that the implementation is not yet complete.
You’ve now successfully scaffolded the main function of our macro! However, the compiler doesn’t yet know that this specific function is responsible for implementing a derive macro named Builder. In our very next task, we will bridge this gap by adding a special attribute to our function, officially registering it as the #[derive(Builder)] implementation.
Further Reading
To understand more about the specific structure of procedural macro functions, refer to the official documentation.
- The Rust Reference: Procedural Macros: A detailed technical explanation of how procedural macros are defined and how they interact with the compiler. https://doc.rust-lang.org/reference/procedural-macros.html
- The
proc_macroCrate Documentation: The API documentation for the compiler-providedproc_macrocrate, includingTokenStream. https://doc.rust-lang.org/proc_macro/
Register a Custom Derive Procedural Macro
Mục tiêu: Use the #[proc\_macro\_derive] attribute to register a function as the implementation for a custom derive macro, linking the public-facing macro name (e.g., Builder) to the function.
In the previous task, you successfully defined the function pub fn derive_builder(...), creating the skeleton for our macro’s logic. However, as it stands, the Rust compiler simply sees this as a public function with a specific signature. It has no way of knowing that this particular function is intended to be a procedural macro, let alone which one. Our current task is to bridge this gap by explicitly registering our function with the compiler as the implementation for #[derive(Builder)].
Registering the Macro with #[proc_macro_derive]
To turn a regular function into a procedural macro, we use a special kind of attribute provided by the proc_macro crate. There are three types of procedural macros, each with its own registration attribute:
- Function-like macros:
#[proc_macro](e.g.,my_macro!(...)) - Attribute macros:
#[proc_macro_attribute](e.g.,#[my_attribute]) - Custom
derivemacros:#[proc_macro_derive](e.g.,#[derive(MyTrait)])
Since we are creating a custom derive macro, we will use #[proc_macro_derive]. This attribute acts as a direct instruction to the Rust compiler. It says: “Hey compiler, the function I’ve attached this attribute to is a derive macro. When you encounter a #[derive(...)] in user code, check if the name inside the parentheses matches the name I’m about to give you. If it does, call this function!”
The attribute takes an argument, which is the public-facing name of our macro. This is the name the user will type. By convention, this name should be in PascalCase, just like a trait. So, to register our macro to respond to #[derive(Builder)], we will annotate our function with #[proc_macro_derive(Builder)].
This elegantly separates the public API (Builder) from the internal implementation detail (our function’s name, derive_builder).
Let’s update your src/lib.rs file to include this crucial attribute. The only change is the addition of the new line right above the function definition.
// This line brings the TokenStream type into scope. TokenStream is the fundamental
// data structure that represents the input and output of a procedural macro.
// It's essentially a sequence of Rust code tokens.
use proc_macro::TokenStream;
// The quote macro is the primary tool for generating Rust code. It allows you to
// write code templates that can be filled with variables, producing a new TokenStream.
use quote::quote;
// syn is the parsing library we use to interpret the input TokenStream. It can
// parse Rust code into a structured data format called an Abstract Syntax Tree (AST),
// making it easy to inspect and analyze the code our macro is attached to.
use syn;
// This attribute registers our function as a custom derive procedural macro.
// The argument `Builder` is the name of the derive macro that users will write.
// When a user writes `#[derive(Builder)]`, the Rust compiler will call this
// `derive_builder` function.
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
// The body is currently empty. We will implement the parsing and code generation
// logic in the upcoming steps.
unimplemented!()
}
With this single line, you have completed the entire scaffolding for our procedural macro. Let’s recap the full workflow that is now in place:
- A developer uses our crate and writes
#[derive(Builder)]on their struct. - During compilation, the compiler sees
#[derive(Builder)]. - It finds our crate because it’s a
proc-macrocrate that provides aBuilderderive. - It calls our
derive_builderfunction, passing the tokens for the user’s struct as theinputargument. - Our function (which is currently
unimplemented!()) will eventually process these tokens and return a newTokenStreamcontaining the generated builder code. - The compiler seamlessly injects this returned code into the location where the
derivewas called.
Our macro is now correctly defined and registered. The next exciting step is to bring it to life by starting to process the input. We will move inside the derive_builder function and use the syn crate to parse the raw TokenStream into a structured, usable format.
Further Reading
To solidify your understanding of how procedural macros are defined and registered, the official Rust documentation is the best resource.
- The Rust Book: Custom
deriveMacros: A chapter dedicated to the exact type of macro we are building. https://doc.rust-lang.org/book/ch19-06-macros.html#how-to-write-a-custom-derive-macro - The Rust Reference:
proc_macro_derive: The formal technical specification for the attribute. https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros
Leave the function body empty for now.
Mục tiêu:
You’ve just successfully attached the #[proc_macro_derive(Builder)] attribute to your function, which was the final piece of the puzzle to register our macro with the Rust compiler. This completes the “Scaffolding” step of our project. We now have a fully defined, albeit empty, procedural macro.
This final task is about confirming our current state and understanding why we are leaving the function body as it is for now.
The Role of unimplemented!()
Currently, the body of our derive_builder function contains a single line: unimplemented!(). This is a standard and very useful macro provided by Rust’s standard library. Let’s break down why it’s the perfect placeholder for us at this stage.
- Type System Satisfaction: Our function signature,
pub fn derive_builder(...) -> TokenStream, makes a promise to the compiler: it will always return a value of typeTokenStream. If we left the body completely empty ({}), the compiler would complain because an empty block implicitly returns(), not aTokenStream. Theunimplemented!()macro cleverly solves this. It has a special return type!(called the “never type”), which can be coerced into any other type. This means it satisfies the compiler’s requirement that our function returns aTokenStream, allowing our code to compile successfully even though it’s not finished. - Explicitly Marking Incomplete Code: Using
unimplemented!()is a clear signal to yourself and other developers that this piece of code is intentionally left unfinished. - Runtime Safety: If, by some chance, our macro were compiled and executed before we replaced the placeholder, calling the function would immediately cause the program to
panic!. The panic message would be clear:"not yet implemented", pointing directly to the line in our macro’s source code. This is much better than the code failing in a silent or confusing way.
Our Completed Scaffold
Let’s take a look at our src/lib.rs file in its entirety. This code represents a complete and correct skeleton for a derive macro. It has all the necessary components in place, ready for us to fill in the logic.
// This line brings the TokenStream type into scope. TokenStream is the fundamental
// data structure that represents the input and output of a procedural macro.
// It's essentially a sequence of Rust code tokens.
use proc_macro::TokenStream;
// The quote macro is the primary tool for generating Rust code. It allows you to
// write code templates that can be filled with variables, producing a new TokenStream.
use quote::quote;
// syn is the parsing library we use to interpret the input TokenStream. It can
// parse Rust code into a structured data format called an Abstract Syntax Tree (AST),
// making it easy to inspect and analyze the code our macro is attached to.
use syn;
// This attribute registers our function as a custom derive procedural macro.
// The argument `Builder` is the name of the derive macro that users will write.
// When a user writes `#[derive(Builder)]`, the Rust compiler will call this
// `derive_builder` function.
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
// This is a placeholder macro. It allows our code to compile while we work
// on the implementation. It will panic if the macro is ever actually used
// in this state, clearly indicating that the work is not yet finished.
unimplemented!()
}
What’s Next? Breathing Life into the Macro
With our scaffolding now firmly in place, we are ready to move on to the most exciting part: bringing our macro to life. The very next step is “Parsing the Input Struct.”
We will replace the unimplemented!() line with our first piece of real logic. Our goal will be to take the raw, unstructured TokenStream that the compiler gives us and use the powerful syn library to parse it. This will transform the stream of code tokens into a structured Rust data type called syn::DeriveInput. This data structure is our key to understanding the user’s code, allowing us to easily access the struct’s name, its fields, and their types, which is the essential information we need to generate our builder.
Further Reading
To learn more about the concepts we’ve finalized in this step, please refer to the official documentation.
- The
unimplemented!macro: The official documentation for this useful placeholder. https://doc.rust-lang.org/std/macro.unimplemented.html - The Never Type (
!): For those curious about the type theory behind whyunimplemented!()works. https://doc.rust-lang.org/std/primitive.never.html - An Overview of Procedural Macros in Rust 2018: A comprehensive blog post that gives a great high-level view of the different types of procedural macros and their structure. https://blog.rust-lang.org/2018/12/21/Procedural-Macros-in-Rust-2018.html