Rust macros

From bibbleWiki
Revision as of 02:33, 10 October 2024 by Iwiseman (talk | contribs) (Created page with "=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. <syntaxhighlight lang="rs"> 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 } }; } mai...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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)

Here is the code structure.

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