Макроси

05 декември 2024

Макроси

Макроси

Могат да се използват за:

1 2 3 4 5 6 7 8 9 10 11 12 13
// функции с произволен брой аргументи
println!("x={}, y={}", x, y);

// спестяване на писането на повтарящ се код
impl_trait_for_integers!(MyAdd, i8, i16, i32, i64, i128, isize);

// derive атрибути
#[derive(Clone, Debug, Default)]
struct MyStruct {}

// "декоратори" за функции
#[tracing::instrument]
pub fn my_function(my_arg: usize) { /* ... */ }

Декларативни макроси

1 2 3 4 5
macro_rules! name_of_macro {
    ($var1:expr, $var2:expr) => {
        some_code($var1, $var2)
    }
}
#![allow(unused_macros)]
fn main() {}
macro_rules! name_of_macro {
    ($var1:expr, $var2:expr) => {
        some_code($var1, $var2)
    }
}

Декларативни макроси

1 2 3 4 5
macro_rules! add {
    ($var1:expr, $var2:expr) => {
        $var1 + $var2
    }
}
#![allow(unused_macros)]
fn main() {}
macro_rules! add {
    ($var1:expr, $var2:expr) => {
        $var1 + $var2
    }
}

Примери

try!

1 2 3 4 5 6 7 8
macro_rules! try_ {
    ($expr:expr) => {
        match $expr {
            Ok(value) => value,
            Err(e) => return Err(e.into()),
        }
    }
}
#![allow(unused_macros)]
fn main () {}
macro_rules! try_ {
    ($expr:expr) => {
        match $expr {
            Ok(value) => value,
            Err(e) => return Err(e.into()),
        }
    }
}

Примери

add!

1 2 3 4 5 6 7 8 9 10
macro_rules! add {
    ($var1:expr, $var2:expr) => {
        $var1 + $var2
    }
}

fn main() {
    println!("{}", add!(1, 1));
    println!("{}", add!("foo".to_string(), "bar"));
}
2 foobar
macro_rules! add {
    ($var1:expr, $var2:expr) => {
        $var1 + $var2
    }
}

fn main() {
    println!("{}", add!(1, 1));
    println!("{}", add!("foo".to_string(), "bar"));
}

Примери

add!

1 2 3 4 5 6 7 8 9 10
macro_rules! add {
    (Чш, я събери ($var1:expr) и ($var2:expr)) => {
        $var1 + $var2
    }
}

fn main() {
    println!("{}", add!(Чш, я събери (1) и (1)));
    println!("{}", add!(Чш, я събери ("foo".to_string()) и ("bar")));
}
2 foobar
macro_rules! add {
    (Чш, я събери ($var1:expr) и ($var2:expr)) => {
        $var1 + $var2
    }
}

fn main() {
    println!("{}", add!(Чш, я събери (1) и (1)));
    println!("{}", add!(Чш, я събери ("foo".to_string()) и ("bar")));
}

Примери

add!

Защо има скоби? За да се знае къде свършва изразa.

1 2 3 4 5
macro_rules! add {
    (Чш, я събери $var1:expr и $var2:expr) => {
        $var1 + $var2
    }
}
error: `$var1:expr` is followed by `и`, which is not allowed for `expr` fragments --> src/bin/main_37ebf19d111716c51c4d834a542f32e8e0bbc423.rs:5:30 | 5 | (Чш, я събери $var1:expr и $var2:expr) => { | ^ not allowed after `expr` fragments | = note: allowed there are: `=>`, `,` or `;` error: could not compile `rust` (bin "main_37ebf19d111716c51c4d834a542f32e8e0bbc423") due to 1 previous error
#![allow(unused_macros)]
fn main () {}
macro_rules! add {
    (Чш, я събери $var1:expr и $var2:expr) => {
        $var1 + $var2
    }
}

Примери

add!

Непосредствено след expr са позволени само (=>), (,) и (;), ако expr не е в скоби

1 2 3 4 5 6 7 8 9 10
macro_rules! add {
    (Чш, я събери $var1:expr, $var2:expr) => {
        $var1 + $var2
    }
}

fn main() {
    println!("{}", add!(Чш, я събери 1, 1));
    println!("{}", add!(Чш, я събери "foo".to_string(), "bar"));
}
2 foobar
macro_rules! add {
    (Чш, я събери $var1:expr, $var2:expr) => {
        $var1 + $var2
    }
}

fn main() {
    println!("{}", add!(Чш, я събери 1, 1));
    println!("{}", add!(Чш, я събери "foo".to_string(), "bar"));
}

Примери

map!

1 2 3 4 5 6 7 8 9 10 11 12
macro_rules! map {
    // ???
}

fn main() {
    let m = map! {
        "a": 1,
        "b": 2
    };

    println!("{:?}", m);
}

Повторения

Повторения

Повторения

Повторения

Повторения

Повторения

1 2 3 4 5 6 7 8 9 10 11 12
macro_rules! map {
    {
        $( $key:expr : $value:expr ),*
    } => {
        // Забележете блока
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}

Повторения

1 2 3 4 5 6 7 8 9 10 11 12
macro_rules! map {
    {
        $( $key:expr : $value:expr ),*
    } => {
        // Забележете блока
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}

Повторения

1 2 3 4 5 6 7 8 9 10 11 12
macro_rules! map {
    {
        $( $key:expr : $value:expr ),*
    } => {
        // Забележете блока
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}

Повторения

Окей, нека да компилираме

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
macro_rules! map {
    {
        $( $key:expr : $value:expr ),*
    } => {
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}


fn main() {
    let m = map! {
        "a": 1,
        "b": 2
    };

    println!("{:?}", m);
}
error: `$key:expr` is followed by `:`, which is not allowed for `expr` fragments --> src/bin/main_085811e736fee0028063ec10a1b8ec346134f42a.rs:3:22 | 3 | $( $key:expr : $value:expr ),* | ^ not allowed after `expr` fragments | = note: allowed there are: `=>`, `,` or `;` error: could not compile `rust` (bin "main_085811e736fee0028063ec10a1b8ec346134f42a") due to 1 previous error
macro_rules! map {
    {
        $( $key:expr : $value:expr ),*
    } => {
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}


fn main() {
    let m = map! {
        "a": 1,
        "b": 2
    };

    println!("{:?}", m);
}

Повторения

Правилата са си правила. Ще използваме => вместо :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
macro_rules! map {
    {
        $( $key:expr => $value:expr ),*
    } => {
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}


fn main() {
    let m = map! {
        "a" => 1,
        "b" => 2
    };

    println!("{:?}", m);
}
{"b": 2, "a": 1}
macro_rules! map {
    {
        $( $key:expr => $value:expr ),*
    } => {
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}


fn main() {
    let m = map! {
        "a" => 1,
        "b" => 2
    };

    println!("{:?}", m);
}

Повторения

А ако искаме да поддържаме trailing comma 🤔

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
macro_rules! map {
    {
        $( $key: expr => $value: expr ),*
    } => {
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}

fn main() {
    let m = map! {
        "a" => 1,
        "b" => 2,   // <- тази запетайка
    };

    println!("{:?}", m);
}
error: unexpected end of macro invocation --> src/bin/main_8b44efda988c973dac4013461fc1752e839b7c67.rs:16:18 | 1 | macro_rules! map { | ---------------- when calling this macro ... 16 | "b" => 2, // <- тази запетайка | ^ missing tokens in macro arguments | note: while trying to match meta-variable `$key:expr` --> src/bin/main_8b44efda988c973dac4013461fc1752e839b7c67.rs:3:12 | 3 | $( $key: expr => $value: expr ),* | ^^^^^^^^^^ error: could not compile `rust` (bin "main_8b44efda988c973dac4013461fc1752e839b7c67") due to 1 previous error
macro_rules! map {
    {
        $( $key: expr => $value: expr ),*
    } => {
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}

fn main() {
    let m = map! {
        "a" => 1,
        "b" => 2,   // <- тази запетайка
    };

    println!("{:?}", m);
}

Повторения

Можем да добавим група, която приема 0 или 1 запетайки

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
macro_rules! map {
    {
        $( $key: expr => $value: expr ),* $(,)?
    } => {                             // ^^^^^ -- тази група
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}

fn main() {
    let m1 = map! {
        "a" => 1,
        "b" => 2
    };
    let m2 = map! {
        "a" => 1,
        "b" => 2,
    };
}
macro_rules! map {
    {
        $( $key: expr => $value: expr ),* $(,)?
    } => {                             // ^^^^^ -- тази група
        {
            let mut map = ::std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    }
}

fn main() {
    let m1 = map! {
        "a" => 1,
        "b" => 2
    };
    let m2 = map! {
        "a" => 1,
        "b" => 2,
    };
}

Разпъване

Макросите в Rust не се разпъват чрез просто заместване на текст, който след това се компилира.

C/C++ работят така и това води до неочаквани резултати

1 2 3 4 5 6 7
#define TIMES_FIVE(x) 5*x

int main() {
    int val = TIMES_FIVE(2 + 3);
        //  = 5 * 2 + 3;
        //  = 13;
}

Разпъване

Макросите в Rust се разпъват след като е построено абстрактно синтактично дърво.
Заместват се node-ове в синтактичното дърво.

1 2 3 4 5
macro_rules! five_times {
    ($x:expr) => (5 * $x);
}

println!("{}", five_times!(2 + 3));
25
fn main() {
macro_rules! five_times {
    ($x:expr) => (5 * $x);
}

println!("{}", five_times!(2 + 3));
}

Разпъване

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
┌─────────┐   ┌─────────┐
│ BinOp   │ ┌╴│ LitInt  │
│ op: Mul │ │ │ val: 5  │
│ lhs: ◌  │╶┘ └─────────┘
│ rhs: ◌  │╶┐ ┌─────────┐
└─────────┘ └╴│   $x    │
              │         │
              └─────────┘

┌─────────┐   ┌─────────┐
│ BinOp   │ ┌╴│ LitInt  │
│ op: Mul │ │ │ val: 5  │
│ lhs: ◌  │╶┘ └─────────┘
│ rhs: ◌  │╶┐ ┌─────────┐   ┌─────────┐
└─────────┘ └╴│ BinOp   │ ┌╴│ LitInt  │
              │ op: Add │ │ │ val: 2  │
              │ lhs: ◌  │╶┘ └─────────┘
              │ rhs: ◌  │╶┐ ┌─────────┐
              └─────────┘ └╴│ LitInt  │
                            │ val: 3  │
                            └─────────┘

Хигиена

Хигиена

В този пример отново заради хигиена двата state-а не се shadow-ват взаимно

1 2 3 4 5 6 7 8 9 10 11
macro_rules! log {
    ($msg:expr) => {{
        let state: i32 = get_log_state();
        if state > 0 {
            println!("log({}): {}", state, $msg);
        }
    }};
}

let state: &str = "reticulating splines";
log!(state);
log(1): reticulating splines
fn get_log_state() -> i32 { 1 }
macro_rules! log {
    ($msg:expr) => {{
        let state: i32 = get_log_state();
        if state > 0 {
            println!("log({}): {}", state, $msg);
        }
    }};
}

fn main() {
let state: &str = "reticulating splines";
log!(state);
}

Хигиена

В този пример отново заради хигиена двата state-а не се shadow-ват взаимно

1 2 3 4 5 6 7 8 9 10 11
macro_rules! log {
    ($msg:expr) => {{
        let state: i32 = get_log_state();
        if state > 0 {
            println!("log({}): {}", state, $msg);
        }
    }};
}

let state: &str = "reticulating splines";
log!(state);
log(1): reticulating splines
fn get_log_state() -> i32 { 1 }
macro_rules! log {
    ($msg:expr) => {{
        let state: i32 = get_log_state();
        if state > 0 {
            println!("log({}): {}", state, $msg);
        }
    }};
}

fn main() {
let state: &str = "reticulating splines";
log!(state);
}

Хигиена

Този пример не се компилира, защото двете a-та имат различнен "цвят"

1 2 3 4 5 6 7 8 9 10
macro_rules! using_a {
    ($e:expr) => {
        {
            let a = 42;
            $e
        }
    }
}

let four = using_a!(a / 10);
error[E0425]: cannot find value `a` in this scope --> src/bin/main_f39435f4cb6c956dce1c09e9930a82e63710566d.rs:11:21 | 11 | let four = using_a!(a / 10); | ^ not found in this scope For more information about this error, try `rustc --explain E0425`. error: could not compile `rust` (bin "main_f39435f4cb6c956dce1c09e9930a82e63710566d") due to 1 previous error
fn main() {
macro_rules! using_a {
    ($e:expr) => {
        {
            let a = 42;
            $e
        }
    }
}

let four = using_a!(a / 10);
}

Хигиена

Пример от https://danielkeep.github.io/tlborm/book/mbe-min-hygiene.html

Хигиена

Вместо това, трябва да подадем a като метапроменлива от тип ident

1 2 3 4 5 6 7 8 9 10 11
macro_rules! using_a {
    ($a:ident, $e:expr) => {
        {
            let $a = 42;
            $e
        }
    }
}

let four = using_a!(a, a / 10);
println!("four={}", four);
four=4
fn main() {
macro_rules! using_a {
    ($a:ident, $e:expr) => {
        {
            let $a = 42;
            $e
        }
    }
}

let four = using_a!(a, a / 10);
println!("four={}", four);
}

Хигиена

Пример от https://danielkeep.github.io/tlborm/book/mbe-min-hygiene.html

Хигиена

По същата причина не можем да декларираме променливи, които се виждат извън макроса

1 2 3 4 5 6 7 8
macro_rules! declare_x {
    () => {
        let x = 3;
    }
}

declare_x!();
println!("{}", x);
error[E0425]: cannot find value `x` in this scope --> src/bin/main_6e5c34cef4a01ff0cdb6c9e4451bd4d88ee8b4df.rs:9:16 | 9 | println!("{}", x); | ^ not found in this scope For more information about this error, try `rustc --explain E0425`. error: could not compile `rust` (bin "main_6e5c34cef4a01ff0cdb6c9e4451bd4d88ee8b4df") due to 1 previous error
macro_rules! declare_x {
    () => {
        let x = 3;
    }
}

fn main() {
declare_x!();
println!("{}", x);
}

Хигиена

Освен ако не подадем името на променливата на макроса

1 2 3 4 5 6 7 8
macro_rules! declare_var {
    ($var:ident) => {
        let $var = 3;
    }
}

declare_var!(x);
println!("{}", x);
3
macro_rules! declare_var {
    ($var:ident) => {
        let $var = 3;
    }
}

fn main() {
declare_var!(x);
println!("{}", x);
}

Хигиена

1 2 3 4 5 6 7 8
macro_rules! declare_foo {
    () => {
        fn foo() { println!("macros!") }
    }
}

declare_foo!();
foo();
macros!
macro_rules! declare_foo {
    () => {
        fn foo() { println!("macros!") }
    }
}

fn main() {
declare_foo!();
foo();
}

Синтаксис

Извикване на макроси

Синтаксис

Извикване на макроси

Синтаксис

Извикване на макроси

Синтаксис

Извикване на макроси

Синтаксис

Metavariables & Fragment specifiers

Всички възможни типове на метапроменливи може да намерите в The Rust Reference

Ограниченията кои токени могат да стоят след кой тип метапроменливи също се намира на същата страница

Синтаксис

Ръкави

Макросите могат да имат повече от един ръкав за matching разделени с ;

1 2 3 4 5
macro_rules! my_macro {
    ($e: expr) => (...);
    ($i: ident) => (...);
    (for $i: ident in $e: expr) => (...);
}

Рекурсия

Макросите могат да извикват други макроси и дори себе си както този прост HTML shorthand

Дъното на рекурсията е най-отгоре, защото опитите за match-ване започват от първия към последния ръкав

1 2 3 4 5 6 7 8 9 10 11 12
macro_rules! write_html {
    ($w: expr, ) => (());

    ($w: expr, $e: tt) => (writeln!($w, "{}", $e)?);

    ($w: expr, $tag: ident [ $( $inner: tt )* ] $( $rest: tt )*) => {{
        writeln!($w, "<{}>", stringify!($tag))?;
        write_html!($w, $($inner)*);
        writeln!($w, "</{}>", stringify!($tag))?;
        write_html!($w, $($rest)*);
    }};
}
#![allow(unused_macros)]
fn main() {}
macro_rules! write_html {
    ($w: expr, ) => (());

    ($w: expr, $e: tt) => (writeln!($w, "{}", $e)?);

    ($w: expr, $tag: ident [ $( $inner: tt )* ] $( $rest: tt )*) => {{
        writeln!($w, "<{}>", stringify!($tag))?;
        write_html!($w, $($inner)*);
        writeln!($w, "", stringify!($tag))?;
        write_html!($w, $($rest)*);
    }};
}

Рекурсия

1 2 3 4 5 6 7 8 9 10 11 12 13
use std::fmt::Write;

let mut out = String::new();

write_html! {
    &mut out,
    html[
        head[title["Macros guide"]]
        body[h1["Macros are the best!"]]
    ]
}

println!("{}", out);
<html> <head> <title> Macros guide </title> </head> <body> <h1> Macros are the best! </h1> </body> </html>
macro_rules! write_html {
($w: expr, ) => (());

($w: expr, $e: tt) => (writeln!($w, "{}", $e)?);

($w: expr, $tag: ident [ $( $inner: tt )* ] $( $rest: tt )*) => {{
writeln!($w, "<{}>", stringify!($tag))?;
write_html!($w, $($inner)*);
writeln!($w, "", stringify!($tag))?;
write_html!($w, $($rest)*);
}};
}
fn main() -> Result<(), ::std::fmt::Error> {
use std::fmt::Write;

let mut out = String::new();

write_html! {
    &mut out,
    html[
        head[title["Macros guide"]]
        body[h1["Macros are the best!"]]
    ]
}

println!("{}", out);
Ok(())
}

Рекурсия

Разгъване по стъпки

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
macro_rules! write_html {
    ($w: expr, ) => (());
    ($w: expr, $e: tt) => (writeln!($w, "{}", $e)?);
    ($w: expr, $tag: ident [ $( $inner: tt )* ] $( $rest: tt )*) => {{
        writeln!($w, "<{}>", stringify!($tag))?;
        write_html!($w, $($inner)*);
        writeln!($w, "</{}>", stringify!($tag))?;
        write_html!($w, $($rest)*);
    }};
 }

write_html!(&mut out, html[head[title["Macros guide"]] body[h1["Macros are the best!"]]])

// ($w: expr, $tag: ident [ $( $inner: tt )* ] $( $rest: tt )*)
{
    writeln!(out, "<{}>", stringify!(html))?;
    write_html!(out, head[title["Macros guide"]]  body[h1["Macros are the best!"]]);
    writeln!(out, "</{}>", stringify!(html))?;
}

// ($w: expr, $tag: ident [ $( $inner: tt )* ] $( $rest: tt )*)
{
    writeln!(out, "<{}>", stringify!(html))?;
    {
        writeln!(out, "<{}>", stringify!(head))?;
        write_html!(out, title["Macros guide"]);
        writeln!(out, "</{}>", stringify!(head))?;
        write_html!(out, body[h1["Macros are the best!"]]);
    }
    writeln!(out, "</{}>", stringify!(html))?;
}

// ...

Scoping

Mакросите имат специфични правила за видимост

Scoping

Mакросите имат специфични правила за видимост

Scoping

Mакросите имат специфични правила за видимост

Scoping

Имаме макроси дефинирани в macros и ще ги използваме в client

1 2 3
#[macro_use]
mod macros;
mod client; // ок
1 2 3
mod client; // компилационна грешка
#[macro_use]
mod macros;

Scoping

При работа на ниво crate

Scoping

При работа на ниво crate

Scoping

При работа на ниво crate

Scoping

Имаме макроси дефинирани в macros и ще ги използваме в client

1 2 3 4 5 6 7 8
// crate macros

mod some_module {
    #[macro_export]
    macro_rules! hello {
        () => (println!("Hello!"))
    }
}
1 2 3 4 5 6 7 8
// crate client

#[macro_use]
extern crate macros;

fn main() {
    hello!();
}

Scoping

Или по-лесният вариант - като нормален import

1 2 3 4 5 6 7 8
// crate macros

mod some_module {
    #[macro_export]
    macro_rules! hello {
        () => (println!("Hello!"))
    }
}
1 2 3 4 5 6 7 8
// crate client

// notice top-level use
use macros::hello;

fn main() {
    hello!();
}

Scoping

Внимавайте с видимостта на типовет, които използвате.
$crate обозначава крейта, в който е дефиниран макросът.

1 2 3 4 5 6 7 8 9 10 11 12 13
macro_rules! try_ {
    ($expr:expr) => {
        match $expr {
            $crate::result::Result::Ok(val) => val,
            $crate::result::Result::Err(err) => {
                return $crate::result::Result::Err($crate::convert::From::from(err));
            }
        }
    };
    ($expr:expr,) => {
        $crate::try!($expr)
    };
}
#[allow(unused_macros)]
macro_rules! try_ {
    ($expr:expr) => {
        match $expr {
            $crate::result::Result::Ok(val) => val,
            $crate::result::Result::Err(err) => {
                return $crate::result::Result::Err($crate::convert::From::from(err));
            }
        }
    };
    ($expr:expr,) => {
        $crate::try!($expr)
    };
}
fn main() {}

Debugging

Дебъгването на макроси е сложно, но има някои полезни команди

Debugging

Дебъгването на макроси е сложно, но има някои полезни команди

Debugging

Дебъгването на макроси е сложно, но има някои полезни команди

Debugging

Дебъгването на макроси е сложно, но има някои полезни команди

Debugging

Дебъгването на макроси е сложно, но има някои полезни команди

Или може да изполваме extension за cargo

1 2
cargo install cargo-expand
cargo expand

Debugging

Има и удобни, но нестабилни макроси, които се ползват през feature gate на nightly

Debugging

Има и удобни, но нестабилни макроси, които се ползват през feature gate на nightly

Debugging

Има и удобни, но нестабилни макроси, които се ползват през feature gate на nightly

Debugging

Има и удобни, но нестабилни макроси, които се ползват през feature gate на nightly

Материали

Процедурни макроси

Процедурни макроси

Процедурни макроси

Процедурни макроси

Процедурни макроси

Процедурни макроси

Видове

Процедурни макроси

За да създадем процедурен макрос ни трябва нов crate, в чиито манифест да се съдържа

1 2
[lib]
proc-macro = true

Процедурни макроси

1 2 3 4 5 6
use proc_macro::TokenStream;

#[proc_macro]
pub fn my_macro(tokens: TokenStream) -> TokenStream {
    todo!()
}

Процедурни макроси

syn and quote

Обикновенно не е удобно да се работи директно с TokenStream, затова се използват два пакета:

syn предоставя парсър, който превръща TokenStream в синтактично дърво AST (Abstract syntax tree)

quote предоставя начин да превърнем синтактичното дърво обратно в TokenStream, който да върнем на компилатора

Библиотеки с интересни процедурни макроси

Ресурси

Въпроси