وحدات C الخارجية في MicroPython

عند تطوير وحدات للاستخدام مع MicroPython قد تجد نفسك تصطدم بقيود في بيئة Python، غالبًا بسبب عدم القدرة على الوصول إلى موارد عتادية معينة أو بسبب قيود سرعة Python.

إذا لم يكن بالإمكان حل القيود التي تواجهها باستخدام الاقتراحات الواردة في تعظيم سرعة MicroPython، فإن كتابة جزء من وحدتك أو كلها بلغة C (و/أو C++ إذا كان مطبقًا في منفذك) يُعد خيارًا قابلًا للتطبيق.

إذا كانت وحدتك مصممة للوصول إلى عتاد أو مكتبات متاحة على نطاق واسع أو للعمل معها، فيُرجى التفكير في تطبيقها داخل شجرة مصدر MicroPython إلى جانب الوحدات المماثلة وتقديمها كطلب سحب (pull request). أما إذا كنت تستهدف أنظمة غامضة أو مملوكة، فقد يكون من الأنسب إبقاؤها خارج المستودع الرئيسي لـ MicroPython.

يصف هذا الفصل كيفية تجميع مثل هذه الوحدات الخارجية ضمن الملف التنفيذي لـ MicroPython أو صورة البرنامج الثابت. كلٌّ من أدوات البناء Make و CMake مدعوم، وعند كتابة وحدة خارجية فمن المستحسن إضافة ملفات البناء لكلتا هاتين الأداتين حتى يمكن استخدام الوحدة على جميع المنافذ. لكن عند تجميع منفذ معيّن ستحتاج فقط إلى استخدام طريقة بناء واحدة، إما Make أو CMake.

ثمة منهج بديل يتمثل في استخدام الشيفرة الآلية الأصلية (Native) في ملفات ‎.mpy الذي يتيح كتابة شيفرة C مخصصة تُوضع في ملف ‎.mpy‎، والذي يمكن استيراده ديناميكيًا في نظام MicroPython قيد التشغيل دون الحاجة إلى إعادة تجميع البرنامج الثابت الرئيسي.

بنية وحدة C الخارجية

وحدة C الخاصة بالمستخدم في MicroPython هي دليل يحتوي على الملفات التالية:

  • ملفات الشيفرة المصدرية *.c / *.cpp / *.h الخاصة بوحدتك.

    ستتضمن هذه الملفات عادةً الوظائف منخفضة المستوى التي يجري تطبيقها بالإضافة إلى دوال الربط الخاصة بـ MicroPython لكشف الدوال والوحدة (أو الوحدات).

    أفضل مرجع حاليًا لكتابة هذه الدوال/الوحدات هو إيجاد وحدات مماثلة ضمن شجرة MicroPython واستخدامها كأمثلة.

  • يحتوي micropython.mk على جزء Makefile الخاص بهذه الوحدة.

    يتوفر $(USERMOD_DIR) في micropython.mk كمسار إلى دليل وحدتك. وبما أنه يُعاد تعريفه لكل وحدة C، ينبغي توسعته في micropython.mk إلى متغير make محلي، مثل EXAMPLE_MOD_DIR := $(USERMOD_DIR)

    يجب أن يضيف micropython.mk الخاص بك ملفات مصدر وحداتك إلى المتغيرين SRC_USERMOD_C أو SRC_USERMOD_LIB_C. سيُعالَج الأول بحثًا عن تعريفات MP_QSTR_ و MP_REGISTER_MODULE، بينما لن يُعالَج الثاني (مثل الدوال المساعدة وشيفرة المكتبات غير المرتبطة بـ MicroPython). ينبغي أن تتضمن هذه المسارات نسختك الموسّعة من $(USERMOD_DIR)، مثل:

    SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c
    SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
    

    وبالمثل، استخدم SRC_USERMOD_CXX و SRC_USERMOD_LIB_CXX لملفات مصدر C++. وإذا أردت تضمين ملفات تجميع (assembly) فاستخدم SRC_USERMOD_LIB_ASM.

    إذا كانت لديك خيارات مترجم مخصصة (مثل -I لإضافة أدلة للبحث عن ملفات الترويسة)، فينبغي إضافتها إلى CFLAGS_USERMOD لشيفرة C وإلى CXXFLAGS_USERMOD لشيفرة C++.

  • يحتوي micropython.cmake على تكوين CMake الخاص بهذه الوحدة.

    في micropython.cmake، يمكنك استخدام ${CMAKE_CURRENT_LIST_DIR} كمسار إلى الوحدة الحالية.

    ينبغي أن يُعرّف micropython.cmake الخاص بك مكتبة من نوع INTERFACE وأن يربط بها ملفات المصدر وتعريفات التجميع وأدلة التضمين الخاصة بك. ثم ينبغي ربط المكتبة بهدف usermod.

    add_library(usermod_cexample INTERFACE)
    
    target_sources(usermod_cexample INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}/examplemodule.c
    )
    
    target_include_directories(usermod_cexample INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}
    )
    
    target_link_libraries(usermod INTERFACE usermod_cexample)
    

    انظر أدناه للحصول على مثال استخدام كامل.

مثال أساسي

توفر وحدة cexample أمثلة لدالة وصنف. تجمع الدالة cexample.add_ints(a, b) وسيطين صحيحين معًا وتُعيد النتيجة. ويُنشئ النوع cexample.Timer() مؤقتات يمكن استخدامها لقياس الزمن المنقضي منذ إنشاء الكائن.

يمكن العثور على الوحدة في شجرة مصدر MicroPython في دليل الأمثلة وهي تحتوي على ملف مصدر وجزء Makefile بمحتوى كما هو موضّح أعلاه:

micropython/
└──examples/
   └──usercmodule/
      └──cexample/
         ├── examplemodule.c
         ├── micropython.mk
         └── micropython.cmake

راجع التعليقات في هذه الملفات للحصول على شرح إضافي. وإلى جانب وحدة cexample توجد أيضًا cppexample التي تعمل بالطريقة نفسها لكنها توضح إحدى طرق مزج شيفرة C و C++ في MicroPython.

تجميع الوحدة (cmodule) ضمن MicroPython

لبناء مثل هذه الوحدة، قم بتجميع MicroPython (انظر البدء)، مع تطبيق تعديلين:

  1. اضبط علم وقت البناء USER_C_MODULES ليشير إلى الوحدات التي تريد تضمينها. بالنسبة للمنافذ التي تستخدم Make ينبغي أن يكون هذا المتغير دليلًا يُبحث فيه عن الوحدات تلقائيًا. أما بالنسبة للمنافذ التي تستخدم CMake فينبغي أن يكون هذا المتغير ملفًا يتضمن الوحدات المراد بناؤها. انظر أدناه للتفاصيل.

  2. فعّل الوحدات بضبط ماكرو المعالج المسبق لـ C المقابل على القيمة 1. لا يلزم ذلك إلا إذا كانت الوحدات التي تبنيها غير مفعّلة تلقائيًا.

لبناء وحدات الأمثلة التي تأتي مع MicroPython، اضبط USER_C_MODULES على دليل examples/usercmodule بالنسبة لـ Make، أو على examples/usercmodule/micropython.cmake بالنسبة لـ CMake.

على سبيل المثال، إليك كيفية بناء منفذ unix مع وحدات الأمثلة:

cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule

قد تحتاج إلى تشغيل make clean مرة واحدة في البداية عند تضمين وحدات مستخدم جديدة في البناء. سيعرض ناتج البناء الوحدات التي عُثر عليها:

...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...

بالنسبة لمنفذ يعتمد على CMake مثل rp2، سيبدو هذا مختلفًا قليلًا (لاحظ أن CMake يُستدعى فعليًا بواسطة make):

cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake

مرة أخرى، قد تحتاج إلى تشغيل make clean أولًا حتى يلتقط CMake وحدات المستخدم. ويسرد ناتج بناء CMake الوحدات بالاسم:

...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...

يمكن استخدام محتويات micropython.cmake ذي المستوى الأعلى للتحكم في الوحدات المفعّلة.

بالنسبة لمشاريعك الخاصة، يكون من الأنسب إبقاء الشيفرة المخصصة خارج شجرة مصدر MicroPython الرئيسية، لذا فإن بنية دليل المشروع النموذجية ستبدو على النحو التالي:

my_project/
├── modules/
│   ├── example1/
│   │   ├── example1.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   ├── example2/
│   │   ├── example2.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   └── micropython.cmake
└── micropython/
    ├──ports/
   ... ├──stm32/
      ...

عند البناء باستخدام Make اضبط USER_C_MODULES على دليل my_project/modules. على سبيل المثال، بناء منفذ stm32:

cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules

عند البناء باستخدام CMake فإن micropython.cmake ذا المستوى الأعلى -- الموجود مباشرةً في دليل my_project/modules -- ينبغي أن يستخدم include لجميع الوحدات التي تريد إتاحتها:

include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)

ثم ابنِ باستخدام:

cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake

يمكنك أيضًا تحديد مسارات مطلقة لـ USER_C_MODULES.

جميع الوحدات المحددة بواسطة المتغير USER_C_MODULES (سواء عُثر عليها في هذا الدليل عند استخدام Make، أو أُضيفت عبر include عند استخدام CMake) سيتم تجميعها، لكن فقط الوحدات المفعّلة هي التي ستكون متاحة للاستيراد. تكون وحدات المستخدم عادةً مفعّلة افتراضيًا (يقرر ذلك مطوّر الوحدة)، وفي هذه الحالة لا يوجد ما يلزم فعله أكثر من ضبط USER_C_MODULES كما هو موضّح أعلاه.

إذا لم تكن الوحدة مفعّلة افتراضيًا، فيجب عندئذٍ تفعيل ماكرو المعالج المسبق لـ C المقابل. ويمكن العثور على اسم هذا الماكرو بالبحث عن سطر MP_REGISTER_MODULE في الشيفرة المصدرية للوحدة (وهو يظهر عادةً في نهاية ملف المصدر الرئيسي). ينبغي أن يكون هذا الماكرو محاطًا بزوج #if X / #endif، ويجب ضبط خيار التكوين X على القيمة 1 باستخدام CFLAGS_EXTRA لجعل الوحدة متاحة. وإذا لم يكن هناك زوج #if X / #endif فإن الوحدة تكون مفعّلة افتراضيًا.

على سبيل المثال، الوحدة examples/usercmodule/cexample مفعّلة افتراضيًا، لذا يحتوي مصدرها على السطر التالي:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);

وبدلًا من ذلك، لجعل هذه الوحدة معطّلة افتراضيًا مع إمكانية اختيارها عبر خيار تكوين في المعالج المسبق، يكون السطر كالتالي:

#if MODULE_CEXAMPLE_ENABLED
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
#endif

في هذه الحالة تُفعَّل الوحدة بإضافة CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 إلى أمر make، أو بتحرير mpconfigport.h أو mpconfigboard.h لإضافة

#define MODULE_CEXAMPLE_ENABLED (1)

لاحظ أن الطريقة الدقيقة تعتمد على المنفذ لأن للمنافذ بنى مختلفة. وإذا لم يُنجَز ذلك بشكل صحيح فسيتم التجميع لكن الاستيراد سيفشل في إيجاد الوحدة.

استخدام الوحدة في MicroPython

بمجرد بنائها ضمن نسختك من MicroPython، يمكن الآن الوصول إلى الوحدة في Python تمامًا مثل أي وحدة مدمجة أخرى، مثل

import cexample
print(cexample.add_ints(1, 3))
# should display 4
from cexample import Timer
from time import sleep_ms

watch = Timer()
sleep_ms(1000)
print(watch.time())
# should display approximately 1000

تخصيص الذاكرة الديناميكي في C

يستخدم MicroPython «كَوْمة Python» الخاصة به لـ إدارة الذاكرة، وهي ليست نفسها «كَوْمة C» التي تستخدمها دوال مكتبة C مثل malloc() و free() وغيرها. ولا يأتي كل منفذ من منافذ MicroPython بـ «كَوْمة C» من الأساس.

تتفاوت منافذ المستوى 1 و 2 في دعمها لتخصيص الذاكرة الديناميكي في C عبر «كَوْمة C»:

  • تدعم منافذ unix و windows و esp32 و webassembly تخصيص الذاكرة الديناميكي في C.

  • سيفشل منفذ rp2 في تخصيص أي ذاكرة في وقت التشغيل ما لم يُبنَ البرنامج الثابت باستخدام MICROPY_C_HEAP_SIZE=n لحجز n بايت من الذاكرة لكَوْمة C. ولن تكون هذه الذاكرة متاحة لاستخدام شيفرة Python.

  • ستفشل بُنى منافذ alif و mimxrt و nrf و renesas-ra و samd و stm32 التي تتضمن تخصيصًا ديناميكيًا في C عند الربط (link-time) برسائل خطأ مثل undefined reference to `malloc'. لا يملك MicroPython دعمًا مدمجًا للتخصيص الديناميكي في C على هذه المنافذ. ويتطلب أي حل إضافة تطبيق كَوْمة C يدويًا إلى البناء المخصص.

  • لا يدعم منفذ zephyr حاليًا البناء مع وحدات المستخدم.

كَوْمة Python ككَوْمة C

قد يكون من العملي أن تستدعي شيفرة C دوال التخصيص الديناميكي لـ «كَوْمة Python» مثل m_malloc() و m_malloc0() و m_free() بدلًا من ذلك.

انظر ذاكرة MicroPython من شيفرة C لمزيد من المعلومات حول هذا المنهج.

وحدات C++

تدعم معظم منافذ MicroPython من المستوى 1 و 2 (وبعض منافذ المستوى 3) بناء وحدات المستخدم بلغة C++، باستخدام متغيرات البيئة الخاصة بـ C++ الموضّحة أعلاه.

ينطوي دمج C++ و MicroPython بنجاح على بعض الاعتبارات الإضافية:

تخصيص الذاكرة الديناميكي في C++

تستخدم برامج C++ (وكذلك ميزات مكتبة C++ القياسية) عادةً التخصيص الديناميكي للذاكرة. ويُطبَّق مخصِّص الذاكرة الافتراضي لـ C++ (أي العاملان new و delete) عادةً كطبقة فوق تخصيص الذاكرة الديناميكي في C.

بالنسبة لمنافذ MicroPython التي لا تتضمن دعمًا لتخصيص الذاكرة الديناميكي في C، يمكن دعم تخصيص الذاكرة الديناميكي في C++ بإحدى طريقتين:

  • تطبيق تخصيص الذاكرة الديناميكي في C ضمن بنائك المخصص.

  • تطبيق مخصِّص C++ مخصص ضمن بنائك المخصص.

اعتبارات الربط

بما أن MicroPython مشروع قائم على C، فإن أي رموز ترتبط بـ MicroPython أو منه تحتاج إلى التأهيل بـ extern "C" في شيفرة C++.

يُنصح بشدة باتباع النمط الموضّح في examples/usercmodule/cppexample، حيث تُطبَّق وحدة Python في ملف C بسيط يغلّف شيفرة C++.