Use Rust macros to generate code at compile time.
macro_rules!
macro, which allows
you to match against patterns of code and generate code based on the matching
pattern.fn
,
let
, and match
, are reserved words in the Rust language that have special
meanings.{
, }
, and ;
, are used to structure and delimit blocks of
code.TokenStream
type is a data type that represents a sequence of tokens. This
type is defined in the proc_macro
crate and is surfaced as a way for you to
write macros based on other code in the codebase.
When defining a procedural macro, the macro input is passed to the macro as a
TokenStream
, which can then be parsed and transformed as needed. The resulting
TokenStream
can then be expanded into the final code output by the macro.
syn
cratesyn
crate is available to help parse a token stream into an AST that macro
code can traverse and manipulate. When a procedural macro is invoked in a Rust
program, the macro function is called with a token stream as the input. Parsing
this input is the first step to virtually any macro.
Take as an example a proc macro that you invoke using my_macro!
as follows:
"hello, world"
) as a TokenStream
to the my_macro
proc macro.
parse_macro_input!
macro from the
syn
crate to parse the input TokenStream
into an abstract syntax tree (AST).
Specifically, this example parses it as an instance of LitStr
that represents
a string literal in Rust. The eprintln!
macro is then used to print the
LitStr
AST for debugging purposes.
eprintln!
macro shows the structure of the LitStr
AST that
was generated from the input tokens. It shows the string literal value
("hello, world"
) and other metadata about the token, such as its kind (Str
),
suffix (None
), and span.
quote
cratequote
crate. This crate is pivotal in the code
generation portion of the macro.
Once a proc macro has finished analyzing and transforming the AST, it can use
the quote
crate or a similar code generation library to convert the AST back
into a token stream. After that, it returns the TokenStream
, which the Rust
compiler uses to replace the original stream in the source code.
Take the below example of my_macro
:
quote!
macro to generate a new TokenStream
consisting
of a println!
macro call with the LitStr
AST as its argument.
Note that the quote!
macro generates a TokenStream
of type
proc_macro2::TokenStream
. To return this TokenStream
to the Rust compiler,
you need to use the .into()
method to convert it to proc_macro::TokenStream
.
The Rust compiler will then use this TokenStream
to replace the original proc
macro call in the source code.
custom!(...)
#[derive(CustomDerive)]
#[CustomAttribute]
#[proc_macro]
attribute. The function must take a TokenStream
as input and
return a new TokenStream
as output to replace the original code.
!
operator. They can be used in various places in a Rust program, such as in
expressions, statements, and function definitions.
#[proc_macro_attribute]
attribute. The function requires two token streams as
input and returns a single TokenStream
as output that replaces the original
item with an arbitrary number of new items.
#[derive]
attribute on a struct, enum, or
union. They are typically used to automatically implement traits for the input
types.
#[proc_macro_derive]
attribute. They’re limited to generating code for structs, enums, and unions.
They take a single token stream as input and return a single token stream as
output.
Unlike the other procedural macros, the returned token stream doesn’t replace
the original code. Rather, the returned token stream gets appended to the module
or block that the original item belongs to. This allows developers to extend the
functionality of the original item without modifying the original code.
describe()
method for a struct.
describe()
method will print a description of the struct’s fields to the
console.
#[proc_macro_derive]
attribute. The input TokenStream
is parsed using the
parse_macro_input!()
macro to extract the struct’s identifier and data.
match
keyword to perform pattern matching on the
data
value to extract the names of the fields in the struct.
The first match
has two arms: one for the syn::Data::Struct
variant, and one
for the “catch-all” _
arm that handles all other variants of syn::Data
.
The second match
has two arms as well: one for the syn::Fields::Named
variant, and one for the “catch-all” _
arm that handles all other variants of
syn::Fields
.
The #(#idents), *
syntax specifies that the idents
iterator will be
“expanded” to create a comma-separated list of the elements in the iterator.
describe()
method for a struct. The expanded
variable is defined using the quote!
macro and the impl
keyword to create an
implementation for the struct name stored in the #ident
variable.
This implementation defines the describe()
method that uses the println!
macro to print the name of the struct and its field names.
Finally, the expanded
variable is converted into a TokenStream
using the
into()
method.
#[derive(Describe)]
attribute is added to a struct, the Rust
compiler automatically generates an implementation of the describe()
method
that can be called to print the name of the struct and the names of its fields.
cargo expand
command from the cargo-expand
crate can be used to expand
Rust code that uses procedural macros. For example, the code for the MyStruct
struct generated using the the #[derive(Describe)]
attribute looks like this:
declare_id
macro shows how function-like macros are used in Anchor. This
macro takes in a string of characters representing a program’s ID as input and
converts it into a Pubkey
type that can be used in the Anchor program.
declare_id
macro is defined using the #[proc_macro]
attribute,
indicating that it’s a function-like proc macro.
#[derive(Accounts)]
is an example of just one of many derive macros that
are used in Anchor.
The #[derive(Accounts)]
macro generates code that implements the Accounts
trait for the given struct. This trait does a number of things, including
validating and deserializing the accounts passed into an instruction. This
allows the struct to be used as a list of accounts required by an instruction in
an Anchor program.
Any constraints specified on fields by the #[account(..)]
attribute are
applied during deserialization. The #[instruction(..)]
attribute can also be
added to specify the instruction’s arguments and make them accessible to the
macro.
proc_macro_derive
attribute, which allows it
to be used as a derive macro that can be applied to a struct. The line
#[proc_macro_derive(Accounts, attributes(account, instruction))]
indicates
that this is a derive macro that processes account
and instruction
helper
attributes.
#[program]
#[program]
attribute macro is an example of an attribute macro used in
Anchor to define the module containing instruction handlers for a Nexis Native Chain
program.
#[program]
attribute is applied to a module, and it is used
to specify that the module contains instruction handlers for a Nexis Native Chain program.
starter
branch of
this repository.
The starter code includes a simple Anchor program that allows you to initialize
and update a Config
account. This is similar to what we did with the
Program Configuration lesson.
The account in question is structured as follows:
programs/admin/src/lib.rs
file contains the program entrypoint with the
definitions of the program’s instructions. Currently, the program has
instructions to initialize this account and then one instruction per account
field for updating the field.
The programs/admin/src/admin_config
directory contains the program’s
instruction logic and state. Take a look through each of these files. You’ll
notice that instruction logic for each field is duplicated for each instruction.
The goal of this lab is to implement a procedural macro that will allow us to
replace all of the instruction logic functions and automatically generate
functions for each instruction.
cargo new custom-macro
. This will create a new
custom-macro
directory with its own Cargo.toml
. Update the new Cargo.toml
file to be the following:
proc-macro = true
line defines this crate as containing a procedural
macro. The dependencies are all crates we’ll be using to create our derive
macro.
Next, change src/main.rs
to src/lib.rs
.
Next, update the project root’s Cargo.toml
file’s members
field to include
"custom-macro"
:
cargo new custom-macro-test
at the project root. Then update the newly
created Cargo.toml
to add anchor-lang
and the custom-macro
crates as
dependencies:
Cargo.toml
to include the new
custom-macro-test
crate as before:
custom-macro-test/src/main.rs
with the following
code. We’ll use this later for testing:
custom-macro/src/lib.rs
file, let’s add our new macro’s
declaration. In this file, we’ll use the parse_macro_input!
macro to parse the
input TokenStream
and extract the ident
and data
fields from a
DeriveInput
struct. Then, we’ll use the eprintln!
macro to print the values
of ident
and data
. For now, we will use TokenStream::new()
to return an
empty TokenStream
.
cargo-expand
command by running cargo install cargo-expand
. You’ll also need
to install the nightly version of Rust by running rustup install nightly
.
Once you’ve done this, you can see the output of the code described above by
navigating to the custom-macro-test
directory and running cargo expand
.
This command expands macros in the crate. Since the main.rs
file uses the
newly created InstructionBuilder
macro, this will print the syntax tree for
the ident
and data
of the struct to the console. Once you have confirmed
that the input TokenStream
is parsing correctly, feel free to remove the
eprintln!
statements.
match
statements to get the named fields from the data
of
the struct. Then we’ll use the eprintln!
macro to print the values of the
fields.
cargo expand
in the terminal to see the output of this code.
Once you have confirmed that the fields are being extracted and printed
correctly, you can remove the eprintln!
statement.
quote!
macro and will include the field’s name and type, as well as a new function name
for the update instruction.
TokenStream
quote!
macro to generate an implementation for the
struct with the name specified by the ident
variable. The implementation
includes the update instructions that were generated for each field in the
struct. The generated code is then converted to a TokenStream
using the
into()
method and returned as the result of the macro.
cargo expand
command to see the expanded form of the macro. The output of this look like the
following:
Config
struct,
first add the custom-macro
crate as a dependency to the program in its
Cargo.toml
:
state.rs
file in the Anchor program and update it with
the following code:
admin_update.rs
file and delete the existing update
instructions. This should leave only the UpdateAdminAccount
context struct in
the file.
lib.rs
in the Anchor program to use the update instructions
generated by the InstructionBuilder
macro.
admin
directory and run anchor test
to verify that
the update instructions generated by the InstructionBuilder
macro are working
correctly.
solution
branch of
the repository.