Avatar ·

Как посмотреть развёрнутый макрос, из-за которого возникает ошибка компиляции в Rust?

Я новичок в Rust и пытаюсь использовать макросы для генерации кода. Взял популярный крейт `serde` и написал такой код: ```rust use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct User { name: String, age: u32, email: String, } fn main() { let user = User { name: "Alice".to_string(), age: 30, email: "alice@example.com".to_string(), }; let json = serde_json::to_string(&user).unwrap(); println!("{}", json); } ``` Всё работает. Но когда я попробовал написать свой собственный макрос для автоматической реализации трейта: ```rust macro_rules! impl_display { ($type:ty, $format:expr) => { impl std::fmt::Display for $type { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, $format, self) } } }; } struct Point { x: i32, y: i32, } impl_display!(Point, "Point({}, {})"); fn main() { let p = Point { x: 10, y: 20 }; println!("{}", p); } ``` Компилятор выдаёт ошибку: ``` error[E0277]: `Point` doesn't implement `std::fmt::Display` --> src/main.rs:15:1 | 8 | impl_display!(Point, "Point({}, {})"); | ------------------------------------- in this macro invocation | = note: required by `std::fmt::Display::fmt` ``` Я не понимаю, что именно генерирует макрос и где ошибка. Я слышал, что в Rust есть способ увидеть развёрнутый код макроса, но не знаю, как это сделать. Как мне посмотреть, во что превращается мой макрос, чтобы понять причину ошибки? Есть ли для этого специальные инструменты или флаги компилятора? Буду благодарен за подробное объяснение. <<>>

Использование флага --expand в nightly-версии компилятора

Самый прямой способ увидеть развёрнутый код макроса — использовать флаг -Z macro-backtrace или cargo expand. Начнём с первого варианта, который доступен в nightly-версии Rust.

Установите nightly-канал (если ещё не сделали этого):

rustup toolchain install nightly

Затем запустите компиляцию с флагом -Z macro-backtrace:

rustc +nightly -Z macro-backtrace main.rs

Это покажет полный стек макросов, включая их развёрнутый код. Для вашего примера с impl_display! вывод будет содержать:

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> main.rs:15:1
   |
15 | impl_display!(Point, "Point({}, {})");
   | ------------------------------------- in this macro invocation
   |
   = note: required by `std::fmt::Display::fmt`
   = note: this error originates in the macro `impl_display` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

The expanded code is:

impl std::fmt::Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Point({}, {})", self)
    }
}

The problem is that `write!` macro expects a format string and arguments, but you passed `self` as a single argument. `self` is of type `&Point`, which doesn't implement `Display`. You need to access fields: `self.x` and `self.y`.

Использование крейта cargo-expand

Более удобный способ — установить утилиту cargo-expand, которая работает со стабильной версией Rust:

cargo install cargo-expand

После установки просто запустите:

cargo expand

Вывод покажет весь развёрнутый код вашего проекта. Для приведённого примера вы увидите:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2018::*;
#[macro_use]
extern crate std;

struct Point {
    x: i32,
    y: i32,
}

impl std::fmt::Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Point({}, {})", self)
    }
}

fn main() {
    let p = Point { x: 10, y: 20 };
    {
        ::std::io::_print(
            ::core::fmt::Arguments::new_v1(
                &["", "\n"],
                &[::core::fmt::ArgumentV1::new_display(&p)],
            ),
        );
    };
}

Теперь ошибка очевидна: в макросе impl_display! вы используете write!(f, $format, self), где $format — это строка "Point({}, {})", но передаёте только один аргумент self. Форматная строка ожидает два аргумента (для {} и {}), а вы передаёте один — структуру Point, которая не реализует Display.

Исправление макроса

Правильная версия макроса должна принимать имена полей:

macro_rules! impl_display {
    ($type:ty, $format:expr, $($field:ident),+) => {
        impl std::fmt::Display for $type {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, $format, $(self.$field),+)
            }
        }
    };
}

struct Point {
    x: i32,
    y: i32,
}

impl_display!(Point, "Point({}, {})", x, y);

fn main() {
    let p = Point { x: 10, y: 20 };
    println!("{}", p); // Выведет: Point(10, 20)
}

Теперь макрос работает корректно, и cargo expand покажет правильный развёрнутый код.

Дополнительные советы

  • Для декларативных макросов (macro_rules!) можно использовать флаг --pretty expanded в компиляторе: rustc -Z unstable-options --pretty expanded main.rs (требуется nightly).
  • Для процедурных макросов (например, #[derive(...)]) cargo expand — лучший выбор, так как он показывает результат после всех этапов.
  • В среде разработки (например, VS Code с расширением rust-analyzer) можно навести курсор на макрос и нажать Ctrl+Shift+I (или Cmd+Shift+I на macOS), чтобы увидеть развёрнутый код.

Эти инструменты помогут вам отлаживать макросы и понимать, что именно генерирует ваш код. Помните, что макросы — это мощный инструмент, но их отладка требует терпения и правильных инструментов!

<<>>

Альтернативный подход: использование trace_macros! и log_syntax!

Если вы не хотите устанавливать дополнительные инструменты или работать с nightly-версией, в Rust есть встроенные макросы для отладки декларативных макросов. Они работают на стабильной версии и могут быть полезны для быстрой проверки.

Макрос trace_macros!(true) включает трассировку всех макросов в текущей области видимости. Вывод будет в консоль при компиляции. Пример:

fn main() {
    trace_macros!(true);
    let p = Point { x: 10, y: 20 };
    println!("{}", p);
    trace_macros!(false);
}

При компиляции вы увидите:

note: trace_macro
  --> src/main.rs:20:5
   |
20 |     println!("{}", p);
   |     ^^^^^^^^^^^^^^^^^
   |
   = note: expanding `println! { "{}" , p }`
   = note: to `print! { "{}\n" , p }`
   = note: expanding `print! { "{}\n" , p }`
   = note: to `$crate::io::_print($crate::fmt::Arguments::new_v1(&["", "\n"], &[$crate::fmt::ArgumentV1::new_display(&p)]))`

Макрос log_syntax! выводит свои аргументы в консоль во время компиляции. Его можно вставить внутрь вашего макроса, чтобы увидеть, какие токены передаются:

macro_rules! impl_display {
    ($type:ty, $format:expr) => {
        log_syntax!("Type:", stringify!($type), "Format:", $format);
        impl std::fmt::Display for $type {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result