معمارية تطبيقات المصادر المفتوحة › بيئة تشغيل اللغات الديناميكية ولغات Iron 35 / 51 السابق التالي

بيئة تشغيل اللغات الديناميكية ولغات Iron

معمارية تطبيقات المصادر المفتوحة (المجلد الثاني)بيئة تشغيل اللغات الديناميكية ولغات Iron

معمارية تطبيقات المصادر المفتوحة (المجلد الثاني) بيئة تشغيل اللغات الديناميكية ولغات Iron

Jeff Hardy

إذا استمتعت بهذه الكتب، فقد يعجبك أيضًا التصميم البرمجي بالأمثلة في بايثون (Software Design by Example in Python) والتصميم البرمجي بالأمثلة في جافاسكربت (Software Design by Example in JavaScript).

لغات Iron هي مجموعة غير رسمية من تنفيذات اللغات تحمل كلمة «Iron» في أسمائها، تكريمًا لأوائلها، IronPython. تتشارك كل هذه اللغات في شيء واحد على الأقل—فهي لغات ديناميكية تستهدف بيئة التشغيل الشائعة (Common Language Runtime، أو CLR)، وهي المشار إليها على نحو أشيع باسم ‎.NET Framework، وهي مبنيّة فوق بيئة تشغيل اللغات الديناميكية (Dynamic Language Runtime، أو DLR). («CLR» هو المصطلح العام؛ ‎.NET Framework هو تطبيق مايكروسوفت له، وهناك أيضًا تطبيق مفتوح المصدر اسمه Mono). وتُعدّ DLR مجموعة مكتبات لـ CLR توفّر دعمًا أفضل بكثير للغات الديناميكية على CLR. ويُستخدم IronPython وIronRuby كلاهما في عشرات المشاريع المغلقة والمفتوحة المصدر، وكلاهما تحت التطوير النشط؛ أما DLR، التي بدأت كمشروع مفتوح المصدر، فهي مُضمَّنة كجزء من ‎.NET Framework ومن Mono.

من الناحية المعمارية، IronPython وIronRuby وDLR بسيطة في آن واحد وشديدة التعقيد على نحو شيطاني. من مستوى عالٍ، تتشبه التصاميم مع كثير من تنفيذات اللغات الأخرى، إذ تحتوي على محلّلات (parsers) ومصرّفات (compilers) ومولّدات شيفرة؛ غير أن إمعان النظر قليلًا يكشف بدء ظهور التفاصيل المثيرة للاهتمام: فمواقع الاستدعاء (call sites) والمُصنِّفات (binders) والتصريف التجاوب وتقنيات أخرى تُستخدم لجعل اللغات الديناميكية تعمل بسرعة تكاد تقارب اللغات الساكنة على منصّة صُمِّمت من أجل اللغات الساكنة.

8.1. التاريخ

يبدأ تاريخ لغات Iron عام 2003. كان جيم هوغونين قد كتب تنفيذًا للغة بايثون اسمه Jython من أجل آلة جافا الافتراضية (JVM). وفي ذلك الوقت، كان وقت التشغيل المشترك (Common Language Runtime، أو CLR) في إطار عمل ‎.NET Framework الذي كان حديثًا آنذاك، يُعدّ من قِبل بعض الناس (ولا أدري بالضبط مَن) ضعيف الملاءمة لتنفيذ لغات ديناميكية مثل بايثون. ولأن جيم كان قد نفّذ بايثون على JVM من قبل، فقد شغله كيف أمكن لِمايكروسوفت أن تجعل ‎.NET أسوأ بكثير من جافا. وفي مقالة على مدونته في سبتمبر 2006، كتب ما يلي:

أردت أن أفهم كيف أمكن لمايكروسوفت أن تفسد الأمور إلى هذا الحد، بحيث يصبح CLR منصّة أسوأ من JVM للغات الديناميكية. كانت خطتي أن أستغرق بضعة أسابيع لبناء نسخة أولية (prototype) من تنفيذ بايثون على CLR، ثم أستخدم ذلك العمل لكتابة مقال قصير لاذع بعنوان «لماذا يُعدّ CLR منصّة سيئة للغات الديناميكية». لكن خطاطي تغيّرت سريعًا أثناء عملي على النسخة الأولية، لأنني اكتشفت أن بايثون يمكن أن تعمل على CLR تشغيلًا ممتازًا—وفي كثير من الحالات أسرع بشكل ملحوظ من التنفيذ المبني على C. وفي حالة معيار pystone القياسي، كان IronPython على CLR أسرع بنحو 1.7 مرة من التنفيذ المبني على C.

(جزء «Iron» في الاسم كان تلاعبًا باسم شركة جيم آنذاك، وهي Want of a Nail Software.)

بعد ذلك بوقت قصير، عيّنت مايكروسوفت جيم كي يجعل ‎.NET منصّة أفضل أيضًا للغات الديناميكية. وطوّر جيم (وعدة أشخاص آخرين) DLR عبر استخراج الأجزاء المحايدة عن اللغة من شيفرة IronPython الأصلية. صُمِّمت DLR لتوفّر نواة مشتركة لتنفيذ اللغات الديناميكية لـ ‎.NET، وكانت إضافةً رئيسية جديدة في ‎.NET 4.

وفي الوقت نفسه الذي أُعلِن فيه عن DLR (أبريل 2007)، أعلنت مايكروسوفت أيضًا أنها ستقوم، بالإضافة إلى إصدار جديد من IronPython مبنيّ فوق DLR (وهو IronPython 2.0)، بتطوير IronRuby فوق DLR لإظهار قدرة DLR على التكيّف مع لغات متعددة. (وفي أكتوبر 2010، أوقفت مايكروسوفت تطوير IronPython وIronRuby وأصبحا مشروعين مفتوحين المصدر مستقلّين). وكان التكامل مع اللغات الديناميكية باستخدام DLR سيكون أيضًا جزءًا رئيسيًا من C# وVisual Basic، مع كلمة مفتاحية جديدة (dynamic) تتيح لتلك اللغات استدعاء أي لغة منفَّذة على DLR، أو أي مصدر بيانات ديناميكي آخر، بسهولة. وكان CLR قد صار بالفعل منصّة جيدة لتنفيذ اللغات الساكنة، وتجعل DLR اللغات الديناميكية موطنًا من الدرجة الأولى.

وتستخدم تنفيذات لغات أخرى من خارج مايكروسوفت DLR أيضًا، ومنها IronScheme وIronJS. إضافةً إلى ذلك، سيستخدم PowerShell v3 من مايكروسوفت DLR بدلًا من نظام الكائنات الديناميكي الخاص به.

8.2. مبادئ بيئة تشغيل اللغات الديناميكية

صُمِّم CLR مراعاةً للغات مُنمَّطة ساكنًا؛ فمعرفة الأنواع مغروسة في عمق وقت التشغيل، ومن افتراضاته الأساسية أن تلك الأنواع لا تتغيّر—أن المتغيّر لا يغيّر نوعه أبدًا، أو أن النوع لا يُضاف إليه حقول أو أعضاء ولا يُحذف منها أيّها أثناء تشغيل البرنامج. وهذا مقبول في لغات مثل C# أو Java، لكن اللغات الديناميكية، بحكم تعريفها، لا تتبع هذه القواعد. كما يوفّر CLR نظام كائنات مشتركًا للأنواع الساكنة، ما يعني أن أي لغة من لغات ‎.NET تستطيع استدعاء كائنات مكتوبة بأي لغة ‎.NET أخرى دون أي جهد إضافي.

من دون DLR، كان على كل لغة ديناميكية أن توفّر نموذج كائناتها الخاص؛ ولم تكن اللغات الديناميكية المختلفة قادرة على استدعاء كائنات في لغة ديناميكية أخرى، ولم يكن C# قادرًا على معاملة IronPython وIronRuby على قدم المساواة. من هنا، فإن قلب DLR هو طريقة قياسية لتنفيذ الكائنات الديناميكية مع السماح في الوقت نفسه بتخصيص سلوك الكائن تبعًا للغة بعينها باستخدام المُصنِّفات (binders). وهي تتضمن أيضًا آلية تُعرف باسم التخزين المؤقت لمواقع الاستدعاء (call-site caching) لضمان أن تكون العمليات الديناميكية في أسرع وقت ممكن، ومجموعة أصناف لبناء أشجار التعبيرات (expression trees) تتيح تخزين الشيفرة كبيانات ومعالجتها بسهولة.

كما يوفّر CLR عدّة ميزات أخرى مفيدة للغات الديناميكية، منها جامع قمامة (garbage collector) متطوّر؛ ومصرّف في الوقت المناسب (Just-in-Time، أو JIT) يحوّل شيفرة بايت اللغة الوسيطة الشائعة (Common Intermediate Language، أو IL) — وهي ما تُخرجه مصرّفات ‎.NET — إلى شيفرة آلة في وقت التشغيل؛ ونظام تأمّل في وقت التشغيل (introspection)، يتيح للغات الديناميكية استدعاء كائنات مكتوبة بأي لغة ساكنة؛ وأخيرًا، الدوال الديناميكية (المعروفة أيضًا بتوليد الشيفرة الخفيف) التي تتيح توليد الشيفرة في وقت التشغيل ثم تنفيذها بتكلفة إضافية هامشية فقط مقارنة باستدعاء دالة ساكنة. (واكتسبت JVM آلية مماثلة عبر invokedynamic في Java 7.)

وتتيجة تصميم DLR هي أن لغات مثل IronPython وIronRuby تستطيع استدعاء كائنات بعضها البعض (وكائنات أي لغة أخرى على DLR)، لأنها تشترك في نموذج كائنات ديناميكي مشترك. وأُضيف دعم لهذا النموذج أيضًا إلى C# 4 (عبر الكلمة المفتاحية dynamic) وإلى Visual Basic 10 (إلى جانب طريقة VB القائمة في «الربط المتأخر (late binding)»)، بحيث تستطيع اللغتان إجراء استدعاءات ديناميكية على الكائنات أيضًا. وهكذا تجعل DLR اللغات الديناميكية موطنًا من الدرجة الأولى على ‎.NET.

ولعلّ من اللافت أن DLR منفَّذة بالكامل كمجموعة مكتبات، ويمكن بناؤها وتشغيلها على ‎.NET 2.0 أيضًا. ولا يلزم أي تغيير على CLR لتنفيذها.

8.3. تفاصيل تنفيذ اللغات

لكل تنفيذ لغة مرحلتان أساسيتان—التحليل النحوي (الواجهة الأمامية) وتوليد الشيفرة (الواجهة الخلفية). في DLR، تنفّذ كل لغة واجهتها الأمامية الخاصة، وفيها محلّل اللغة ومولّد شجرة الصياغة؛ وتوفّر DLR واجهة خلفية مشتركة تأخذ أشجار التعبيرات لتُنتج اللغة الوسيطة (Intermediate Language، أو IL) يستهلكها CLR؛ ثم يمرّر CLR الـ IL إلى مصرّف في الوقت المناسب (Just-In-Time، أو JIT) ينتج شيفرة آلة تُنفَّذ على المعالج. أما الشيفرة المُعرَّفة في وقت التشغيل (وتُنفَّذ باستخدام eval) فتُعالَج بطريقة مماثلة، باستثناء أن كل شيء يحدث عند موقع استدعاء eval بدلًا من أن يحدث عند تحميل الملف.

هناك بضع طرق مختلفة لتنفيذ الأجزاء الجوهرية للواجهة الأمامية للغة، وعلى الرغم من أن IronPython وIronRuby متشابهتان جدًا (فقد طُوِّرتا جنبًا إلى جنب، بعد كل شيء)، فإنهما تختلفان في بضع مجالات جوهرية. ولكل من IronPython وIronRuby تصاميم محلّلات قياسية إلى حد كبير—فكلاهما يستخدم المُرمِّز (tokenizer) (المعروف أيضًا بـ المُحلِّل اللفظي (lexer)) لتقسيم النص إلى رموز (tokens)، ثم يحوّل المحلّل تلك الرموز إلى شجرة صياغة مجرّدة (Abstract Syntax Tree، أو AST) تمثّل البرنامج. غير أن اللغات تختلف اختلافًا تامًّا في تنفيذ هذه الأجزاء.

8.4. التحليل النحوي

رمّز IronPython موجود في الصنف IronPython.Compiler.Tokenizer، ومحلّلها في الصنف IronPython.Compiler.Parser. الرمّز آلة حالات مكتوبة يدويًا تتعرّف على الكلمات المفتاحية وعوامل التشغيل وأسماء بايثون وتُنتج الرموز المقابلة. ويحمل كل رمز معه أي معلومات إضافية (مثل قيمة ثابت أو اسم)، وكذلك الموضع الذي وُجد عنده الرمز في المصدر، للمساعدة في التصحيح. ثم يأخذ المحلّل مجموعة الرموز هذه ويقارنها بقواعد بايثون (grammar) ليتبيّن ما إذا كانت تطابق بنى بايثون مشروعة.

محلّل IronPython هو محلّل تنازلي تعاوُضي من نوع LL(1). ينظر المحلّل في الرمز الوارد، ويستدعي دالة إذا كان الرمز مسموحًا به ويعيد خطأً إن لم يكن. ويُبنى المحلّل التنازلي التعاوشوي من مجموعة دوال متداخلة معًا تعاوشيًا؛ وهذه الدوال تنفّذ في النهاية آلة حالات، بحيث إن كل رمز جديد يُطلق انتقالًا بين الحالات. ومثل الرمّز، فإن محلّل IronPython مكتوب يدويًا.

أما IronRuby، من جهة أخرى، فلها رمّز ومحلّل مولَّدان بواسطة Gardens Point Parser Generator (GPPG). ويُوصف المحلّل في الملف Parser.y (Languages/Ruby/Ruby/Compiler/Parser/Parser.y)، وهو ملف بصيغة yacc يصف قواعد IronRuby على مستوى عالٍ باستخدام قواعد تصف الصياغة. ثم يأخذ GPPG ملف Parser.y وينشئ دوال المحلّل وجداوله الفعلية؛ والنتيجة محلّل LALR(1) قائم على الجداول. أما الجداول المولَّدة فهي مصفوفات طويلة من الأعداد الصحيحة، حيث يمثّل كل عدد صحيح حالةً؛ وبناءً على الحالة الحالية والرمز الحالي، تحدّد الجداول الحالة التي ينبغي الانتقال إليها تاليًا. وبينما محلّل IronPython التنازلي التعاوشي سهل القراءة إلى حد كبير، فإن المحلّل المولَّد في IronRuby ليس كذلك. فجدول الانتقال ضخم (540 حالة متمايزة وأكثر من 45,000 انتقال)، ويكاد يستحيل تعديله يدويًا.

وفي نهاية المطاف، هذا مقايضة هندسية—فمحلّل IronPython بسيط بما يكفي ليُعدَّل يدويًا، لكنه معقّد بما يكفي ليُخفي بنية اللغة. أما محلّل IronRuby، من جهة أخرى، فيجعل فهم بنية اللغة في الملف Parser.y أسهل بكثير، لكنه صار الآن معتمدًا على أداة من طرف ثالث تستخدم لغة خاصة بالمجال (domain-specific language) مخصّصة (وإن كانت معروفة)، وقد تكون لها أخطاؤها أو سماتها الخاصة. في هذه الحالة، لم ترغب فريق IronPython في الالتزام باعتماد على أداة خارجية، فيما لم يمانع فريق IronRuby ذلك.

ما هو واضح، مع ذلك، هو مدى أهمية آلات الحالات في التحليل النحوي، في كل مرحلة. فبالنسبة إلى أي مهمة تحليل نحوي، مهما كانت بسيطة، فإن آلة الحالات هي الإجابة الصحيحة دائمًا.

أما مُخرَج المحلّل لأي من اللغتين فهو شجرة صياغة مجرّدة (AST). وهذه تصف بنية البرنامج على مستوى عالٍ، حيث يقابل كل عقدة بنيةَ لغة مباشرة—جملة أو تعبير. ويمكن التلاعب بهذه الأشجار في وقت التشغيل، غالبًا لإجراء تحسينات على البرنامج قبل الترجمة. غير أن شجرة صياغة أي لغة مرتبطة بتلك اللغة؛ ويحتاج DLR إلى العمل على أشجار لا تحتوي على أي بنى خاصة بالغة، بل بنى عامة فقط.

8.5. أشجار التعبيرات

شجرة التعبير هي أيضًا تمثيل لبرنامج يمكن التلاعب به في وقت التشغيل، لكن في صورة أدنى المستوى ومستقلة عن اللغة. في ‎.NET، أنواع العقد موجودة في فضاء الأسماء System.Linq.Expressions، وجميع أنواع العقد مشتقّة من الصنف المجرّد Expression. (فضاء الأسماء هذا أثر تاريخي؛ فقد أُضيفت أشجار التعبيرات أصلًا في ‎.NET 3.5 لتنفيذ LINQ—‎Language Integrated Query—ووسّعت أشجار تعبيرات DLR ذاك). غير أن أشجار التعبيرات هذه تغطّي أكثر من مجرّد التعبيرات، إذ توجد أنواع عقد لعبارات if وكتل try والحلقات أيضًا؛ وفي بعض اللغات (Ruby مثلًا) هذه تعبيرات وليست جملًا.

وتوجد عقد تغطّي تقريبًا كل ميزة قد ترغب فيها لغة برمجة. غير أنها تميل إلى أن تُعرَّف عند مستوى منخفض نوعًا ما—فبدلًا من وجود ForExpression وWhileExpression وما إلى ذلك، توجد عقدة واحدة LoopExpression يمكنها، عند ربطها بـ GotoExpression، أن تصف أي نوع من أنواع الحلقات. ولوصف لغة عند مستوى أعلى، يمكن للغات أن تُعرّف أنواع عقدها الخاصة بالاشتقاق من Expression وتجاوز طريقة Reduce() التي تُعيد شجرة تعبيرات أخرى. وفي IronPython، شجرة التحليل النحوي هي أيضًا شجرة تعبيرات DLR، لكنها تحتوي على عقد كثيرة مخصّصة لا يفهمها DLR عادةً (مثل ForStatement). ويمكن اختصار هذه العقد المخصّصة إلى أشجار تعبيرات يفهمها DLR فعلًا (مثل توليفة من LoopExpression وGotoExpression). ويمكن لعقدة تعبير مخصّصة أن تُختصر إلى عقد تعابير مخصّصة أخرى، فيتحرّك الاختصار بشكل متكرر حتى لا تبقى سوى عقد DLR الأصلية. ومن الفروق الجوهرية بين IronPython وIronRuby أن شجرة صياغة IronPython هي أيضًا شجرة تعبيرات، أما شجرة IronRuby فليست كذلك. وبدلًا من ذلك، تُحوَّل شجرة صياغة IronRuby إلى شجرة تعبيرات قبل الانتقال إلى المرحلة التالية. ومن القابل للنقاش ما إذا كان كون شجرة الصياغة شجرة تعبيرات أيضًا مفيدًا حقًّا، ولذلك لم ينفّذ IronRuby الأمر على هذه النحو.

كل نوع عقد يعرف كيف يختصر نفسه، ويمكن عادةً اختصاره بطريقة واحدة فقط. أما التحويلات التي تأتي من شيفرة خارج الشجرة—تحسينات مثل طيّ الثوابت (constant folding)، أو تنفيذ IronPython لمُولِّدات بايثون (generators)—فيُستخدم عندها صنف مشتق من صنف ExpressionVisitor. ويملك ExpressionVisitor طريقة Visit() تستدعي الطريقة Accept() على Expression، وتتجاوز الأصناف المشتقّة من Expression الدالة Accept() لتستدعي طريقة Visit() محدّدة على ExpressionVisitor، مثل VisitBinary(). وهذه هي تنفيذٌ نموذجي من كتاب لنمط الزائر (Visitor pattern) كما قدّمه Gamma وآخرون—هناك مجموعة ثابتة من أنواع العقد التي تُزار، وعدد لا نهائي من العمليات التي يمكن إجراؤها عليها. وعندما يزور زائر التعبير عقدة ما، فإنه يزور عادةً أبناءها كذلك على نحو متكرر، ثم أبناء أبناءهم، وهكذا نزولًا في الشجرة. غير أن ExpressionVisitor لا يستطيع في الواقع تعديل شجرة التعبيرات التي يزورها، لأن أشجار التعبيرات غير قابلة للتغيير (immutable). فإذا احتاج زائر التعبير إلى تعديل عقدة (مثل حذف أبناء)، فيجب أن يُنتج عقدة جديدة تحلّ محل القديمة، وكذلك كل آباءها.

وبعد أن تُنشأ شجرة التعبيرات وتُختصر وتُزار، فإنها في النهاية تحتاج إلى التنفيذ. وبينما يمكن لأشجار التعبيرات أن تُصرَّف مباشرةً إلى شيفرة IL، فإن IronPython وIronRuby يمرّرانها أولًا إلى مُفسِّر (interpreter)، لأن التصريف المباشر إلى IL مكلف جدًا في حالة الشيفرة التي قد لا تُنفَّذ سوى عدد قليل من المرات.

8.6. التفسير والترجمة

من عيوب استخدام مصرّف JIT، كما تفعل ‎.NET، أنه يفرض عقوبة زمنية عند بدء التشغيل، إذ يستغرق تحويل شيفرة بايت IL إلى شيفرة آلة يستطيع المعالج تنفيذها وقتًا. ويجعل التصريف في الوقت المناسب الشيفرة أسرع بكثير أثناء التشغيل مقارنةً باستخدام مُفسِّر، لكن التكلفة الأولية قد تكون باهظة، بحسب ما يجري تنفيذه. فعلى سبيل المثال، تستفيد عملية خادم طويلة العمر كتطبيق ويب من JIT، لأن زمن بدء التشغيل لا يهم كثيرًا في حين أن زمن معالجة كل طلب حاسم، فهو يميل إلى تشغيل الشيفرة نفسها مرارًا. وعلى النقيض، فإن برنامجًا يُشغَّل كثيرًا لكن لفترات قصيرة فقط، مثل عميل سطر الأوامر Mercurial، يكون أفضل حالًا بزمن بدء تشغيل قصير، لأنه من المرجَّح أنه لا ينفّذ كل مقطع شيفرة إلا مرة واحدة، وكون شيفرة JIT أسرع لا يُعوّض كونها تستغرق وقتًا أطول كي تبدأ العمل.

لا يستطيع ‎.NET تنفيذ شيفرة IL مباشرةً؛ بل تُصرَّف دائمًا عبر JIT إلى شيفرة آلة، وهذا يستغرق وقتًا. وعلى وجه الخصوص، تُعدّ أزمنة بدء تشغيل البرامج إحدى نقاط ضعف ‎.NET Framework، لأن كثيرًا من الشيفرة يحتاج إلى تصريف JIT. وبينما توجد طرائق لتجنّب عقوبة JIT في برامج ‎.NET الساكنة (توليد الصور الأصلية (Native Image Generation)، أو NGEN)، فإنها لا تنفع مع البرامج الديناميكية. وبدلًا من التصريف دائمًا مباشرةً إلى IL، يستخدم IronRuby وIronPython مُفسِّرهما الخاص (الموجود في Microsoft.Scripting.Interpreter) الذي ليس أسرع من الشيفرة المصروفة عبر JIT، لكنه يستغرق وقتًا أقل بكثير كي يبدأ. والمُفسِّر مفيد أيضًا في الحالات التي لا يُسمح فيها بتوليد الشيفرة الديناميكيًا، كما في المنصات المحمولة؛ وإلا لما استطاعت لغات DLR على الإطلاق أن تعمل.

قبل التنفيذ، يجب أن تُلَفّ شجرة التعبيرات بالكامل داخل دالة كي تصبح قابلة للتنفيذ. في DLR، تُمثَّل الدوال بعقد LambdaExpression. وبينما تكون lambda في معظم اللغات دالة مجهولة الهوية، فإن DLR لا تملك مفهوم الأسماء؛ فجميع الدوال مجهولة. وLambdaExpression فريدة من نوعها لأنها نوع العقدة الوحيد الذي يمكن تحويله إلى مُفوِّض (delegate) — وهو ما يسمّيه ‎.NET بالدوال من الدرجة الأولى — باستخدام طريقة Compile(). ويُشبه المُفوِّض مؤشر دالة من لغة C—إنه ببساطة مقبض إلى قطعة شيفرة يمكن استدعاؤها.

في البداية، تُلَفّ شجرة التعبيرات في LightLambdaExpression، وهي أيضًا تستطيع إنتاج مُفوِّض قابل للتنفيذ، لكن بدلًا من توليد شيفرة IL (التي ستستدعي بدورها JIT)، فإنها تصرّف شجرة التعبيرات إلى قائمة تعليمات (instructions) تُنفَّذ بدورها على الآلة الافتراضية البسيطة الخاصة بالمُفسِّر. والمُفسِّر بسيط ومبني على مكدّس (stack-based)؛ فالتعليمات تُخرج القيم من المكدّس، وتنفّذ عملية، ثم تدفع النتيجة مجددًا إلى المكدّس. وكل تعليمة هي نسخة من صنف مشتق من Microsoft.Scripting.Interpreter.Instruction (مثل AddInstruction أو BranchTrueInstruction) له خصائص تصف كم عنصرًا يخرجه من المكدّس وكم عنصرًا سيدفعه عليه، وله طريقة Run() تنفّذ التعليمة بإخراج القيم ودفعها على المكدّس وإرجاع إزاحة التعليمة التالية. ويأخذ المُفسِّر قائمة التعليمات وينفّذها واحدةً تلو الأخرى، قافزًا إلى الأمام أو إلى الخلف بحسب القيمة التي تُرجعها طريقة Run().

وبعد أن تُنفَّذ قطعة الشيفرة عددًا معيّنًا من المرات، فإنها تُحوَّل إلى LambdaExpression كاملةً عبر استدعاء LightLambdaExpression.Reduce()، ثم تُصرَّف إلى مُفوِّض من نوع DynamicMethod (على خيط خلفي للحصول على قدر من التوازي)، ويُستبدل موقع استدعاء المُفوِّض القديم بالمُفوِّض الأحدث والأسرع. وهذا يقلّل إلى حد كبير تكلفة تنفيذ الدوال التي قد لا تُستدعى سوى بضع مرات، مثل الدالة الرئيسة في برنامج، فيما يجعل الدوال كثيرة الاستدعاء تعمل في أسرع وقت ممكن. وافتراضيًا، يُضبط عتبة التصريف عند 32 تنفيذًا، لكن يمكن تغيير ذلك بخيار على سطر الأوامر أو بواسطة البرنامج المُضيف، ويمكن أن يشمل ذلك تعطيل التصريف أو المُفسِّر كليًا.

سواء كان التنفيذ عبر المُفسِّر أو بالتصريف إلى IL، فإن عمليات اللغة ليست مكتوبة بشكل جامد (hard-coded) بواسطة مصرّف شجرة التعبيرات. وبدلًا من ذلك، يولّد المصرّف مواقع استدعاء لكل عملية قد تكون ديناميكية (وهي تقريبًا كل العمليات). وتمنح مواقع الاستدعاء هذه للكائنات فرصةً لتطبيق سلوك ديناميكي مع الإبقاء على الأداء مرتفعًا.

8.7. مواقع الاستدعاء الديناميكية

في لغة ‎.NET ساكنة، تُتَّخذ كل القرارات بشأن الشيفرة التي ينبغي استدعاؤها وقت الترجمة. فعلى سبيل المثال، خذ سطر C# التالي:

var z = x + y;

يعرف المصرّف نوعَي x' وy' وما إذا كان يمكن جمعهما أم لا. ويستطيع المصرّف أن يُصدر الشيفرة المناسبة للتعامل مع العوامل المُحمَّلة (overloaded operators) أو تحويلات الأنواع أو أي شيء آخر قد يلزم كي يعمل الشيفرة بصورة صحيحة، بالاعتماد حصريًا على المعلومات الساكنة التي يعرفها عن الأنواع المتدخّلة. والآن، خذ سطر شيفرة بايثون التالي:

z = x + y

لا يملك مصرّف IronPython أي فكرة عمّا قد يفعله هذا حين يصطدم به، لأنه لا يعرف نوعَي x وy، وحتى لو كان يعرفهما لكانت قدرة x وy على الجمع قد تتغيّر في وقت التشغيل على أي حال. (من حيث المبدأ يمكن أن تتغيّر، لكن لا IronPython ولا IronRuby يقومان بالاستدلال على الأنواع). وبدلًا من إصدار شيفرة IL لجمع الأرقام، يُصدر IronPython موقع استدعاء (call site) يُحلّ في وقت التشغيل.

موقع الاستدعاء هو عنصر نائب لعملية يُقرّر في وقت التشغيل؛ وهي منفَّذة كنسخ من الصنف System.Runtime.CompilerServices.CallSite. وفي لغة ديناميكية مثل Ruby أو Python، لدى كل عملية تقريبًا مكوّن ديناميكي؛ وهذه العمليات الديناميكية ممثَّلة في أشجار التعبيرات كعقد DynamicExpression، ويعرف مصرّف شجرة التعبيرات كيف يحوّلها إلى موقع استدعاء. وحين يُنشأ موقع استدعاء، فإنه لا يعرف بعد كيف يؤدّي العملية المطلوبة؛ غير أنه يُنشأ مع نسخة من مُصنِّف موقع الاستدعاء المناسب، وهو مُخصَّص للغة المستخدَمة، ويحتوي على كل المعلومات الضرورية عن كيفية تنفيذ العملية.

الشكل 8.1: مخطط أصناف CallSite

وستكون لكل لغة مُصنِّف موقع استدعاء مختلف لكل عملية، وغالبًا ما يعرف المُصنِّفات طرقًا كثيرة مختلفة لتنفيذ العملية بحسب الوسائط المُمرَّرة إلى موقع الاستدعاء. غير أن توليد هذه القواعد مكلف (ولا سيما تصريفها إلى مُفوِّض للتنفيذ، وهو ما يستدعي استدعاء ‎.NET JIT)، لذا يملك موقع الاستدعاء ذاكرة مؤقتة لمواقع الاستدعاء (call site cache) متعددة المستويات تخزّن القواعد التي أُنشئت بالفعل لاستخدامها لاحقًا.

الشكل 8.2: مخطط انسيابي لمواقع الاستدعاء CallSite

المستوى الأول، L0، هو الخاصية CallSite.Target الموجودة على نسخة موقع الاستدعاء نفسها. وهي تخزّن قاعدة الاستخدام الأحدث لموقع الاستدعاء هذا؛ فبالنسبة إلى عدد هائل من مواقع الاستدعاء، لن يحتاج هذا إلى أكثر من ذلك أبدًا، إذ لا تُستدعى إلا بمجموعة واحدة من أنواع الوسائط. ويمتلك موقع الاستدعاء ذاكرة مؤقتة أخرى، L1، تخزّن 10 قواعد إضافية. فإذا لم تكن Target صالحة لهذا الاستدعاء (مثلًا إذا اختلفت أنواع الوسائط)، فإن موقع الاستدعاء يفحص أولًا ذاكرته المؤقتة للقواعد ليرى ما إذا كان قد أنشأ المُفوِّض المناسب من استدعاء سابق، فيعيد استخدام تلك القاعدة بدلًا من إنشاء قاعدة جديدة.

وتخزين القواعد في الذاكرة المؤقتة تحكمه المدة التي يستغرقها تصريف قاعدة جديدة فعليًا مقارنةً بالمدة التي يستغرقها فحص القواعد الموجودة. وبصورة تقريبية، يستغرق ‎.NET نحو 10 نانوثانية لتنفيذ فحص نوع على متغيّر (وفحص دالة ثنائية يستغرق 20 نانوثانية، إلخ)، وهذا هو أكثر أنواع مسندات القواعد شيوعًا. أما تصريف دالة بسيطة لإضافة الأعداد المزدوجة، من جهة أخرى، فيستغرق نحو 80 µث، أي ثلاثة مراتب طولًا أطول. وحجم الذاكرات المؤقتة محدود لمنع إهدار الذاكرة بتخزين كل قاعدة تُستخدم عند موقع استدعاء؛ فبالنسبة إلى عملية جمع بسيطة، يحتاج كل تنويعة إلى نحو 1 كيلوبايت من الذاكرة. غير أن التنميط أظهر أن عددًا ضئيلًا جدًا من مواقع الاستدعاء تجاوزت 10 تنويعات في أي وقت.

وأخيرًا، هناك ذاكرة L2 المؤقتة، وهي مخزَّنة على نسخة المُصنِّف نفسها. وقد تحمل نسخة المُصنِّف المرتبطة بموقع استدعاء بعض المعلومات الإضافية التي تجعلها خاصة بهذا الموقع، لكن عددًا كبيرًا من مواقع الاستدعاء لا تتميّز بأي شيء ويمكن أن تتشارك نسخة المُصنِّف نفسها. فعلى سبيل المثال، في بايثون، القواعد الأساسية للجمع واحدة في أنحاء البرنامج كلها؛ فالمتوقف عند نوعَي الطرفين على جانبي +، لا شيء غير ذلك. ويمكن لعمليات الجمع جميعها في البرنامج أن تتشارك المُصنِّف نفسه، وإذا أخفقت الذاكرتان L0 وL1 معًا، فتحتوي الذاكرة L2 على عدد أكبر بكثير من القواعد الحديثة (128) مجموعة من أنحاء البرنامج كلها. وحتى لو كان موقع استدعاء في تنفيذه الأول، فهناك احتمال جيد أن يجد قاعدة مناسبة في الذاكرة L2. ولضمان أن يعمل هذا بأقصى فعالية، يملك IronPython وIronRuby كليهما مجموعة من نسخ المُصنِّفات المعيارية (canonical) المستخدَمة في العمليات الشائعة كـ الجمع.

وإذا أخفقت الذاكرة L2، يُطلب من المُصنِّف إنشاء تنفيذ (implementation) لموقع الاستدعاء، مراعيًا أنواع الوسائط (وربما قيمها كذلك). وفي المثال أعلاه، إذا كان x وy من النوع double (أو نوع أصلي آخر)، فإن التنفيذ يقوم ببساطة بصبّهما إلى double ثم يستدعي تعليمة IL add. كما ينتج المُصنِّف اختبارًا يفحص الوسائط ويتأكد من صلاحيتها للتنفيذ. ويشكّل التنفيذ والاختبار معًا قاعدة. وفي معظم الحالات، يُنشَأ التنفيذ والاختبار معًا ويُخزَّنان كشجرتَي تعبير. (لكن بنية موقع الاستدعاء لا تعتمد على أشجار التعبيرات؛ إذ يمكن استخدامها مع المُفوِّضات وحدها.)

لو كانت أشجار التعبيرات قد كُتبت بلغة C#، لكانت على نحو ما يلي:

if(x is double && y is double) {       // check for doubles
      return (double)x + (double)y;    // execute if doubles
 }
 return site.Update(site, x, y);       // not doubles, so find/create another rule 
                                       // for these types

ينتج المُصنِّف بعد ذلك مُفوِّضًا من أشجار التعبيرات، ما يعني أن القاعدة تُصرَّف إلى IL ثم إلى شيفرة آلة. وفي حالة جمع رقمين، من المرجَّح أن يتحوّل ذلك إلى فحص نوع سريع ثم إلى تعليمة آلة تضيف الرقمين. وحتى مع كل الآليات المنخرطة، فإن النتيجة النهائية أبطأ بشكل هامشي فقط من الشيفرة الساكنة. ويضمّ IronPython وIronRuby أيضًا مجموعة من القواعد المُصرَّفة مسبقًا للعمليات الشائعة كجمع الأنواع الأولية، مما يوفّر الوقت لأنها لا يلزم إنشاءها في وقت التشغيل، لكنه يكلّف بعض المساحة الإضافية على القرص.

8.8. بروتوكول الكائنات الوصفية

إلى جانب بنية اللغة، فإن الجزء الجديد الأهم من DLR هو قدرة اللغة (وهي لغة المُضيف (host language)) على إجراء استدعاءات ديناميكية على كائنات مُعرَّفة في لغة أخرى (وهي لغة المصدر (source language)). ولجعل ذلك ممكنًا، يجب أن يفهم DLR ما إذا كانت العمليات صالحة على كائن بعينه، بغضّ النظر عن اللغة التي كُتب بها. ويمتلك Python وRuby نموذجَي كائنات متشابهين إلى حد كبير، لكن JavaScript يملك نظام أنواع مختلفًا جذريًا قائمًا على النماذج الأولية (prototype-based) في مقابل القائم على الأصناف (class-based). وبدلًا من محاولة توحيد أنظمة الأنواع المختلفة، فإن DLR يتعامل معها جميعًا وكأنها قائمة على تمرير الرسائل بأسلوب Smalltalk.

في نظام موجَّه نحو الكائنات قائم على تمرير الرسائل، ترسل الكائنات رسائل إلى كائنات أخرى (عادةً مع وسائط)، ويمكن للكائن أن يُعيد كائنًا آخر كنتيجة. وبذلك، وبينما لكل لغتها تصور خاص عمّا هو الكائن، يمكن جعلها جميعًا متكافئة تقريبًا بالنظر إلى استدعاءات الدوال بوصفها رسائل تُرسَل بين الكائنات. وبالطبع، حتى اللغات الموجَّهة نحو الكائنات الساكنة تندرج في هذا النموذج إلى حد ما؛ وما يجعل اللغات الديناميكية مختلفة هو أن الدالة المستدعاة لا يلزم أن تكون معروفة وقت الترجمة، ولا حتى أن تكون موجودة على الكائن أصلًا (مثل method_missing في Ruby)، وأن الكائن الهدف عادةً ما تتاح له فرصة اعتراض الرسالة ومعالجتها بطريقة مختلفة عند الحاجة (مثل __getattr__ في Python).

يعرّف DLR الرسائل التالية:

  • {Get|Set|Delete}Member: عمليات للتعامل مع أعضاء كائن
  • {Get|Set|Delete}Index: عمليات للكائنات المفهرَسة (مثل المصفوفات أو القواميس)
  • Invoke, InvokeMember: استدعاء كائن أو عضو في كائن
  • CreateInstance: إنشاء نسخة من كائن
  • Convert: تحويل كائن من نوع إلى آخر
  • UnaryOperation, BinaryOperation: تنفيذ عمليات قائمة على العوامل، مثل النفي (!) أو الجمع (+)

ومعًا، ينبغي أن تكون هذه العمليات كافية لتنفيذ نموذج كائنات أي لغة تقريبًا.

ولأن CLR مُنمَّط ساكنًا بطبيعته، فلا بدّ من أن تظل كائنات اللغات الديناميكية ممثَّلة بأصناف ساكنة. والتقنية المعتادة هي توفير صنف ساكن مثل PythonObject وجعل كائنات بايثون الفعلية نسخًا من هذا الصنف أو من أصنافه المشتقّة. ولأسباب تتعلق بالتكامل والأداء، فإن آلية DLR أعقد بكثير. فبدلًا من التعامل مع كائنات خاصة بالغة، يتعامل DLR مع الكائنات الوصفية (meta-objects)، وهي أصناف مشتقّة من System.Dynamic.DynamicMetaObject ولها طرق للتعامل مع كل الرسائل السابقة. ولكل لغة أصنافها المشتقّة الخاصة من DynamicMetaObject التي تنفّذ نموذج كائنات تلك اللغة، مثل MetaPythonObject في IronPython. كما أن الأصناف الوصفية لها أصناف خرسانية مقابلة تنفّذ واجهة System.Dynamic.IDynamicMetaObjectProtocol، وهي الطريقة التي يتعرّف بها DLR على الكائنات الديناميكية.

الشكل 8.3: مخطط أصناف IDMOP

ومن صنف ينفّذ IDynamicMetaObjectProtocol، يستطيع DLR الحصول على DynamicMetaObject عبر استدعاء GetMetaObject(). ويوفّر هذا DynamicMetaObject من اللغة، وينفّذ دوال الربط التي يتطلبها ذلك الكائن. ويحمل كل DynamicMetaObject أيضًا قيمة ونوع الكائن الأساسي، إن كانا متاحَين. وأخيرًا، يخزّن DynamicMetaObject شجرة تعبيرات تمثّل موقع الاستدعاء حتى تلك اللحظة، وأي قيود على ذلك التعبير، على نحو شبيه بالمُصنِّفات لمواقع الاستدعاء.

وحين يصرّف DLR استدعاءَ دالة على صنف مُعرَّف من المستخدم، فإنه ينشئ أولًا موقع استدعاء (أي نسخة من الصنف CallSite). ويبدأ موقع الاستدعاء عملية الربط على النحو الموصوف أعلاه في قسم «مواقع الاستدعاء الديناميكية»، مما ينتهي به الأمر إلى استدعاء GetMetaObject() على نسخة من OldInstance، وهي تُعيد MetaOldInstance. (ولدى Python أصناف بالنمط القديم وأصناف بالنمط الجديد، لكن ذلك ليس ذا صلة هنا.) بعد ذلك، يُستدعى مُصنِّف (PythonGetMemberBinder.Bind())، الذي يستدعي بدوره MetaOldInstance.BindGetMember()؛ وهو يُعيد DynamicMetaObject جديدًا يصف كيفية البحث عن اسم الدالة على الكائن. ثم يُستدعى مُصنِّف آخر، وهو PythonInvokeBinder.Bind()، فيستدعي MetaOldInstance.BindInvoke()، ويغلّف DynamicMetaObject الأول بأخر يمثّل كيفية استدعاء الدالة التي جرى البحث عنها. ويشمل ذلك الكائن الأصلي، وشجرة التعبيرات الخاصة بالبحث عن اسم الدالة، وDynamicMetaObjects تمثّل وسائط الدالة.

وبعد أن يُبنى DynamicMetaObject الأخير في تعبير ما، تُستخدم شجرة تعبيراته وقيوده لبناء مُفوِّض يُعاد بعد ذلك إلى موقع الاستدعاء الذي بدأ عملية الربط. ومن هناك يمكن تخزين الشيفرة في الذاكرات المؤقتة لمواقع الاستدعاء، مما يجعل العمليات على الكائنات في سرعة الاستدعاءات الديناميكية الأخرى، وقريبة جدًا من سرعة الاستدعاءات الساكنة.

وعلى اللغات المُضيف التي تريد تنفيذ عمليات ديناميكية على اللغات الديناميكية أن تشتقّ مُصنِّفاتها من DynamicMetaObjectBinder. وسيسأل DynamicMetaObjectBinder أولًا الكائن الهدف أن يربط العملية (عبر استدعاء GetMetaObject() والمرور بعملية الربط الموصوفة أعلاه) قبل الرجوع إلى دلالات الربط الخاصة بلغة المُضيف. ونتيجةً لذلك، إذا كان كائن IronRuby يُستدعى من برنامج IronPython، فإن الربط يُجرَّب أولًا بدلالات Ruby (اللغة الهدف)؛ وإن أخفق ذلك، فإن DynamicMetaObjectBinder يرجع إلى دلالات Python (لغة المُضيف). وإذا كان الكائن الجاري ربطه غير ديناميكي (أي أنه لا ينفّذ IDynamicMetaObjectProvider)، مثل أصناف من مكتبة الأصناف الأساسية في ‎.NET، فإنه يُستدعى بدلالات لغة المُضيف باستخدام انعكاس ‎.NET (reflection).

ول للغات بعض حرية في كيفية تنفيذ ذلك؛ فـ PythonInvokeBinder في IronPython لا تشتقّ من InvokeBinder لأنها تحتاج إلى معالجة إضافية خاصة بكائنات بايثون. وطالما أنها لا تتعامل إلا مع كائنات بايثون فلا مشكلة؛ أما إذا صادفت كائنًا ينفّذ IDynamicMetaObjectProvider وليس كائن بايثون، فإنها تحوّل الطلب إلى صنف CompatibilityInvokeBinder الذي يرث فعلًا من InvokeBinder ويستطيع التعامل مع الكائنات الأجنبية على نحو صحيح.

وإذا تعذّر على الآلية الاحتياطية ربط العملية، فإنها لا ترمي استثناءً؛ وبدلًا من ذلك فإنها تُعيد DynamicMetaObject يمثّل الخطأ. عندئذٍ يتولّى مُصنِّف لغة المُضيف معالجة ذلك بالطريقة المناسبة للغة المُضيف؛ فعلى سبيل المثال، قد يُعيد الوصول إلى عضو مفقود في كائن IronPython من تنفيذ افتراضي لـ JavaScript القيمة undefined، بينما تنفيذ الشيء نفسه على كائن JavaScript من IronPython سيرفع AttributeError.

وقدرتُ اللغات على العمل مع الكائنات الديناميكية عديمة الفائدة إلى حد كبير ما لم تكن هناك القدرة أولًا على تحميل شيفرة مكتوبة بلغات أخرى وتنفيذها، وهو ما يوفّر له DLR آلية مشتركة لاستضافة اللغات الأخرى.

8.9. الاستضافة

إلى جانب توفير تفاصيل تنفيذ اللغات المشتركة، يوفّر DLR أيضًا واجهة استضافة مشتركة. وتُستخدم واجهة الاستضافة من قِبل لغة المُضيف (عادةً لغة ساكنة مثل C#) لتنفيذ شيفرة مكتوبة بلغة أخرى مثل Python أو Ruby. وهذه تقنية شائعة تتيح للمستخدمين النهائيين توسيع تطبيق ما، ويأخذ DLR خطوة أبعد بجعله أمرًا تافهًا لاستخدام أي لغة برمجة نصّية (scripting) لها تنفيذ على DLR. وهناك أربعة أجزاء رئيسية في واجهة الاستضافة: وقت التشغيل (runtime)، والمحرّكات (engines)، والمصادر (sources)، والنطاقات (scopes).

يُشارَك ScriptRuntime عادةً بين جميع اللغات الديناميكية في التطبيق. ويتولى وقت التشغيل كل مراجع التجميعات الحالية التي تُقدَّم إلى اللغات المحمّلة، ويوفّر طرائق للتنفيذ السريع لملف، ويوفّر طرائق لإنشاء محرّكات جديدة. وفي مهام البرمجة النصّية البسيطة، لا يلزم سوى واجهة وقت التشغيل، غير أن DLR يوفّر أيضًا أصنافًا تمنح تحكّمًا أكبر في كيفية تشغيل السكربتات.

عادةً ما يُستخدم ScriptEngine واحد فقط لكل لغة برمجة نصّية. ويعني بروتوكول الكائنات الوصفية في DLR أن بإمكان برنامج تحميل سكربتات من لغات متعددة، وأن الكائنات التي تنشئها كل لغة يمكنها جميعًا أن تتفاعل بسلاسة. ويغلّف المحرّك LanguageContext خاصًا باللغة (مثل PythonContext أو RubyContext)، ويُستخدم لتنفيذ شيفرة من ملفات أو نصوص، ولإجراء عمليات على كائنات ديناميكية من لغات لا تدعم DLR أصلًا (مثل C# قبل ‎.NET 4). والمحرّكات آمنة الخيوط (thread-safe)، ويمكنها تنفيذ سكربتات متعددة على التوازي، ما دام لكل خيط نطاقه الخاص. كما توفّر طرائق لإنشاء مصادر السكربتات، ما يتيح تحكّمًا أدقّ في تنفيذ السكربتات.

يحمل ScriptSource مقطعًا من الشيفرة مُقرَّرًا للتنفيذ؛ فهو يربط كائن SourceUnit — الذي يحوي الشيفرة الفعلية — بـ ScriptEngine الذي أنشأ المصدر. ويتيح هذا الصنف تجميع الشيفرة (وهي عملية تنتج كائن CompiledCode يمكن تخزينه مؤقتًا) أو تنفيذها مباشرةً. فإذا كان مقطع شيفرة سيُنفَّذ على نحو متكرر، فمن الأفضل تصريفه أولًا ثم تنفيذ الشيفرة المصروفة؛ أما السكربتات التي لن تُنفَّذ سوى مرة واحدة، فمن الأفضل تنفيذها مباشرةً.

وأخيرًا، أيًّا كانت الطريقة التي يصل بها الشيفرة إلى التنفيذ، يجب توفير ScriptScope كي تُنفَّذ فيه الشيفرة. ويُستخدم النطاق للاحتفاظ بجميع متغيّرات السكربت، ويمكن أن يُحمَّل مسبقًا بمتغيّرات من المُضيف عند الحاجة. وهذا يتيح للمُضيف توفير كائنات مخصّصة للسكربت عند بدء تشغيله—فمثلًا قد يوفّر محرّر صور طريقةً للوصول إلى بكسلات الصورة التي يعمل عليها السكربت. وبعد أن ينفَّذ السكربت، يمكن قراءة أي متغيّرات أنشأها من النطاق. أمّا الاستخدام الرئيسي الآخر للنطاقات فهو توفير العزل، بحيث يمكن تحميل عدة سكربتات وتنفيذها في الوقت نفسه دون أن تتداخل مع بعضها.

ومن المهمّ ملاحظة أن جميع هذه الأصناف يوفّرها DLR لا اللغة نفسها؛ فـ LanguageContext المستخدم في المحرّك وحده هو ما يأتي من تنفيذ اللغة. ويوفّر سياق اللغة كل الوظائف—تحميل الشيفرة، وإنشاء النطاقات، والتصريف، والتنفيذ، والعمليات على الكائنات الديناميكية—التي يحتاجها المُضيف، وتوفّر أصناف الاستضافة في DLR واجهةً أكثر قابلية للاستخدام للوصول إلى تلك الوظائف. ولهذا السبب، يمكن استخدام شيفرة الاستضافة نفسها لاستضافة أي لغة مبنية على DLR.

وبالنسبة إلى تنفيذات اللغات الديناميكية المكتوبة بلغة C (مثل Python وRuby الأصليَّين)، يجب كتابة شيفرة غلاف خاصة للوصول إلى الشيفرة غير المكتوبة باللغة الديناميكية، ويجب تكرارها لكل لغة برمجة نصّية مدعومة. وبينما توجد برمجيات مثل SWIG لتسهيل ذلك، فإن إضافة واجهة برمجة نصّية بالبايثون أو بالـ Ruby إلى برنامج وكشف نموذج كائناته للتعامل من قبل السكربتات الخارجية ليس بالأمر التافه. أما في برامج ‎.NET، من جهة أخرى، فإن إضافة البرمجة النصّية بسيطة مثل تهيئة وقت تشغيل، وتحميل تجميعات البرنامج في وقت التشغيل، واستخدام ScriptScope.SetVariable() لجعل كائنات البرنامج متاحةً للسكربتات. ويمكن إضافة دعم البرمجة النصّية إلى تطبيق ‎.NET في دقائق معدودة، وهذه ميزة هائلة من مزايا DLR.

8.10. تنظيم التجميعات

ولأن DLR تطوّر من مكتبة منفصلة إلى جزء من CLR، فإن هناك أجزاء موجودة في CLR (مواقع الاستدعاء، وأشجار التعبيرات، والمُصنِّفات، وتوليد الشيفرة، والكائنات الوصفية الديناميكية)، وأجزاء أخرى تنتمي إلى مشروع IronLanguages مفتوح المصدر (الاستضافة، والمُفسِّر، وبعض الأجزاء الأخرى التي لم نناقشها هنا). والأجزاء الموجودة في CLR مضمَّنة أيضًا في مشروع IronLanguages ضمن Microsoft.Scripting.Core. وتنقسم أجزاء DLR إلى تجميعين، وهما Microsoft.Scripting وMicrosoft.Dynamic—يحتوي الأول على واجهات برمجة الاستضافة، ويحتوي الثاني على شيفرة للتكامل مع COM، والمُفسِّر، وبعض الأجزاء الأخرى المشتركة بين اللغات الديناميكية.

وتنقسم اللغات نفسها إلى قسمين أيضًا: فـ IronPython.dll وIronRuby.dll ينفّذان اللغات نفسها (المحلّلات، والمُصنِّفات، إلخ)، بينما ينفّذ IronPython.Modules.dll وIronRuby.Libraries.dll الأجزاء من المكتبة القياسية المنفَّذة بلغة C في تنفيذي Python وRuby الكلاسيكيَّين.

8.11. الدروس المستفادة

DLR مثال مفيد على منصّة محايدة اللغة للغات الديناميكية مبنيّة فوق وقت تشغيل ساكن. والتقنيات التي تستخدمها لتحقيق شيفرة ديناميكية عالية الأداء صعبة التنفيذ على نحو سليم، لذا تلتقط DLR هذه التقنيات وتُتيحها لكل تنفيذ للغة ديناميكية.

وIronPython وIronRuby مثالان جيّدان على كيفية بناء لغة فوق DLR. والتنفيذان متشابهان جدًا لأنهما طُوِّرا في الوقت نفسه بواسطة فريقين متقاربين، ومع ذلك فلا تزال بينهما فروق جوهرية في التنفيذ. ولأن عدّة لغات مختلفة قد تطوّرت بالتوازي (IronPython، وIronRuby، ونموذج أوّلي لـ JavaScript، وVBx الغامض—وهي نسخة VB ديناميكية بالكامل)، إضافةً إلى المزايا الديناميكية في C# وVB، فقد ضمن ذلك أن تصميم DLR خضع لاختبار وافر أثناء التطوير.

أما التطوير الفعلي لـ IronPython وIronRuby وDLR فكان يُدار على نحو مختلف جدًا لمعظم مشاريع مايكروسوفت في ذلك الوقت—كان نموذج تطوير رشيقًا وتكراريًا إلى حد بعيد، مع تكامل مستمر (continuous integration) يعمل منذ اليوم الأول. وهذا مكّنها من التغيير سريعًا كلما اضطُررت إلى ذلك، وكان أمرًا حسنًا لأن DLR ارتبطت مبكرًا في تطورها بمزايا C# الديناميكية. وبينما اختبارات DLR سريعة جدًا ولا تستغرق سوى اثنتي عشرة ثانية تقريبًا، فإن اختبارات اللغات تستغرق وقتًا طويلًا أكثر مما ينبغي (تستغرق مجموعة اختبارات IronPython نحو 45 دقيقة، حتى مع التنفيذ على التوازي)؛ وكان تحسين ذلك قد رفع سرعة التكرار. وفي نهاية المطاف، تقاطعت هذه التكرارات على تصميم DLR الحالي، الذي يبدو مفرط التعقيد في بعض أجزائه، لكنه يتّسق مع بعضه الآخر إلى حد كبير.

وكان ارتباط DLR بـ C# بالغ الأهمية لأنه ضمن لها مكانًا و«غرضًا»، لكن ما إن أُنجزت مزايا C# الديناميكية تغيّر المناخ السياسي (مصادفًا لركود اقتصادي) وفقدت لغات Iron دعمها داخل الشركة. فواجهات برمجة الاستضافة، على سبيل المثال، لم تدخل ‎.NET Framework قط (ومن المرجَّح جدًا ألّا تدخل أبدًا)؛ وهذا يعني أن PowerShell 3، المبني هو الآخر على DLR، يستخدم مجموعة مختلفة تمامًا من واجهات برمجة الاستضافة عن IronPython وIronRuby، وإن كانت كل كائناتها تظل قادرة على التفاعل على النحو الموصوف أعلاه. (وانتقل بعض أعضاء فريق DLR لاحقًا إلى العمل على مكتبة مُصرِّف C# كخدمة باسمها الرمزي «Roslyn»، وهي تشبه إلى حد لافت واجهات برمجة الاستضافة في IronPython وIronRuby). لكن، بفضل عجب الترخيص مفتوح المصدر، فستستمر في البقاء بل وفي الازدهار.