ماکروها (Macros)

ما در طول این کتاب از ماکروهایی مانند println! استفاده کرده‌ایم، اما هنوز به طور کامل بررسی نکرده‌ایم که یک ماکرو چیست و چگونه کار می‌کند. اصطلاح ماکرو به مجموعه‌ای از قابلیت‌ها در Rust اشاره دارد: ماکروهای اعلانی (declarative) با macro_rules! و سه نوع ماکرو رویه‌ای (procedural):

  • ماکروهای سفارشی #[derive] که کدی را که با ویژگی derive برای ساختارها (structs) و شمارش‌ها (enums) اضافه می‌شود مشخص می‌کنند.
  • ماکروهای شبیه ویژگی (Attribute-like) که ویژگی‌های سفارشی تعریف می‌کنند که می‌توانند روی هر آیتمی استفاده شوند.
  • ماکروهای شبیه تابع (Function-like) که مانند فراخوانی تابع به نظر می‌رسند اما روی توکن‌هایی که به عنوان آرگومان مشخص شده‌اند عمل می‌کنند.

ما به نوبت درباره هر یک از این‌ها صحبت خواهیم کرد، اما ابتدا بیایید نگاهی بیندازیم که چرا اصلاً به ماکروها نیاز داریم وقتی قبلاً توابع را داریم.

تفاوت بین ماکروها و توابع

در اصل، ماکروها روشی برای نوشتن کدی هستند که کد دیگری را می‌نویسد، که به عنوان فرابرنامه‌نویسی (metaprogramming) شناخته می‌شود. در پیوست C، ما ویژگی derive را بررسی می‌کنیم که پیاده‌سازی ویژگی‌های مختلف را برای شما تولید می‌کند. همچنین ما از ماکروهای println! و vec! در طول کتاب استفاده کرده‌ایم. همه این ماکروها توسعه پیدا می‌کنند تا کدی بیشتر از کدی که به صورت دستی نوشته‌اید تولید کنند.

فرابرنامه‌نویسی برای کاهش مقدار کدی که باید بنویسید و نگهداری کنید مفید است، که یکی از نقش‌های توابع نیز هست. با این حال، ماکروها توانایی‌های اضافی دارند که توابع ندارند.

یک امضای تابع باید تعداد و نوع پارامترهایی که تابع دارد را مشخص کند. از سوی دیگر، ماکروها می‌توانند تعداد متغیری از پارامترها را بپذیرند: می‌توانیم println!("hello") را با یک آرگومان یا println!("hello {}", name) را با دو آرگومان فراخوانی کنیم. همچنین، ماکروها قبل از اینکه کامپایلر معنی کد را تفسیر کند گسترش می‌یابند، بنابراین یک ماکرو می‌تواند، به عنوان مثال، یک ویژگی را روی یک نوع مشخص پیاده‌سازی کند. اما یک تابع نمی‌تواند، زیرا در زمان اجرا فراخوانی می‌شود و یک ویژگی باید در زمان کامپایل پیاده‌سازی شود.

عیب پیاده‌سازی یک ماکرو به جای یک تابع این است که تعریف ماکروها پیچیده‌تر از تعریف توابع است زیرا شما در حال نوشتن کدی در Rust هستید که کد دیگری را در Rust می‌نویسد. به دلیل این واسطه‌گری، تعریف ماکروها به طور کلی سخت‌تر از توابع خوانده می‌شود، فهمیده می‌شود و نگهداری می‌شود.

یکی دیگر از تفاوت‌های مهم بین ماکروها و توابع این است که شما باید ماکروها را قبل از فراخوانی آن‌ها در یک فایل تعریف کنید یا به دامنه بیاورید، برخلاف توابع که می‌توانید آن‌ها را در هر جایی تعریف کرده و در هر جایی فراخوانی کنید.

ماکروهای اعلانی با macro_rules! برای فرابرنامه‌نویسی عمومی

پرکاربردترین شکل ماکروها در Rust، ماکروهای اعلانی هستند. به این ماکروها گاهی اوقات “ماکروهای با مثال”، “ماکروهای macro_rules!” یا فقط “ماکروها” گفته می‌شود. در هسته خود، ماکروهای اعلانی به شما اجازه می‌دهند چیزی مشابه یک عبارت match در Rust بنویسید. همان‌طور که در فصل ۶ بحث شد، عبارات match ساختارهای کنترلی هستند که یک عبارت را می‌گیرند، مقدار حاصل از عبارت را با الگوها مقایسه می‌کنند و سپس کدی که با الگوی تطابق یافته مرتبط است را اجرا می‌کنند. ماکروها نیز مقدار را با الگوهایی که با کدی خاص مرتبط هستند مقایسه می‌کنند: در این حالت، مقدار کد منبع Rust است که به ماکرو ارسال شده است؛ الگوها با ساختار آن کد منبع مقایسه می‌شوند؛ و کدی که با هر الگو مرتبط است، وقتی تطابق یافت، جایگزین کدی می‌شود که به ماکرو ارسال شده است. همه این‌ها در طول کامپایل اتفاق می‌افتد.

برای تعریف یک ماکرو، از ساختار macro_rules! استفاده می‌کنید. بیایید بررسی کنیم چگونه از macro_rules! استفاده کنیم با نگاهی به نحوه تعریف ماکروی vec!. فصل ۸ پوشش داد که چگونه می‌توانیم از ماکروی vec! برای ایجاد یک بردار جدید با مقادیر خاص استفاده کنیم. به عنوان مثال، ماکروی زیر یک بردار جدید حاوی سه عدد صحیح ایجاد می‌کند:

#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

ما همچنین می‌توانیم از ماکروی vec! برای ساخت یک بردار شامل دو عدد صحیح یا یک بردار شامل پنج برش رشته استفاده کنیم. نمی‌توانیم از یک تابع برای انجام همین کار استفاده کنیم زیرا نمی‌دانیم تعداد یا نوع مقادیر از پیش چیست.

لیست ۲۰-۲۹ یک تعریف کمی ساده‌شده از ماکروی vec! را نشان می‌دهد.

Filename: src/lib.rs
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
Listing 20-29: یک نسخه ساده‌شده از تعریف ماکروی vec!

نکته: تعریف واقعی ماکروی vec! در کتابخانه استاندارد شامل کدی است که مقدار حافظه مناسب را از پیش تخصیص می‌دهد. آن کد بهینه‌سازی‌ای است که در اینجا برای ساده‌تر شدن مثال شامل نشده است.

حاشیه‌نویسی #[macro_export] نشان می‌دهد که این ماکرو باید هر زمان که crate‌ای که ماکرو در آن تعریف شده است به دامنه آورده شود، در دسترس قرار گیرد. بدون این حاشیه‌نویسی، ماکرو نمی‌تواند به دامنه آورده شود.

سپس تعریف ماکرو را با macro_rules! و نام ماکرویی که تعریف می‌کنیم بدون علامت تعجب شروع می‌کنیم. نام، که در اینجا vec است، با آکولادهایی دنبال می‌شود که بدنه تعریف ماکرو را مشخص می‌کنند.

ساختار بدنه vec! مشابه ساختار یک عبارت match است. در اینجا یک بازو با الگوی ( $( $x:expr ),* ) داریم، که با => و بلوک کدی که با این الگو مرتبط است دنبال می‌شود. اگر الگو تطابق یابد، بلوک کد مرتبط گسترش می‌یابد. با توجه به اینکه این تنها الگو در این ماکرو است، تنها یک روش معتبر برای تطابق وجود دارد؛ هر الگوی دیگری باعث خطا خواهد شد. ماکروهای پیچیده‌تر ممکن است بیش از یک بازو داشته باشند.

سینتکس الگوی معتبر در تعریف ماکروها با سینتکسی که در فصل ۱۹ برای الگوها پوشش داده شد متفاوت است زیرا الگوهای ماکروها بر اساس ساختار کد Rust و نه مقادیر تطابق داده می‌شوند. بیایید مرور کنیم که قسمت‌های الگوی لیست ۲۰-۲۹ چه معنایی دارند؛ برای مشاهده کامل سینتکس الگوهای ماکرو، به مستندات مرجع Rust مراجعه کنید.

ابتدا، مجموعه‌ای از پرانتزها را برای شامل کردن کل الگو استفاده می‌کنیم. از علامت دلار ($) برای اعلام یک متغیر در سیستم ماکرو استفاده می‌کنیم که کد Rust تطابق‌یافته با الگو را در خود جای می‌دهد. علامت دلار مشخص می‌کند که این یک متغیر ماکرو است، نه یک متغیر معمولی Rust. سپس مجموعه‌ای از پرانتزها می‌آیند که مقادیری را که با الگو درون پرانتزها تطابق دارند، برای استفاده در کد جایگزین ثبت می‌کنند. درون $()، $x:expr قرار دارد که با هر عبارت Rust تطابق دارد و به آن عبارت نام $x می‌دهد.

کامای بعد از $() نشان می‌دهد که یک کاراکتر کامای جداکننده باید بین هر نمونه از کدی که با کد درون $() تطابق دارد ظاهر شود. علامت * مشخص می‌کند که الگو با صفر یا بیشتر از هر چیزی که قبل از * است، تطابق دارد.

وقتی این ماکرو را با vec![1, 2, 3]; فراخوانی می‌کنیم، الگوی $x سه بار با سه عبارت 1، 2 و 3 تطابق پیدا می‌کند.

حالا بیایید به الگویی که در بدنه کد مرتبط با این بازو وجود دارد نگاه کنیم: temp_vec.push() درون $()* برای هر بخشی که با $() در الگو تطابق دارد، صفر یا بیشتر بار بسته به اینکه الگو چند بار تطابق پیدا می‌کند، تولید می‌شود. $x با هر عبارتی که تطابق پیدا کند جایگزین می‌شود. وقتی این ماکرو را با vec![1, 2, 3]; فراخوانی می‌کنیم، کدی که جایگزین این فراخوانی ماکرو می‌شود به شکل زیر خواهد بود:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

ما یک ماکرو تعریف کرده‌ایم که می‌تواند هر تعداد آرگومان از هر نوعی را بپذیرد و کدی برای ایجاد یک بردار که شامل عناصر مشخص‌شده است تولید کند.

برای یادگیری بیشتر در مورد نحوه نوشتن ماکروها، به مستندات آنلاین یا منابع دیگر مانند “The Little Book of Rust Macros” که توسط Daniel Keep آغاز و توسط Lukas Wirth ادامه داده شده است، مراجعه کنید.

ماکروهای رویه‌ای (Procedural) برای تولید کد از ویژگی‌ها (Attributes)

دومین شکل ماکروها، ماکروی رویه‌ای (procedural macro) است که بیشتر شبیه به یک تابع عمل می‌کند (و نوعی رویه است). ماکروهای رویه‌ای کدی را به عنوان ورودی می‌پذیرند، روی آن کد عمل می‌کنند و به جای تطابق با الگوها و جایگزین کردن کد با کدی دیگر مانند ماکروهای اعلانی، کدی را به عنوان خروجی تولید می‌کنند. سه نوع ماکروی رویه‌ای شامل derive سفارشی، شبیه ویژگی (attribute-like) و شبیه تابع (function-like) هستند و همه به شیوه‌ای مشابه عمل می‌کنند.

هنگام ایجاد ماکروهای رویه‌ای، تعاریف باید در یک crate مجزا با نوع crate خاص خود قرار گیرند. این به دلایل فنی پیچیده‌ای است که امیدواریم در آینده برطرف شود. در لیست ۲۰-۳۰، نحوه تعریف یک ماکروی رویه‌ای را نشان می‌دهیم که در آن some_attribute به عنوان جایگزین برای استفاده از نوع خاصی از ماکرو است.

Filename: src/lib.rs
use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listing 20-30: یک مثال از تعریف یک ماکروی رویه‌ای

تابعی که یک ماکروی رویه‌ای را تعریف می‌کند، یک TokenStream را به عنوان ورودی می‌گیرد و یک TokenStream را به عنوان خروجی تولید می‌کند. نوع TokenStream توسط crate به نام proc_macro تعریف شده است که با Rust همراه است و نمایانگر یک توالی از توکن‌ها است. این هسته ماکرو است: کد منبعی که ماکرو روی آن عمل می‌کند ورودی TokenStream را تشکیل می‌دهد و کدی که ماکرو تولید می‌کند خروجی TokenStream است. این تابع همچنین دارای یک ویژگی (attribute) متصل به خود است که مشخص می‌کند کدام نوع از ماکروی رویه‌ای را ایجاد می‌کنیم. ما می‌توانیم چندین نوع از ماکروهای رویه‌ای را در یک crate داشته باشیم.

بیایید به تایپ‌های مختلف ماکروهای رویه‌ای نگاهی بیندازیم. با یک ماکروی derive سفارشی شروع می‌کنیم و سپس تفاوت‌های کوچک بین اشکال دیگر را توضیح می‌دهیم.

نحوه نوشتن یک ماکروی derive سفارشی

بیایید یک crate به نام hello_macro ایجاد کنیم که یک ویژگی به نام HelloMacro را با یک تابع وابسته به نام hello_macro تعریف کند. به جای اینکه کاربران ما ویژگی HelloMacro را برای هر یک از انواع خود پیاده‌سازی کنند، ما یک ماکروی رویه‌ای فراهم می‌کنیم تا کاربران بتوانند نوع خود را با #[derive(HelloMacro)] حاشیه‌نویسی کنند و یک پیاده‌سازی پیش‌فرض برای تابع hello_macro دریافت کنند. پیاده‌سازی پیش‌فرض متن Hello, Macro! My name is TypeName! را چاپ می‌کند که در آن TypeName نام نوعی است که این ویژگی روی آن تعریف شده است. به عبارت دیگر، ما crateای خواهیم نوشت که به برنامه‌نویس دیگری امکان می‌دهد کدی مانند لیست ۲۰-۳۱ را با استفاده از crate ما بنویسد.

Filename: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}
Listing 20-31: کدی که کاربر crate ما می‌تواند هنگام استفاده از ماکروی رویه‌ای ما بنویسد

این کد متن Hello, Macro! My name is Pancakes! را چاپ می‌کند وقتی کار ما تمام شود. اولین قدم این است که یک crate جدید از نوع کتابخانه بسازیم، به این صورت:

$ cargo new hello_macro --lib

سپس، ویژگی HelloMacro و تابع وابسته به آن را تعریف می‌کنیم:

Filename: src/lib.rs
pub trait HelloMacro {
    fn hello_macro();
}

ما اکنون یک ویژگی و تابع وابسته به آن داریم. در این مرحله، کاربر crate ما می‌تواند ویژگی را پیاده‌سازی کند تا به عملکرد مورد نظر برسد، به این صورت:

use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}

با این حال، آن‌ها باید بلوک پیاده‌سازی را برای هر نوعی که می‌خواهند با hello_macro استفاده کنند بنویسند؛ ما می‌خواهیم آن‌ها را از انجام این کار بی‌نیاز کنیم.

علاوه بر این، ما هنوز نمی‌توانیم برای تابع hello_macro یک پیاده‌سازی پیش‌فرض ارائه دهیم که نام نوعی که ویژگی روی آن پیاده‌سازی شده است را چاپ کند: Rust قابلیت‌های بازتاب (reflection) ندارد، بنابراین نمی‌تواند نام نوع را در زمان اجرا جستجو کند. ما به یک ماکرو نیاز داریم تا کد را در زمان کامپایل تولید کند.

مرحله بعدی این است که ماکروی رویه‌ای را تعریف کنیم. در زمان نگارش این متن، ماکروهای رویه‌ای باید در یک crate جداگانه قرار گیرند. این محدودیت ممکن است در آینده برداشته شود. روش استاندارد برای ساختاردهی جعبه‌ها (crates) و جعبه‌ها (crates)ی ماکرو به این صورت است: برای یک crate به نام foo، یک ماکروی رویه‌ای سفارشی derive به نام foo_derive نام‌گذاری می‌شود. بیایید یک crate جدید به نام hello_macro_derive در پروژه hello_macro ایجاد کنیم:

$ cargo new hello_macro_derive --lib

دو crate ما به شدت به هم مرتبط هستند، بنابراین ما crate ماکروی رویه‌ای را درون دایرکتوری crate hello_macro ایجاد می‌کنیم. اگر تعریف ویژگی را در hello_macro تغییر دهیم، باید پیاده‌سازی ماکروی رویه‌ای در hello_macro_derive را نیز تغییر دهیم. این دو crate باید به طور جداگانه منتشر شوند و برنامه‌نویسانی که از این جعبه‌ها (crates) استفاده می‌کنند باید هر دو را به عنوان وابستگی اضافه کرده و آن‌ها را به دامنه بیاورند. در عوض، می‌توانستیم crate hello_macro از hello_macro_derive به عنوان یک وابستگی استفاده کند و کد ماکروی رویه‌ای را دوباره صادر کند. با این حال، روشی که پروژه را ساختاربندی کرده‌ایم، این امکان را فراهم می‌کند که برنامه‌نویسان از hello_macro حتی اگر عملکرد derive را نخواهند، استفاده کنند.

ما باید crate hello_macro_derive را به عنوان یک crate ماکروی رویه‌ای اعلام کنیم. همچنین به عملکردهایی از جعبه‌ها (crates)ی syn و quote نیاز خواهیم داشت، همان‌طور که به زودی خواهید دید، بنابراین باید آن‌ها را به عنوان وابستگی اضافه کنیم. موارد زیر را به فایل Cargo.toml برای hello_macro_derive اضافه کنید:

Filename: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"

برای شروع تعریف ماکروی رویه‌ای، کد لیست ۲۰-۳۲ را در فایل src/lib.rs برای crate hello_macro_derive قرار دهید. توجه داشته باشید که این کد تا زمانی که تعریف تابع impl_hello_macro را اضافه نکنیم کامپایل نخواهد شد.

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_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_hello_macro(&ast)
}
Listing 20-32: کدی که اکثر جعبه‌ها (crates)ی ماکروی رویه‌ای برای پردازش کد Rust نیاز دارند

توجه کنید که کد را به دو تابع تقسیم کرده‌ایم: hello_macro_derive که مسئول پردازش TokenStream است، و impl_hello_macro که مسئول تبدیل درخت نحوی است. این کار نوشتن یک ماکروی رویه‌ای را آسان‌تر می‌کند. کد تابع بیرونی (hello_macro_derive در اینجا) تقریباً برای تمام جعبه‌ها (crates)ی ماکروی رویه‌ای که می‌بینید یا ایجاد می‌کنید یکسان خواهد بود. کدی که در بدنه تابع داخلی (impl_hello_macro در اینجا) مشخص می‌کنید بسته به هدف ماکروی رویه‌ای شما متفاوت خواهد بود.

ما سه crate جدید معرفی کرده‌ایم: proc_macro، syn، و quote. crate proc_macro همراه با Rust ارائه می‌شود، بنابراین نیازی به افزودن آن به وابستگی‌ها در Cargo.toml نداریم. crate proc_macro API کامپایلر است که به ما اجازه می‌دهد کد Rust را از کد خود بخوانیم و دستکاری کنیم.

crate syn کد Rust را از یک رشته به یک ساختار داده‌ای تبدیل می‌کند که می‌توانیم عملیات روی آن انجام دهیم. crate quote ساختارهای داده syn را دوباره به کد Rust تبدیل می‌کند. این جعبه‌ها (crates) پردازش هر نوع کد Rust که بخواهیم مدیریت کنیم را بسیار ساده‌تر می‌کنند: نوشتن یک تجزیه‌کننده کامل برای کد Rust کار ساده‌ای نیست.

تابع hello_macro_derive زمانی فراخوانی می‌شود که یک کاربر از کتابخانه ما ویژگی #[derive(HelloMacro)] را روی یک نوع مشخص کند. این امر به این دلیل ممکن است که ما تابع hello_macro_derive را با proc_macro_derive حاشیه‌نویسی کرده‌ایم و نام HelloMacro را مشخص کرده‌ایم، که با نام ویژگی ما مطابقت دارد؛ این روش معمولی‌ای است که بیشتر ماکروهای رویه‌ای دنبال می‌کنند.

تابع hello_macro_derive ابتدا input را از یک TokenStream به یک ساختار داده تبدیل می‌کند که سپس می‌توانیم آن را تفسیر کرده و عملیات‌هایی روی آن انجام دهیم. اینجاست که crate syn به کار می‌آید. تابع parse در syn یک TokenStream می‌گیرد و یک ساختار DeriveInput را که نمایانگر کد Rust تجزیه‌شده است، بازمی‌گرداند. لیست ۲۰-۳۳ بخش‌های مرتبط از ساختار DeriveInput را نشان می‌دهد که هنگام تجزیه کد struct Pancakes; دریافت می‌کنیم:

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}
Listing 20-33: نمونه‌ای از DeriveInput که هنگام تجزیه کدی که ویژگی ماکرو در لیست ۲۰-۳۱ را دارد، دریافت می‌کنیم

فیلدهای این ساختار نشان می‌دهند که کد Rust که تجزیه کرده‌ایم یک ساختار واحد (unit struct) با شناسه (ident) به نام Pancakes است. این ساختار فیلدهای بیشتری برای توصیف انواع مختلف کد Rust دارد؛ برای اطلاعات بیشتر به مستندات syn برای DeriveInput مراجعه کنید.

به زودی تابع impl_hello_macro را تعریف خواهیم کرد، جایی که کد جدیدی که می‌خواهیم اضافه کنیم را تولید خواهیم کرد. اما قبل از این کار، توجه داشته باشید که خروجی ماکروی derive ما نیز یک TokenStream است. TokenStream بازگردانده شده به کدی که کاربران crate ما می‌نویسند اضافه می‌شود، بنابراین وقتی crate آن‌ها کامپایل می‌شود، قابلیت‌های اضافی‌ای که ما در TokenStream تغییر داده‌شده فراهم کرده‌ایم را دریافت خواهند کرد.

ممکن است متوجه شده باشید که ما از unwrap استفاده می‌کنیم تا در صورتی که فراخوانی تابع syn::parse شکست بخورد، تابع hello_macro_derive به وحشت بیفتد (panic). لازم است ماکروی رویه‌ای ما در صورت بروز خطا به وحشت بیفتد، زیرا توابع proc_macro_derive باید به جای Result یک TokenStream بازگردانند تا با API ماکروهای رویه‌ای سازگار باشند. برای ساده کردن این مثال از unwrap استفاده کرده‌ایم؛ در کد تولیدی، بهتر است پیام‌های خطای خاص‌تری درباره مشکل ایجاد شده با استفاده از panic! یا expect ارائه دهید.

اکنون که کدی داریم که کد Rust حاشیه‌نویسی‌شده را از یک TokenStream به یک نمونه DeriveInput تبدیل می‌کند، بیایید کدی که ویژگی HelloMacro را روی نوع حاشیه‌نویسی‌شده پیاده‌سازی می‌کند، تولید کنیم، همان‌طور که در لیست ۲۰-۳۴ نشان داده شده است.

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_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_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    gen.into()
}
Listing 20-34: پیاده‌سازی ویژگی HelloMacro با استفاده از کد Rust تجزیه‌شده

ما با استفاده از ast.ident یک نمونه از ساختار Ident که شامل نام (شناسه) نوع حاشیه‌نویسی‌شده است، دریافت می‌کنیم. ساختار موجود در لیست ۲۰-۳۳ نشان می‌دهد که وقتی تابع impl_hello_macro را روی کد لیست ۲۰-۳۱ اجرا می‌کنیم، فیلد ident با مقدار "Pancakes" پر خواهد شد. بنابراین، متغیر name در لیست ۲۰-۳۴ یک نمونه از ساختار Ident را شامل می‌شود که وقتی چاپ می‌شود، رشته "Pancakes"، یعنی نام ساختار در لیست ۲۰-۳۱، خواهد بود.

ماکروی quote! به ما اجازه می‌دهد کد Rust مورد نظر خود برای بازگرداندن را تعریف کنیم. کامپایلر به چیزی متفاوت از نتیجه مستقیم اجرای ماکروی quote! نیاز دارد، بنابراین باید آن را به یک TokenStream تبدیل کنیم. این کار را با فراخوانی متد into انجام می‌دهیم که این نمایش میانی را مصرف کرده و مقداری از نوع TokenStream مورد نیاز بازمی‌گرداند.

ماکروی quote! همچنین برخی قابلیت‌های جالب الگوگذاری (templating) ارائه می‌دهد: می‌توانیم #name را وارد کنیم و quote! آن را با مقدار موجود در متغیر name جایگزین می‌کند. حتی می‌توانید تکرارهایی مشابه با نحوه کار ماکروهای معمولی انجام دهید. برای مقدمه‌ای جامع به مستندات crate quote مراجعه کنید.

ما می‌خواهیم ماکروی رویه‌ای ما یک پیاده‌سازی از ویژگی HelloMacro برای نوعی که کاربر حاشیه‌نویسی کرده است تولید کند، که می‌توانیم با استفاده از #name به آن دسترسی پیدا کنیم. پیاده‌سازی ویژگی شامل یک تابع به نام hello_macro است که بدنه آن قابلیت مورد نظر ما، یعنی چاپ Hello, Macro! My name is و سپس نام نوع حاشیه‌نویسی‌شده، را ارائه می‌دهد.

ماکروی stringify! که در اینجا استفاده شده است، به صورت داخلی در Rust ساخته شده است. این ماکرو یک عبارت Rust، مانند 1 + 2، را گرفته و در زمان کامپایل آن را به یک رشته ثابت، مانند "1 + 2"، تبدیل می‌کند. این با ماکروهایی مانند format! یا println! که عبارت را ارزیابی کرده و سپس نتیجه را به یک String تبدیل می‌کنند، متفاوت است. احتمال دارد ورودی #name یک عبارتی برای چاپ باشد، بنابراین از stringify! استفاده می‌کنیم. استفاده از stringify! همچنین با تبدیل #name به یک رشته ثابت در زمان کامپایل، یک تخصیص را صرفه‌جویی می‌کند.

در این مرحله، دستور cargo build باید با موفقیت در هر دو crate hello_macro و hello_macro_derive اجرا شود. بیایید این جعبه‌ها (crates) را به کد موجود در لیست ۲۰-۳۱ متصل کنیم تا ماکروی رویه‌ای را در عمل ببینیم! یک پروژه باینری جدید در دایرکتوری projects خود با استفاده از دستور cargo new pancakes ایجاد کنید. باید hello_macro و hello_macro_derive را به عنوان وابستگی در فایل Cargo.toml crate pancakes اضافه کنیم. اگر نسخه‌های خود از hello_macro و hello_macro_derive را در crates.io منتشر می‌کنید، آن‌ها به عنوان وابستگی‌های معمولی خواهند بود؛ در غیر این صورت، می‌توانید آن‌ها را به صورت وابستگی‌های path به شکل زیر مشخص کنید:

hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

کد موجود در لیست ۲۰-۳۱ را در فایل src/main.rs قرار دهید و دستور cargo run را اجرا کنید: باید عبارت Hello, Macro! My name is Pancakes! را چاپ کند. پیاده‌سازی ویژگی HelloMacro که از ماکروی رویه‌ای آمده بود، بدون نیاز به پیاده‌سازی آن توسط crate pancakes اضافه شد؛ ویژگی #[derive(HelloMacro)] پیاده‌سازی ویژگی را اضافه کرد.

در ادامه، بیایید بررسی کنیم که انواع دیگر ماکروهای رویه‌ای چه تفاوتی با ماکروهای سفارشی derive دارند.

ماکروهای شبیه ویژگی (Attribute-like macros)

ماکروهای شبیه ویژگی مشابه ماکروهای سفارشی derive هستند، اما به جای تولید کد برای ویژگی derive، به شما امکان می‌دهند ویژگی‌های جدید ایجاد کنید. آن‌ها همچنین انعطاف‌پذیرتر هستند: derive فقط برای ساختارها (structs) و شمارش‌ها (enums) کار می‌کند؛ اما ویژگی‌ها می‌توانند به آیتم‌های دیگر نیز اعمال شوند، مانند توابع. در اینجا یک مثال از استفاده از یک ماکروی شبیه ویژگی آورده شده است: فرض کنید یک ویژگی به نام route دارید که توابع را هنگام استفاده از یک فریم‌ورک برنامه وب حاشیه‌نویسی می‌کند:

#[route(GET, "/")]
fn index() {

این ویژگی #[route] توسط فریم‌ورک به عنوان یک ماکروی رویه‌ای تعریف می‌شود. امضای تابع تعریف ماکرو به این صورت خواهد بود:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

در اینجا، دو پارامتر از نوع TokenStream داریم. پارامتر اول برای محتوای ویژگی است: بخش GET, "/". پارامتر دوم برای بدنه آیتمی است که ویژگی به آن متصل شده است: در این مورد، fn index() {} و باقی بدنه تابع.

علاوه بر این، ماکروهای شبیه ویژگی به همان شیوه ماکروهای سفارشی derive کار می‌کنند: شما یک crate با نوع proc-macro ایجاد می‌کنید و تابعی را پیاده‌سازی می‌کنید که کدی را که می‌خواهید تولید می‌کند!

ماکروهای شبیه تابع

ماکروهای شبیه تابع، ماکروهایی را تعریف می‌کنند که شبیه به فراخوانی توابع به نظر می‌رسند. مشابه با ماکروهای macro_rules!، این ماکروها انعطاف‌پذیرتر از توابع هستند؛ برای مثال، می‌توانند تعداد نامشخصی از آرگومان‌ها را بپذیرند. با این حال، ماکروهای macro_rules! فقط می‌توانند با استفاده از سینتکس شبیه به match که در بخش “ماکروهای اعلانی با macro_rules! برای فرابرنامه‌نویسی عمومی” بحث شد تعریف شوند. ماکروهای شبیه تابع یک پارامتر TokenStream می‌گیرند و تعریف آن‌ها این TokenStream را با استفاده از کد Rust مانند دو نوع دیگر ماکروهای رویه‌ای دستکاری می‌کند. مثالی از یک ماکروی شبیه تابع، ماکروی sql! است که ممکن است به این صورت فراخوانی شود:

let sql = sql!(SELECT * FROM posts WHERE id=1);

این ماکرو عبارت SQL داخل خود را تجزیه کرده و بررسی می‌کند که از نظر نحوی درست باشد، که پردازش بسیار پیچیده‌تری نسبت به آنچه یک ماکروی macro_rules! می‌تواند انجام دهد، دارد. ماکروی sql! به این صورت تعریف می‌شود:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

این تعریف مشابه امضای ماکروی سفارشی derive است: ما توکن‌هایی که داخل پرانتزها قرار دارند را دریافت می‌کنیم و کدی را که می‌خواهیم تولید کنیم بازمی‌گردانیم.

خلاصه

وای! اکنون شما برخی از ویژگی‌های Rust را در ابزار خود دارید که احتمالاً به ندرت از آن‌ها استفاده می‌کنید، اما می‌دانید که در شرایط خاصی در دسترس هستند. ما موضوعات پیچیده متعددی را معرفی کردیم تا زمانی که با پیشنهادات پیام‌های خطا یا کدهای دیگران مواجه شدید، بتوانید این مفاهیم و سینتکس را بشناسید. از این فصل به عنوان مرجعی برای یافتن راه‌حل‌ها استفاده کنید.

در ادامه، همه چیزهایی که در طول کتاب بحث کردیم را در عمل پیاده‌سازی می‌کنیم و یک پروژه دیگر انجام خواهیم داد!