استخدمنا الماكرو مثل 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(HelloMacro)]#
تنفيذ السمة.
سنتحدث تاليًا عن الاختلافات بين الأنواع الأُخرى من الماكرو الإجرائي من الماكرو المشتق الخاص.
الماكرو الشبيه بالسمة
يشابه الماكرو الشبيه بالسمة الماكرو المشتق الخاص لكن بدلًا من إنشاء شيفرة لسمة 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.