Макросы в Rust: как подружить их с модулями?
Вы когда-нибудь писали на Rust полезный макрос, а потом пытались использовать его в соседнем модуле — и получали загадочную ошибку компилятора? Если да, вы не одиноки. Вопрос "How do I use a macro across module files?" собрал тысячи просмотров на Stack Overflow, и это неспроста.
В Rust макросы — мощный инструмент метапрограммирования. Они позволяют генерировать код на лету, избегать дублирования и писать DSL (предметно-ориентированные языки). Но вот незадача: макросы ведут себя не так, как обычные функции. У них свои правила видимости, и новички часто спотыкаются об это.
В этом туториале я покажу вам три проверенных способа организовать макросы между модульными файлами. Поехали!
Почему макросы не работают "по умолчанию"?
Прежде чем нырять в решение, давайте разберёмся с причиной. В Rust функции и типы по умолчанию видны только в своём модуле (если не помечены pub). С макросами та же история, но есть нюанс: pub для макросов работает не так, как для функций.
Рассмотрим простой пример. Предположим, у нас есть проект с такой структурой:
src/
├── main.rs
├── helpers/
│ ├── mod.rs
│ └── macros.rs
В macros.rs мы объявили макрос:
// src/helpers/macros.rs
macro_rules! greet {
($name:expr) => {
println!("Привет, {}!", $name);
};
}
Теперь хотим использовать его в main.rs:
// src/main.rs
mod helpers;
fn main() {
greet!("Мир"); // Ошибка! Макрос не виден.
}
Компилятор выдаст что-то вроде: error: macro `greet` is not imported. Почему? Потому что макросы по умолчанию не экспортируются из модуля, даже если модуль объявлен как pub.
Способ 1: Атрибут #[macro_use] для импорта макросов
Самый простой способ — использовать атрибут #[macro_use] перед объявлением модуля. Это заставляет Rust импортировать все макросы из этого модуля в текущую область видимости.
Исправим наш пример:
// src/main.rs
#[macro_use]
mod helpers;
fn main() {
greet!("Мир"); // Теперь работает!
}
Но есть подвох: #[macro_use] импортирует все макросы из модуля. Если у вас много макросов, это может привести к конфликтам имён. Кроме того, такой подход считается устаревшим в современном Rust (начиная с edition 2018).
Где это всё ещё полезно? В небольших проектах или когда вы пишете прототип и хотите быстро подключить пару макросов без лишних телодвижений.
Важный нюанс: порядок объявления
Если вы используете #[macro_use], макросы становятся доступны сразу после объявления модуля. Это значит, что следующий код не сработает:
// src/main.rs
fn use_greet() {
greet!("Тест"); // Ошибка! Модуль ещё не объявлен.
}
#[macro_use]
mod helpers;
fn main() {
use_greet();
}
Порядок имеет значение. Держите это в голове.
Способ 2: Явный экспорт макросов с помощью #[macro_export]
Более современный и контролируемый способ — использовать атрибут #[macro_export] прямо в определении макроса. Он делает макрос доступным на уровне крейта (crate), а не только модуля.
Вот как это работает:
// src/helpers/macros.rs
#[macro_export]
macro_rules! greet {
($name:expr) => {
println!("Привет, {}!", $name);
};
}
Теперь в main.rs мы можем импортировать макрос явно с помощью use:
// src/main.rs
mod helpers;
use helpers::greet;
fn main() {
greet!("Мир"); // Работает!
}
Обратите внимание: мы используем use helpers::greet, а не use helpers::macros::greet. Почему? Потому что #[macro_export] помещает макрос в корень модуля, где он был объявлен. Если ваш макрос находится во вложенном файле, он всё равно будет доступен через родительский модуль.
Этот способ гораздо чище. Вы явно контролируете, какие макросы экспортируются, и импортируете их точечно. Никаких конфликтов имён.
А что с re-export?
Иногда нужно, чтобы макрос был доступен не только в текущем крейте, но и в других (если вы пишете библиотеку). Для этого используется комбинация #[macro_export] и реэкспорт через pub use:
// src/lib.rs
#[macro_export]
macro_rules! greet {
($name:expr) => {
println!("Привет, {}!", $name);
};
}
pub use greet; // Реэкспортируем для внешних пользователей
Теперь любой, кто подключит ваш крейт, сможет написать use my_crate::greet;.
Способ 3: Макросы в отдельном файле с модульной структурой
Когда проект разрастается, макросы лучше выносить в отдельные файлы. Давайте построим правильную архитектуру.
Предположим, у нас такая структура:
src/
├── main.rs
├── macros/
│ ├── mod.rs
│ ├── math.rs
│ └── io.rs
В macros/math.rs:
// src/macros/math.rs
#[macro_export]
macro_rules! add {
($a:expr, $b:expr) => {
$a + $b
};
}
#[macro_export]
macro_rules! multiply {
($a:expr, $b:expr) => {
$a * $b
};
}
В macros/io.rs:
// src/macros/io.rs
#[macro_export]
macro_rules! print_result {
($val:expr) => {
println!("Результат: {}", $val);
};
}
В macros/mod.rs:
// src/macros/mod.rs
pub mod math;
pub mod io;
В main.rs:
// src/main.rs
mod macros;
use macros::add;
use macros::print_result;
fn main() {
let sum = add!(5, 3);
print_result!(sum);
// Также можно использовать напрямую:
println!("