Let's play with Rust's Procedural Macro ... Reload

Powered by

> whoami

michele.damico@gmail.com

https://github.com/la10736

https://www.linkedin.com/in/damico/

@PhenoCoder

Perché?

  1. La Meta Programmazione è Strafiga?
  2. E' Molto Difficile?
  3. Possiamo Raccontarlo In Giro?
  1. La Meta Programmazione è Strafiga?
  2. E' Molto Difficile?
  3. Possiamo Raccontarlo In Giro?
  • Scrivere codice in Rust
  • Togliere la magia
  • Rust ha degli ottimi strumenti di Meta Programmazione

Come

Procedural macro

Custom derive

Function-like macro

Attribute macro

#[derive(Serialize)]
struct Message {
    uid: u64,
    data: String,
} 
my_macro!(first, second);
macro_rules!
++
#[my_attribute]
fn some_function(arg1: i32, arg2: String) {
    // Body
}

Attribute macro

User code

use my_proc_macro_crate::my_attribute;

#[my_attribute(some_args)]
fn user_function(arg_1: u32) -> String {
    // Body
}

Proc macro crate

#[proc_macro_attribute]
pub fn my_attribute( 
    args: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    // Code that's modify input code and return it
}
my_proc_macro_crate

AST

my_attribute

AST

Compiler

syn & quote

syn
quote
  • Strutture dell' AST
  • Trait Parse e funzioni di parse
  • Info posizione codice
  • visit, fold ... altre features opzionabili
struct MyData {
    idents: Vec<Ident>
}

impl Parse for MyData {
    fn parse(input: ParseStream) -> Result<Self> {
        let idents = Punctuated::<Ident, Token![,]>::
            parse_terminated(input)?;
        Ok(MyData {
            idents: idents.into_iter().collect(),
        })
    }
}

let data: MyData = parse(&tokens).unwrap();

Macro quote! con la quale scrivere codice Rust usando anche variabili locali e ripetizioni che verra' trasformato nei token.

let sign_args = fn_args(fixture).take(n);
let fixture_args = fn_args_idents(fixture);
let name = Ident::new(&format!("partial_{}", n), 
                      Span::call_site());

quote! {
    #[allow(unused_mut)]
    pub fn #name #generics (#(#sign_args),*) #output 
    #where_clause 
    {
        #inject
        Self::get(#(#fixture_args),*)
    }
}

How to use it...

Debug

> cargo install cargo-expand
> cargo expand

Good way

Raw way

println!("{}", tokens);
println!("{:#?}", tokens);

<- AST

Cosa ?

Un semplice clone di              .....

pytest

..... O meglio implementiamo il concetto di fixture

Fixture

def test_dump_empty_repository(out, repository):
    dump_repository(repository, out)

    content = open(out).read()
    assert content.strip() == "No Entries"


def test_dump_no_empty_repository(out, two_entries_repository):
    dump_repository(two_entries_repository, out)

    content = open(out).read()
    assert "Michele, d'Amico : 44" in content
    assert "John, Doe : 37" in content
    assert 2 == len(content.splitlines())
@pytest.fixture
def repository():
    return Repository()


@pytest.fixture
def out(tmpdir):
    return os.path.join(tmpdir, "dump")


@pytest.fixture
def two_entries_repository(repository):
    repository.insert(Person("Michele", "d'Amico", 44))
    repository.insert(Person("John", "Doe", 37))
    return repository

Desiderata

#[rstest]
fn dump_empty_repository<R: Repository>(out: &Path, repository: R) {
    dump_repository(&repository, out);

    let mut content = String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert_eq!(content.trim(), "No Entries");
}

#[rstest]
fn dump_no_empty_repository<R: Repository>(out: &Path, two_entries_repository: R) {
    dump_repository(&two_entries_repository, out);

    let mut content = String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert!(content.contains("Michele, d'Amico : 44"));
    assert!(content.contains("John, Doe : 37"));
    assert_eq!(2, content.lines().count());
}

Magari...

  • Generics
  • &Path
  • Run parallelo

Fattibile

#[rstest]
fn dump_empty_repository(out: TempPath, repository: Repository) {
    dump_repository(&repository, &out);

    let mut content= String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert_eq!(content.trim(), "No Entries");
}

#[rstest]
fn dump_no_empty_repository(out: TempPath, two_entries_repository: Repository) {
    dump_repository(&two_entries_repository, &out);

    let mut content = String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert!(content.contains("Michele, d'Amico : 44"));
    assert!(content.contains("John, Doe : 37"));
    assert_eq!(2, content.lines().count());
}

L'idea...

Sostituire ogni argomento con una variabile locale inizializzata chiamando la funzione della fixture

#[test]
fn dump_empty_repository() {
    let out = out();
    let repository = repository();

    dump_repository(&repository, &out);

    let mut content = String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert_eq!(content.trim(), "No Entries");
}

Hello proc_macro

Struttura

use hello_proc_macro::hello;

#[hello]
fn other(s: &str, v: i32) { 
    println!("other body <- '{}', {} ", s, v) 
}

fn main() { other("pippo", 42) }
main.rs
[workspace]
members = [
    "hello_proc_macro"
]

[dependencies]
hello_proc_macro = { path = "hello_proc_macro" }
Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = {version="*", features=["full"]}
quote = "*"
proc-macro2 = "*"
hello_proc_macro/Cargo.toml

Hello proc_macro (cont.)

#[proc_macro_attribute]
pub fn hello(
    _attrs: proc_macro::TokenStream, 
    body: proc_macro::TokenStream
) -> proc_macro::TokenStream{
    // Parse tokens
    let ast = syn::parse(body).unwrap();

    // Build impl
    let gen = impl_hello_world(ast);

    // Return the generated impl
    gen.into()
}

Core code:

fn impl_hello_world(
    item: syn::ItemFn
) -> proc_macro2::TokenStream {

    let vis = &item.vis;
    let attrs = &item.attrs;
    let sig = &item.sig;
    let block = &item.block;

    let args = item.sig.inputs.iter()
        .filter_map(maybe_ident);

    quote! {
        #(#attrs)*
        #vis #sig {
            #( 
                println!("{} = {}", stringify!(#args), #args);
            )*
            #block
        }
    }
}

Obbiettivo

use rstest::rstest;

fn fixture() -> &'static str {
    "42"
}

fn fix_string() -> String {
    "String".to_string()
}

#[rstest]
fn some_test(fixture: &str, fix_string: String) {
    assert_eq!(fixture, "42");
    assert_eq!(fix_string, "String".to_string());
}

Aggiungere un crate rstest al workspace con una macro procedurale rstest che

trasformi le fixture in variabili locali

Un po di link

Ora iniziamo!

> git clone https://github.com/la10736/rstest_playground

Let's Play with Rust's Procedural Macro

By Michele D'Amico

Let's Play with Rust's Procedural Macro

Giochiamo con le macro procedurali per scrivere in clone di pytest

  • 203