ملفات ‎.mpy‎ في MicroPython

يُعرّف MicroPython مفهوم ملف ‎.mpy‎ وهو صيغة ملف حاوية ثنائية تحتوي على شيفرة مُصرّفة مسبقًا، ويمكن استيراده مثل أي وحدة ‎.py‎ عادية. يمكن استيراد الملف foo.mpy عبر import foo طالما أمكن العثور على foo.mpy بالطريقة المعتادة من خلال آلية الاستيراد. وعادةً يُبحث في كل دليل مُدرَج في sys.path بالترتيب. وعند البحث في دليل معيّن يُبحث أولًا عن foo.py فإن لم يُعثر عليه يُبحث عن foo.mpy، ثم يستمر البحث في الدليل التالي إن لم يُعثر على أي منهما. وبهذا تكون الأولوية لـ foo.py على foo.mpy.

يمكن أن تحتوي ملفات ‎.mpy‎ هذه على bytecode الذي يُولَّد عادةً من ملفات مصدر Python (ملفات ‎.py‎) عبر برنامج mpy-cross. وبالنسبة لبعض المعماريات يمكن أن يحتوي ملف ‎.mpy‎ أيضًا على شيفرة آلة أصلية، يمكن توليدها بطرق متنوعة، أبرزها من شيفرة مصدر C.

مُصرِّف mpy-cross

mpy-cross هو المُصرِّف العابر الذي يحوّل ملف المصدر .py إلى حاوية ثنائية .mpy جاهزة للاستيراد على الكاميرا. وهو جزء من شجرة مصدر MicroPython (نفسها المستخدمة لبناء البرنامج الثابت للكاميرا)، ويُنشر أيضًا كحزمة pip للاستخدام على الجهاز المضيف دون الحاجة إلى استنساخ البرنامج الثابت بالكامل:

$ pip install --user mpy-cross

أو عبر pipx:

$ pipx install mpy-cross

بمجرد التثبيت، شغّله على ملف مصدر واحد:

$ mpy-cross foo.py

ينتج عن هذا foo.mpy في الدليل الحالي، جاهزًا لنسخه إلى نظام ملفات الكاميرا إلى جانب الوحدات الأخرى أو لتغذيته إلى صورة ROMFS.

أكثر خيارات سطر الأوامر فائدةً:

  • -o <path> -- مسار الإخراج للملف .mpy المُولَّد (يكون افتراضيًا اسم ملف الإدخال مع استبدال الامتداد؛ ‏-o - يكتب إلى stdout).

  • -O<n> -- مستوى التحسين من 0 إلى 3. القيمة الافتراضية 0 تحافظ على التأكيدات ومواقع المصدر الكاملة؛ بينما 3 يزيل التأكيدات وسلاسل التوثيق ويعيد كتابة كتل if __debug__. ويتحكم المستوى في نفس سطح micropython.opt_level الذي يكشفه وقت التشغيل.

  • -march=<arch> -- المعمارية الأصلية الهدف للدوال المُزخرفة بـ @native و @viper. وهو مطلوب عندما يستخدم المصدر تلك المزخرِفات. ويجب أن تطابق القيمة فئة وحدة المعالجة الدقيقة للكاميرا: اخترها من القائمة التي يطبعها mpy-cross --help، أو اقرأها من الكاميرا في وقت التشغيل عبر sys.implementation._mpy.

  • -s <path> -- سلسلة مسار المصدر المضمَّنة في معلومات التصحيح الخاصة بملف .mpy. مفيد عندما يختلف المسار على القرص عن مسار الاستيراد الذي ينبغي أن يظهر تحته الملف في تتبعات الأخطاء.

  • -X emit=bytecode|native|viper -- اختر المُصدِّر الافتراضي للوحدة بأكملها (بديل على مستوى الدالة عن المزخرِفات @native / @viper).

  • --version -- يطبع نسخة صيغة .mpy التي يصدرها هذا الملف الثنائي. ويجب أن يطابق هذا الرقم النسخة التي يدعمها وقت تشغيل الكاميرا (انظر جدول الإصدارات أدناه) وإلا سيرفع الاستيراد ValueError('incompatible .mpy file').

شغّل mpy-cross --help للحصول على قائمة الرايات الكاملة.

تكشف حزمة pip أيضًا واجهة برمجة وحدة Python صغيرة بحيث يمكن لبرامج البناء النصية تشغيل المُصرِّف داخل العملية بدلًا من تفريع عملية فرعية يدويًا:

import mpy_cross

mpy_cross.compile('foo.py', dest='build/foo.mpy', opt=3,
                  march=mpy_cross.NATIVE_ARCH_ARMV7EMSP)

mpy_cross.compile و mpy_cross.run و mpy_cross.mpy_version هي نقاط الدخول الثلاث؛ بينما يحمل mpy_cross.CrossCompileError خرج الخطأ القياسي (stderr) للمُصرِّف عند حدوث خطأ ما. وتطابق ثوابت المعمارية (NATIVE_ARCH_ARMV7EMSP و NATIVE_ARCH_ARMV7EMDP وغيرها) السلاسل التي تقبلها الراية -march.

الإصدارات والتوافق لملفات ‎.mpy‎

قد يكون ملف ‎.mpy‎ معيّن متوافقًا أو غير متوافق مع نظام MicroPython معيّن. ويستند التوافق إلى ما يلي:

  • نسخة ملف ‎.mpy‎: يجب أن تطابق نسخة الملف النسخة التي يدعمها النظام الذي يحمّله.

  • النسخة الفرعية لملف ‎.mpy‎: إذا كان ملف ‎.mpy‎ يحتوي على شيفرة آلة أصلية فيجب أن تطابق النسخة الفرعية للملف النسخة التي يدعمها النظام الذي يحمّله. أما إن لم تكن هناك شيفرة آلة أصلية في ملف ‎.mpy‎ فيُتجاهل رقم النسخة الفرعية عند التحميل.

  • بتات العدد الصحيح الصغير: سيتطلب ملف ‎.mpy‎ عددًا أدنى من البتات في small integer ويجب أن يدعم النظام الذي يحمّله هذا العدد من البتات على الأقل.

  • المعمارية الأصلية: إذا كان ملف ‎.mpy‎ يحتوي على شيفرة آلة أصلية فسيحدد معمارية تلك الشيفرة الآلية ويجب أن يدعم النظام الذي يحمّله تنفيذ شيفرة تلك المعمارية.

إذا كان نظام MicroPython يدعم استيراد ملفات ‎.mpy‎ فسيكون الحقل sys.implementation._mpy موجودًا ويعيد عددًا صحيحًا يُرمّز النسخة (البتات الثمانية الدنيا) والميزات والمعمارية الأصلية.

محاولة استيراد ملف ‎.mpy‎ يفشل في أحد الاختبارات الأربعة الأولى سترفع ValueError('incompatible .mpy file'). ومحاولة استيراد ملف ‎.mpy‎ يفشل في اختبار المعمارية الأصلية (إن كان يحتوي على شيفرة آلة أصلية) سترفع ValueError('incompatible .mpy arch').

إذا فشل استيراد ملف ‎.mpy‎ فجرّب ما يلي:

  • حدّد نسخة ‎.mpy‎ والرايات التي يدعمها نظام MicroPython لديك بتنفيذ:

    import sys
    sys_mpy = sys.implementation._mpy
    arch = [None, 'x86', 'x64',
        'armv6', 'armv6m', 'armv7m', 'armv7em', 'armv7emsp', 'armv7emdp',
        'xtensa', 'xtensawin', 'rv32imc', 'rv64imc'][(sys_mpy >> 10) & 0x0F]
    print('mpy version:', sys_mpy & 0xff)
    print('mpy sub-version:', sys_mpy >> 8 & 3)
    print('mpy flags:', end='')
    if arch:
        print(' -march=' + arch, end='')
    if (sys_mpy >> 16) != 0:
        print(' -march-flags=' + (sys_mpy >> 16), end='')
    print()
    
  • تحقق من صحة ملف ‎.mpy‎ بفحص أول بايتين من الملف. ينبغي أن يكون البايت الأول حرف 'M' كبيرًا، وأن يكون البايت الثاني رقم النسخة، الذي ينبغي أن يطابق نسخة النظام أعلاه. فإن لم يطابق فأعد بناء ملف ‎.mpy‎.

  • تحقق مما إذا كانت نسخة ‎.mpy‎ للنظام تطابق النسخة التي يصدرها mpy-cross الذي استُخدم لبناء ملف ‎.mpy‎، والتي تُعرف عبر mpy-cross --version. فإن لم تطابق فأعد تصريف mpy-cross من مستودع Git المستنسخ عند الوسم (أو الهاش) الذي يبلّغ عنه mpy-cross --version.

  • تأكد من أنك تستخدم رايات mpy-cross الصحيحة، التي تُعرف عبر الشيفرة أعلاه، أو بفحص متغير Makefile باسم MPY_CROSS_FLAGS للمنفذ (port) الذي تستخدمه.

  • إذا كان البايت الثالث من ملف ‎.mpy‎ يحمل البت رقم #6 مضبوطًا، فتحقق مما إذا كانت بتات الراية vuint الخاصة بالمعمارية المُرمَّزة متوافقة مع الهدف الذي تستورد الملف عليه.

يوضح الجدول التالي التطابق بين إصدار MicroPython ونسخة ‎.mpy‎.

إصدار MicroPython

نسخة ‎.mpy‎

v1.23.0 وما فوق

6.3

v1.22.x

6.2

v1.20 - v1.21.0

6.1

v1.19.x

6

v1.12 - v1.18

5

v1.11

4

v1.9.3 - v1.10

3

v1.9 - v1.9.2

2

v1.5.1 - v1.8.7

0

وللاكتمال، يوضح الجدول التالي إيداع Git في مستودع MicroPython الرئيسي الذي تغيّرت عنده نسخة ‎.mpy‎.

تغيّر نسخة ‎.mpy‎

إيداع Git

6.2 إلى 6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 إلى 6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 إلى 6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 إلى 6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 إلى 5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 إلى 4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 إلى 3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 إلى 2

dd11af209d226b7d18d5148b239662e30ed60bad

0 إلى 1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

النسخة الأولية 0

d8c834c95d506db979ec871417de90b7951edc30

الترميز الثنائي لملفات ‎.mpy‎

ملفات ‎.mpy‎ في MicroPython هي صيغة حاوية ثنائية تحتوي على كائنات شيفرة (شيفرة بايت وشيفرة آلة أصلية) مخزّنة داخليًا في تسلسل هرمي متداخل. تُخزَّن شيفرة الوحدة الخارجية أولًا، ثم تتبعها فروعها. وقد يكون لكل فرع فروع إضافية، مثلًا في حالة صنف له توابع، أو دالة تعرّف lambda أو comprehension. وللحفاظ على صغر الملفات مع توفير نطاق واسع من القيم الممكنة، يستخدم مفهوم العدد الصحيح غير المُوقَّع المُرمَّز بصورة متغيرة (vuint) في عدة مواضع. وعلى غرار ترميز UTF-8، يخزّن هذا الترميز 7 بتات لكل بايت مع ضبط البت الثامن (الأكثر دلالة، MSB) إذا تبعه بايت واحد أو أكثر. وتُخزَّن بتات العدد الصحيح غير المُوقَّع في الـ vuint بصيغة الأقل دلالة أولًا (LSB).

يتكوّن المستوى الأعلى لملف ‎.mpy‎ من ثلاثة أجزاء:

  • الترويسة.

  • جداول qstr العامة والثوابت العامة.

  • الشيفرة الخام للنطاق الخارجي للوحدة. ويُنفَّذ هذا النطاق الخارجي عند استيراد ملف ‎.mpy‎.

يمكنك فحص محتويات ملف ‎.mpy‎ باستخدام mpy-tool.py، على سبيل المثال (يُشغَّل من جذر مستودع MicroPython الرئيسي):

$ ./tools/mpy-tool.py -xd myfile.mpy

الترويسة

ترويسة ‎.mpy‎ هي:

الحجم

الحقل

بايت

القيمة 0x4d (الحرف 'M' في ASCII)

بايت

رقم النسخة الرئيسية لـ ‎.mpy‎

بايت

رايات الميزات، المعمارية الأصلية، رقم النسخة الفرعية (كانت رايات الميزات في النسخ الأقدم)

بايت

عدد البتات في عدد صحيح صغير

البايت الثالث مقسّم كما يلي (الأكثر دلالة أولًا):

البت

المعنى

7

محجوز، يجب أن يكون 0

6

تتبع الترويسةَ راية vuint خاصة بالمعمارية

5..2

رقم المعمارية الأصلية

1..0

رقم النسخة الفرعية

الرايات الخاصة بالمعمارية

إذا كان البت #6 من بايت رايات الميزات في الترويسة مضبوطًا، فستتبع الترويسةَ راية vuint تحتوي على معلومات اختيارية خاصة بالمعمارية. وتعتمد محتويات هذا العدد الصحيح على المعمارية الأصلية التي يستهدفها الملف.

يُستخدم هذا حاليًا لتخزين امتدادات معالج RISC-V التي يحتاجها ملف MPY ليعمل بصورة صحيحة، بخلاف I و M و C و Zicsr. وتُحدَّد النكهات المختلفة من ArmV7 برقم معماريتها الأصلية، لكن إعادة استخدام تلك الآلية ستُعقّد الأمور بالنسبة إلى RV32 و RV64.

ملفات MPY التي تستهدف RV32 أو RV64 ولا تحتاج أي امتدادات معالج معيّنة لا تحتاج إلى توفير عدد صحيح للرايات (إلى جانب ضبط البت المناسب في الترويسة). ويُستخدم غياب قيمة رايات لملفات MPY الخاصة بـ RV32 و RV64 للدلالة على عدم الحاجة إلى أي امتدادات معيّنة، ويوفّر بايتًا واحدًا في الملف الثنائي النهائي.

انظر أيضًا خيار سطر الأوامر -march-flags في كل من mpy-tool.py و mpy-cross، وخيار سطر الأوامر --arch-flags في mpy_ld.py لضبط هذه القيمة عند إنشاء ملفات MPY.

جداول qstr العامة والثوابت العامة

يحتوي ملف ‎.mpy‎ على جدول qstr واحد، وجدول كائنات ثوابت واحد. وهذان عامّان لملف ‎.mpy‎، وتشير إليهما جميع كائنات الشيفرة الخام المتداخلة. ويربط جدول qstr رقم qstr الداخلي (الداخلي لملف ‎.mpy‎) برقم qstr المُحلَّل لوقت التشغيل الذي يُستورَد إليه ملف ‎.mpy‎. وهذا يربط ملف ‎.mpy‎ ببقية النظام الذي يُنفَّذ داخله. ويُملأ جدول كائنات الثوابت بالإشارات إلى جميع كائنات الثوابت التي يحتاجها ملف ‎.mpy‎.

الحجم

الحقل

vuint

عدد سلاسل qstr

vuint

عدد كائنات الثوابت

...

بيانات qstr

...

كائنات الثوابت المُرمَّزة

عناصر الشيفرة الخام

يحتوي عنصر الشيفرة الخام على شيفرة، إما شيفرة بايت أو شيفرة آلة أصلية. ومحتوياته هي:

الحجم

الحقل

vuint

النوع والحجم وما إذا كانت هناك عناصر شيفرة خام فرعية

...

الشيفرة (شيفرة بايت أو شيفرة آلة)

vuint

عدد عناصر الشيفرة الخام الفرعية (فقط إن كان غير صفري)

...

عناصر الشيفرة الخام الفرعية

يُرمّز أول vuint في عنصر الشيفرة الخام نوع الشيفرة المخزّنة في هذا العنصر (البتان الأقل دلالة)، وما إذا كان لهذه الشيفرة الخام أي فروع (البت الثالث الأقل دلالة)، وطول الشيفرة التي تليها (مقدار ذاكرة RAM المطلوب تخصيصها لها).

بعد الـ vuint تأتي الشيفرة نفسها. وما لم يكن نوع الشيفرة شيفرة viper بإزاحات (relocations)، فهذه الشيفرة بيانات ثابتة ولا تحتاج إلى تعديل.

إذا كان لهذه الشيفرة الخام أي فروع (كما يدل عليه بت في أول vuint)، فبعد الشيفرة يأتي vuint يَعُدّ عدد عناصر الشيفرة الخام الفرعية.

وأخيرًا تُخزَّن أي عناصر شيفرة خام فرعية، بصورة تعاودية.