Rust macros
Macros
I did macros using YouTube and the course. The first example is the course, the second is YouTube.
Greatest Common Denominator Macro
Here is the simple example.
macro_rules! gcd {
($a: expr, $b: expr) => {
{
let mut a = $a;
let mut b = $b;
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
};
}
main() {
print!("gcd is {} ", gcd!(14,28));
}
Declarative Macros
This seems similar to C++ with improvements. Need to learn the syntax but this should be enough for me to get going. Here is a basic macro.
#[macro_export]
macro_rules! my_macro {
() => {
{ // Do Stuff }
};
}
Export allows the macro to be seen, macro_rules! is the syntax to say I am a macro then the name of the macro. We can now write an arrow function similar to typescript. This has curly braces around whatever we are returning.
This example appends a "# " to the text passed. The function arguments are $var and in this case the type is literal. These are known as fragment specifiers. And a list can be found [here]
#[macro_export]
macro_rules! header {
($var:literal) => {
{concat!("# ", $var)}
};
}
fn main() {
let fred = header!("fred");
println!("Hello, world! {}", fred);
}
We can test this using the testing framework offered by rust and running cargo test
#[cfg(test)]
mod tests {
#[test]
fn test_header() {
let val = header!("Hello");
assert_eq!(val, "# Hello");
}
}
A more complicated macro maybe one that takes a list of values. E.g. ulist!["a","b","c"]. First we define a parameter with multiple inputs, to do this start with $(), separator , and repetition operator +. A list of repetition operators may be found [here]
macro_rules! ulist {
($(),+) => {
};
}
To emphasise the separator I have changed it to a % to make it clear how it is used.
#[macro_export]
macro_rules! ulist {
($($var:literal)%+) => {
{ concat!( $("item:", $var, "\n",)+ ).trim_end() }
};
}
Basically the example concatenates the values together and using .trim_end() removes the last "\n". Left it in as it may remind me how to do this. And here is the test.
#[test]
fn test_ulist() {
let val = ulist!("foo"%"bar");
assert_eq!(val, "item:foo\nitem:bar");
}
Procedural Macros
The previous macros were like C++ macros which match against patterns. Procedural macros are like functions, they take code as input, operate on it and then output code. There are three types of these
- Custom #[derive] macros.
- Attribute-like macros.
- Function-like macros
We are going to look at the derived as this is what I have come across and need to know
This type of macro takes code as input and outputs code.
- Create a library crate called billprod_macro
- Create a procedural macro crate inside billprod_macro (billprod_macro_derive)
Macro Library
This just defines the trait we are going to build. In the lib.rs we declare this trait (interface)
pub trait BillProdMacro {
fn bill_prod_macro();
}
Macro Derive Library
This lives inside of the macro library. For the procedural macro we need to define the Cargo.toml as a proc-macro and add dependencies syn and quote.
[package]
name = "billprod_macro_derive"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
syn = "1.0.75"
quote = "1.0.9"
For the code, we have something called TokenStream which is capable of parsing rust code. We write a function to receive the code, do something, and a function to output the code. My understanding is these are the same for most macros. We annotate it with the name of the macro #[proc_macro_derive(BillProdMacro)]
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(BillProdMacro)]
pub fn billprod_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_billprod_macro(&ast)
}
The implementation takes the code (ast) prints out text with the type.
fn impl_billprod_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl BillProdMacro for #name {
fn bill_prod_macro() {
println!("Hello, world! My name is {}", stringify!(#name));
}
}
};
gen.into()
}
Using the Macro
We need both packages. As they are not published we must reference them on disk
...
[dependencies]
billprod_macro = { path = "../billprod_macro" }
billprod_macro_derive = { path = "../billprod_macro/billprod_macro_derive" }
So now we can use them in the code.
use billprod_macro::BillProdMacro;
use billprod_macro_derive::BillProdMacro;
#[derive(BillProdMacro)]
struct BillCake;
....
fn main() {
BillCake::bill_prod_macro();
}
This outputs
Hello, world! My name is BillCake