اذهب إلى المحتوى

الماكرو Macros في لغة رست


Naser Dakhel

استخدمنا الماكرو مثل println!‎ سابقًا ضمن هذه السلسلة البرمجة بلغة رست، إلا أننا لم نتحدث بالكامل عما هو الماكرو وكيفية عمله، إذ تشير كلمة ماكرو إلى مجموعة من الميّزات في رست، ألا وهي الماكرو التصريحية declarative مع macro_rules!‎، إضافةً إلى ثلاثة أنواع من الماكرو الإجرائي procedural:

  • ماكرو [derive]# مخصص يحدد شيفرة مضافة بسمة derive المستخدمة على الهياكل والمعدّدات.
  • ماكرو شبيه بالسمة attribute، الذي يعرف سمات معينة تُستخدم على أية عنصر.
  • ماكرو يشبه الدالة ويشابه استدعاءات الدالة ولكن يعمل على المفاتيح المحددة مثل وسائطها.

سنتحدث عن كلِّ مما سبق بدوره ولكن لنتحدث أولًا عن حاجتنا للماكرو بالرغم من وجود الدوال.

الفرق بين الماكرو والدوال

الماكرو هو طريقة لكتابة شيفرة تكتب شيفرة أُخرى والمعروف بالبرمجة الوصفية metaprogramming، وحدثنا في الملحق "ت" عن سمة derive التي تنشئ تنفيذًا لسمات متعددة، واستخدمنا أيضًا ماكرو ‎‎‎‎‎println‎!‎‎‎ و ‎vec!‎ سابقًا. تتوسع كل هذه الماكرو لتضيف شيفرةً أكثر من الشيفرة التي كُتبت يدويًا.

تفيد البرمجة الوصفية في تقليل كمية الشيفرة التي يجب كتابتها والمحافظة عليها وهو أيضًا أحد أدوار الدوال، لكن لدى الماكرو بعض القوى الإضافية غير الموجودة في الدوال.

يجب أن تصرّح بصمة الدالة signature على عدد ونوع المعاملات الموجودة في الدالة، أما في حالة الماكرو فيمكن أن يأخذ عدد متغير من المعاملات، إذ يمكننا استدعاء println!("hello")‎ بوسيط واحد أو println!("hello {}", name)‎ بوسيطين. يتوسع أيضًا الماكرو قبل أن يفسر المصرف معنى الشيفرة لذا يمكن للماكرو مثلًا تنفيذ سمة على أي نوع مُعطى ولا يمكن للدالة فعل ذلك لأنها تُستدعى وقت التنفيذ وتحتاج لسمة لتُنفذ وقت التصريف.

من مساوئ تنفيذ الماكرو بدلًا من الدالة هو أن تعاريف الماكرو أكثر تعقيدًا من تعاريف الدالة لأننا نكتب شيفرة رست لتكتب شيفرة رست، بالتالي تكون تعاريف الماكرو أكثر تعقيدًا للقراءة والفهم والمحافظة عليها من تعاريف الدالة. هناك فرق آخر مهم بين الماكرو والدوال هو أنه يجب تعريف الماكرو أو جلبه إلى النطاق في ملف قبل استدعائه، على عكس الدوال التي يمكنك تعريفها واستدعائها في كل وقت ومكان.

الماكرو التصريحي مع macro_rules!‎ للبرمجة الوصفية العامة

أكثر أنواع الماكرو استخدامًا في رست هو الماكرو التصريحي الذي يسمى أحيانًا "ماكرو بالمثال macros by example" أو "ماكرو macro_rules!‎" أو ببساطة "ماكرو". يسمح لك الماكرو التصريحي بكتابة شيء مشابه لتعبير match في رست بداخله. تعابير match -كما تحدثنا في المقال بنية match للتحكم بسير برامج لغة رست- هي هياكل تحكم تقبل تعبيرًا وتقارن القيمة الناتجة من التعبير مع النمط وبعدها تنفذ الشيفرة المرتبطة مع النمط المُطابق. يقارن الماكرو أيضًا قيمةً مع أنماط مرتبطة بشيفرة معينة، وتكون القيمة في هذه الحالة هي الشيفرة المصدرية لرست المُمَررة إلى الماكرو. تُقارن الأنماط مع هيكل الشيفرة المصدرية والشيفرة المرتبطة بكل نمط، وعند حدوث التطابق يستبدل الشيفرة المُمَررة إلى الماكرو، ويحصل كل ذلك وقت التصريف.

نستخدم بنية macro_rules!‎ لتعريف الماكرو. دعنا نتحدث عن كيفية استخدام macro_rules!‎ بالنظر إلى كيفية تعريف ماكرو vec!‎، إذ تحدثنا سابقًا في المقال تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست عن كيفية استخدام ماكرو vec!‎ من أجل إنشاء شعاع جديد بقيم معينة. ينشئ الماكرو التالي مثلًا شعاع جديد يحتوي على ثلاثة أعداد صحيحة.

let v: Vec<u32> = vec![1, 2, 3];

يمكن استخدام الماكرو vec!‎ لإنشاء شعاع بعددين صحيحين أو شعاع بخمس سلاسل شرائح نصية string slice، ولا يمكننا فعل ذلك باستخدام الدوال لأننا لا نعرف عدد أو نوع القيم مسبقًا.

تبين الشيفرة 28 تعريفًا مبسطًا لماكرو vec!‎.

  • اسم الملف: src/main.rs
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

[الشيفرة 28: نسخة مبسطة من تعريف ماكرو vec!‎]

ملاحظة: يتضمن التعريف الفعلي لماكرو vec!‎ في المكتبة القياسية شيفرة للحجز الصحيح للذاكرة مسبقًا، وهذه الشيفرة هي تحسين لم نضفه هنا لجعل المثال أبسط.

يشير توصيف ‏[macro_export]#‏‎ إلى أن هذا الماكرو يجب أن يبقى متاحًا عندما يجري إحضار الوحدة المُصرّفة crate المعرّفة داخلها الماكرو إلى النطاق، ولا يمكن إضافة الماكرو إلى النطاق دون هذا التوصيف.

عندما نبدأ بتعريف الماكرو مع macro_rules!‎ ويكون اسم الماكرو الذي نعرّفه بدون علامة التعجب، يكون الاسم في هذه الحالة vec متبوعًا بقوسين معقوصين تدل على متن تعريف الماكرو.

يشابه الهيكل في متن vec!‎ الهيكل في تعبير match، إذ لدينا هنا ذراع واحد مع النمط ‎( $( $x:expr ),* )‎‏‎‎‎ متبوعةً بالعامل ‎=>‎ وكتلة الشيفرة المرتبطة في النمط، وستُرسل الكتلة المرتبطة إذا تطابق النمط. بما أن هذا هو النمط الوحيد في الماكرو، هناك طريقة وحيدة للمطابقة، وأي أنماط أُخرى ستسبب خطأ، ويكون لدى الماكرو الأكثر تعقيدًا أكثر من ذراع واحدة.

تختلف الصيغة الصحيحة في تعاريف الماكرو عن صيغة النمط المذكور سابقًا في المقال صياغة أنماط التصميم الصحيحة Pattern Syntax في لغة رست لأن أنماط الماكرو تُطابق مع هيكل شيفرة رست بدلًا من القيم. لنتحدث عن ماذا تعني أقسام النمط في الشيفرة 28. لقراءة صيغة نمط ماكرو الكاملة راجع مرجع رست.

استخدمنا أولًا مجموعة أقواس لتغليف كامل النمط، واستخدمنا علامة الدولار ($) للتصريح عن متغير في نظام الماكرو الذي يحتوي على شيفرة رست مطابقة للنمط، إذ توضح إشارة الدولار أن هذا متغير ماكرو وليس متغير رست عادي. تأتي بعد ذلك مجموعةٌ من الأقواس التي تلتقط القيم التي تطابق النمط داخل القوسين لاستخدامها في الشيفرة المُستبدلة. توجد ‎$x:expr داخل ‎$()‎‎، التي تطابق أي تعبير رست وتعطي التعبير الاسم ‎$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.

الماكرو الإجرائي لإنشاء شيفرة من السمات

الشكل الثاني من الماكرو هو الماكرو الإجرائي الذي يعمل أكثر مثل دالة (وهي نوع من الإجراءات). يقبل الماكرو الإجرائي بعض الشيفرة مثل دخل ويعمل على الشيفرة ويُنتج بعض الشيفرة مثل خرج بدلًا من مطابقة الأنماط وتبديل الشيفرة بشيفرة أُخرى كما يعمل الماكرو التصريحي. أنواع الماكرو الإجرائي الثلاث، هي: مشتقة مخصصة custom derive، أو مشابهة للسمة attribute-like، أو مشابهة للدالة function-like وتعمل كلها بطريقة مشابهة.

عند إنشاء ماكرو إجرائي، يجب أن يبقى التعريف داخل الوحدة المصرّفة الخاصة به بنوع وحدة مصرّفة خاص، وذلك لأسباب تقنية معقدة نأمل أن نتخلص من وجودها مستقبلًا، تبين الشيفرة 29 كيفية تعريف الماكرو الإجرائي، إذ أن some_attribute هو عنصر مؤقت لاستخدام نوع ماكرو معين.

  • اسم الملف: src/lib.rs
use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

[الشيفرة 29: مثال لتعريف ماكرو إجرائي]

تأخذ الدالة التي تعرّف الماكرو الإجرائي TokenStream مثل دخل وتنتج TokenStream في الخرج. يُعرّف نوع TokenStream بالوحدة المصرّفة proc_macro المتضمنة في رست وتمثّل سلسلة من المفاتيح. هذا هو صلب الماكرو: تكون الشيفرة المصدرية التي يعمل فيها الماكرو هي الدخل TokenStream والشيفرة التي ينتجها الماكرو هي الخرج TokenStream. لدى الدالة سمة مرتبطة بها تحدد أي نوع من الماكرو الإجرائي يجب أن نُنشئ، ويمكن أيضًا الحصول على العديد من الماكرو الإجرائي في الوحدة المصرّفة ذاتها.

لنتحدث عن الأشكال المختلفة من الماكرو الإجرائي. سنبدأ بالماكرو المشتق الخاص ونفسر الاختلافات البسيطة التي تجعل باقي الأشكال مختلفة.

كيفية كتابة ماكرو derive مخصص

لننشئ وحدة مصرّفة اسمها hello_macro التي تعرف سمةً اسمها HelloMacro مع دالة مرتبطة associated اسمها hello_macro، وبدلًا من إجبار المستخدمين على تنفيذ السمة HelloMacro لكل من أنواعهم، سنؤمن ماكرو إجرائي لكي يتمكن المستخدمين من توصيف نوعهم باستخدام ‏‏‏[derive(HelloMacro)‎]‏‏# للحصول على تنفيذ افتراضي للدالة hello_macro.

سيطبع النفيذ الافتراضي:

Hello, Macro! My name is TypeName!‎

إذ أن TypeName هو اسم النوع المُعرّفة عليه السمة، بمعنى آخر سنكتب وحدة مصرّفة تسمح لمبرمج آخر بكتابة الشيفرة باستخدام حزمتنا المصرفة كما في الشيفرة 30.

  • اسم الملف:src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

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

[الشيفرة 30: الشيفرة التي يستطيع مستخدم الوحدة المصرفة فيها الكتابة عند استخدام الماكرو الإجرائي الخاص بنا]

ستطبع الشيفرة عندما تنتهي ما يلي:

Hello, Macro! My name is Pancakes!‎

الخطوة الأولى هي إنشاء وحدة مكتبة مصرّفة على النحو التالي:

$ cargo new hello_macro --lib

بعدها نعرّف سمة HelloMacro والدّالة التابعة لها.

  • اسم الملف: src/lib.rs
pub trait HelloMacro {
    fn hello_macro();
}

لدينا السمة ودوالها، ويستطيع هنا مستخدم الوحدة المصرّفة تنفيذ السمة للحصول على الوظيفة المرغوبة على النحو التالي:

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 التنفيذ الافتراضي الذي سيطبع اسم نوع السمة المُطبقة عليه، إذ ليس لدى رست قدرة على الفهم لذا لا تستطيع البحث عن اسم النوع وقت التنفيذ، وفي هذه الحالة نحن بحاجة لماكرو لإنشاء شيفرة وقت التنفيذ.

الخطوة التالية هي تعريف الماكرو الإجرائي. يحتاج الماكرو الإجرائي حتى الآن إلى وحدة مصرّفة خاصة به، ربما سيُرفع هذا التقييد بالنهاية. يأتي اصطلاح الوحدات المصرّفة الهيكلية والوحدات المصرّفة للماكرو على النحو التالي: يسمى الماكرو الإجرائي الخاص المشتق foo_derive لاسم موحدة مصرفة foo. لنبدأ بإنشاء وحدة مصرّفة جديدة اسمها hello_macro_derive داخل المشروع hello_macro.

$ cargo new hello_macro_derive --lib

الوحدتان المصرّفتان مرتبطتان جدًا، لذلك سننشئ وحدةً مصرّفةً للماكرو الإجرائي داخل مجلد الوحدة المصرّفة hello_macro. يجب علينا تغيير تنفيذ الماكرو الإجرائي في hello_macro_derive إذا غيرنا تعريف السمة في hello_macro أيضًا. تحتاج الوحدتان المصرّفتان أن تُنشَرا بصورةٍ منفصلة ويجب أن يضيف مستخدمو هاتين الوحدتين المصرّفتين مثل اعتماديتين dependencies وجلبهما إلى النطاق. يمكن -بدلًا من ذلك- جعل الحزمة المصرّفة hello_macro تستخدم hello_macro_derive مثل اعتمادية وتعيد تصدير شيفرة الماكرو الإجرائي ولكن الطريقة التي بنينا فيها المشروع تسمح للمبرمجين استخدام hello_macro حتى لو كانوا لا يرغبون باستخدام وظيفة derive.

يجب علينا التصريح عن الوحدة المصرفة hello_macro_derive مثل وحدة مصرفة لماكرو إجرائي ونحتاج أيضًا إلى وظائف من الوحدات المصرّفة syn و quote كما سنرى بعد قليل لذا سنحتاج لإضافتهم كاعتماديات. أضِف التالي إلى ملف Cargo.toml من أجل hello_macro_derive:

  • اسم الملف: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true

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

لنبدأ بتعريف الماكرو الإجرائي. ضع الشيفرة 31 في ملف src/lib.rs من أجل الوحدة المصرّفة hello_macro-derive. لاحظ أن الشيفرة لن تصرّف حتى نضيف التعريف لدالة impl_hello_macro.

  • اسم الملف: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // إنشاء تمثيل لشيفرة رست مثل شجرة صيغة يمكننا التلاعب بها 
    let ast = syn::parse(input).unwrap();
    // بناء تنفيذ السمة
    impl_hello_macro(&ast)
}

[الشيفرة 31: الشيفرة التي تتطلبها معظم الوحدات المصرّفة للماكرو الإجرائي لكي تعالج شيفرة رست]

لاحظ أننا قسّمنا الشيفرة إلى دالة hello_macro_derive المسؤولة عن تحليل TokenStream، ودالة Impl_hello_macro المسؤولة عن تحويل شجرة الصيغة syntax tree التي تجعل كاتبة الماكرو الإجرائي لتكون أكثر ملائمة. ستكون الشيفرة في الدالة الخارجية (في هذه الحالة hello_macro_derive) هي نفسها لمعظم الوحدات المصرّفة للماكرو الإجرائي الذي تراه أو تنشئه، وستكون الشيفرة التي تحددها في محتوى الدالة الداخلية (في هذه الحالة impl_hello_macro) مختلفة اعتمادًا على غرض الماكرو الإجرائي.

أضفنا ثلاث وحدات مصرّفة هي proc_macro و syn و quote. لا نحتاج لإضافة الوحدة المصرفة proc_macro إلى الاعتماديات في Cargo.toml لأنها تأتي مع رست، وهذه الوحدة المصرفة هي واجهة برمجة التطبيق للمصرف التي تسمح بقراءة وتعديل شيفرة رست من شيفرتنا.

تحلّل الوحدة المصرّفة syn شيفرة رست من سلسلة نصية إلى هيكل بيانات يمكننا إجراء عمليات عليه. تحوّل الوحدة المصرّفة quote هيكل بيانات syn إلى شيفرة رست. تسهّل هذه الوحدات المصرّفة تحليل أي نوع من شيفرة رست يمكن أن نعمل عليه. تُعد كتابة محلل parser كامل لرست أمرًا صعبًا.

تُستدعى دالة hello_macro_derive عندما يحدد مستخدم مكتبتنا [derive(HelloMacro)‎]# على نوع، وهذا ممكن لأننا وصفّنا دالة hello_macro_dervie باستخدام proc_macro_dervie وحددنا اسم HelloMacro الذي يطابق اسم سِمتنا، وهذا هو الاصطلاح الذي يتبعه معظم الماكرو الإجرائي.

تحوّل دالة hello_macro_derive أولًا input من TokenStream إلى هيكل بيانات يمكن أن نفسره ونجري عمليات عليه. هنا يأتي دور syn. تأخذ دالة parse في syn القيمة TokenStream وتُعيد هيكل DeriveInput يمثّل شيفرة رست المحلّلة. تظهر الشيفرة 32 الأجزاء المهمة من هيكل 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
            )
        }
    )
}

[الشيفرة 32: نسخة DeriveInput التي نحصل عليها من تحليل الشيفرة التي فيها سمة الماكرو في الشيفرة 30]

تظهر حقول هذا الهيكل بأن شيفرة رست التي حللناها هي هيكل وحدة مع ident (اختصارًا للمعرّف، أي الاسم) الخاصة بالاسم Pancakes. هناك حقول أخرى في هذا الهيكل لوصف كل أنواع شيفرة رست. راجع وثائق syn من أجل DeriveInput لمعلومات أكثر.

سنعرِّف قريبًا دالة impl_hello_macro، التي سنبني فيها شيفرة رست الجديدة التي نريد ضمها، لكن قبل ذلك لاحظ أن الخرج من الماكرو المشتق الخاص بنا هو أيضًا TokenStream، إذ تُضاف TokenStream المُعادة إلى الشيفرة التي كتبها مستخدمو حزمتنا المصرّفة، لذلك سيحصلون عند تصريف الوحدة المصرّفة على وظائف إضافية قدمناها في TokenStream المعدلة.

ربما لاحظت أننا استدعينا unwrap لتجعل الدالة hello_macro_derive تهلع إذا فشل استدعاء الدالة syn::parse. يجب أن يهلع الماكرو الإجرائي على الأخطاء، لأنه يجب أن تعيد الدالة proc_macro_derive الـقيمة TokenStream بدلًا من Result لتتوافق مع واجهة برمجة التطبيقات للماكرو الإجرائي. بسّطنا هذا المثال باستخدام unwrap، إلا أنه يجب تأمين رسالة خطأ محددة أكثر في شيفرة الإنتاج باستخدام panic!‎ أو expect.

الآن لدينا الشيفرة لتحويل شيفرة رست الموصّفة من TokenStream إلى نسخة DeriveInput لننشئ الشيفرة التي تطبّق سمة HelloMacro على النوع الموصّف كما تظهر الشيفرة 33.

  • اسم الملف: hello_macro_derive/src/lib.rs
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()
}

[الشيفرة 33: تنفيذ سمة HelloMacro باستخدام شيفرة رست المحلّلة]

نحصل على نسخة هيكل Indent يحتوي على الاسم (المُعرّف) على النوع الموصّف باستخدام ast.ident. يظهر الهيكل في الشيفرة 32 أنه عندما نفّذنا دالة impl_hello_macro على الشيفرة 30 سيكون لدى ident التي نحصل عليها حقل ident مع القيمة "Pancakes"، لذلك سيحتوي المتغير name في الشيفرة 33 نسخة هيكل Ident، الذي سيكون سلسلة نصية "Pancakes" عندما يُطبع، وهو اسم الهيكل في الشيفرة 30.

يسمح لنا ماكرو quote!‎ بتعريف شيفرة رست التي نريد إعادتها. يتوقع المصرف شيئًا مختلفًا عن النتيجة المباشرة لتنفيذ ماكرو quote!‎، لذا نحتاج لتحويله إلى TokenStream، وذلك عن طريق استدعاء تابع into الذي يستهلك التعبير الوسطي ويعيد القيمة من النوع TokenStream المطلوب.

يؤمن ماكرو quote!‎ تقنيات قولبة templating جيدة، إذ يمكننا إدخال ‎#‎‎name ويبدّلها quote!‎ بالقيمة الموجودة في المتغير name، ويمكنك أيضًا إجراء بعض التكرارات بطريقة مشابهة لكيفية عمل الماكرو العادي. راجع توثيق الوحدة المصرّفة quote لتعريف وافي عنها.

نريد أن يُنشئ الماكرو الإجرائي تنفيذًا لسمة HelloMacro للنوع الذي يريد توصيفه المستخدم، والذي نحصل عليه باستخدام ‎#‎‎name. يحتوي تنفيذ السمة دالةً واحدةً hello_macro تحتوي على الوظيفة المراد تقديمها ألا وهي طباعة Hello, ‎Macro! My name is وبعدها اسم النوع الموصَّف.

الماكرو stringify!‎ المُستخدم هنا موجود داخل رست، إذ يأخذ تعبير رست مثل 1‎ + 2‎ ويحول التعبير إلى سلسلة نصية مجرّدة مثل "‎1 +2". هذا مختلف عن format!‎ و println!‎، الماكرو الذي يقيّم التعبير ويحول القيمة إلى String. هناك احتمال أن يكون الدخل ‎#‎name تعبيرًا للطباعة حرفيًا literally، لذا نستخدم stringify!‎، الذي يوفر مساحةً محجوزةً عن طريق تحويل ‎‎#‎name إلى سلسلة نصية مجرّدة وقت التصريف.

الآن، يجب أن ينتهي cargo build بنجاح في كل من hello_macro و hello_macro_derive. لنربط هذه الوحدات المصرّفة مع الشيفرة في الشيفرة 30 لنرى كيفية عمل الماكرو الإجرائي. أنشئ مشروعًا ثنائيًا جديدًا في مجلد المشاريع باستخدام cargo new pancakes. نحتاج لإضافة hello_macro و hello_macro_derive مثل اعتماديات في ملف Cargo.toml الخاص بالوحدة المصرّفة pancakes. إذا نشرت النسخ الخاصة بك من hello_macro و hello_macro_derive إلى crates.io فستكون اعتماديات عادية، وإذا لم يكونوا كذلك فبإمكانك تحديدها مثل اعتماديات path على النحو التالي:

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

ضع الشيفرة 30 في الملف src/main.rs ونفذ cargo run يجب أن تطبع Hello, Macro! My name is Pancakes!‎. كان تنفيذ سمة HelloMacro من الماكرو الإجرائي متضمنًا دون أن تحتاج الوحدة المصرفة pancakes أن تنفّذه. أضاف [‎‎derive(HelloMac‎ro)‎]# تنفيذ السمة.

سنتحدث تاليًا عن الاختلافات بين الأنواع الأُخرى من الماكرو الإجرائي من الماكرو المشتق الخاص.

الماكرو الشبيه بالسمة

يشابه الماكرو الشبيه بالسمة الماكرو المشتق الخاص لكن بدلًا من إنشاء شيفرة لسمة derive يسمح لك بإنشاء سمات جديدة وهي أيضًا أكثر مرونة، تعمل derive فقط مع الهياكل والـتعدادات enums، يمكن أن تطبق السمات attributes على عناصر أُخرى أيضًا مثل الدوال. فيما يلي مثال عن استخدام الماكرو الشبيه بالسمة: لنقل أن لديك سمة اسمها route توصّف الدوال عند استخدام إطار عمل تطبيق ويب:

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

تُعرَّف سمة ‏‏[route]# بإطار العمل مثل ماكرو إجرائي. ستكون بصمة دالة تعريف الماكرو على النحو التالي:

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

لدينا هنا معاملان من النوع TokenStream، الأول هو من أجل محتوى السمة (جزء GET, "/"‎)، والثاني هو لمتن العنصر الذي ترتبط به السمة والذي هو fn index‎() {}‎ في هذه الحالة والباقي هو متن الدالة.

عدا عن ذلك، تعمل الماكرو الشبيهة بالسمة بنفس طريقة الماكرو المشتق الخاص عن طريق إنشاء وحدة مصرفة مع نوع الوحدة المصرّفة proc-macro وتنفذ الدالة التي تنشئ الشيفرة المرغوبة.

الماكرو الشبيه بالدالة

يعرّف الماكرو الشبيه بالدالة الماكرو ليشبه استدعاءات الدوال، وعلى نحوٍ مشابه لماكرو macro_rules!‎، فهي أكثر مرونة من الدوال؛ إذ يستطيع الماكرو أخذ عدد غير معروف من الوسطاء، ولكن يمكن أن يعرّف ماكرو macro_rules!‎ فقط باستخدام صيغة تشبه المطابقة التي تحدثنا عنها سابقًا في قسم "الماكرو التصريحي مع macro_rules!‎ للبرمجة الوصفية العامة". يأخذ الماكرو الشبيه بالدالة معامل TokenStream ويعدل تعريفها القيمة TokenStream باستخدام شيفرة رست كما يفعل الماكرو الإجرائي السابق. إليك مثالًا عن ماكرو شبيه بالدالة هو ماكرو sql!‎ التي يمكن استدعاؤه على النحو التالي:

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

يحلل هذا الماكرو تعليمة SQL داخله ويتحقق إذا كانت صياغتها صحيحة، وهذه المعالجة أعقد مما يستطيع macro_rules!‎ معالجته ويكون تعريف ماكرو sql!‎ على النحو التالي:

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

يشابه التعريف بصمة الماكرو المشتق الخاص، إذ أخذنا المفاتيح التي داخل القوسين وأعدنا الشيفرة التي نريد إنشاءها.

ترجمة -وبتصرف- لقسم من الفصل Advanced Features من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...