קוד מכונה מקורי (native) בקבצי .mpy

חלק זה מתאר כיצד לבנות ולעבוד עם קבצי .mpy המכילים קוד מכונה מקורי (native) משפה שאינה Python. הדבר מאפשר לכם לכתוב קוד בשפה כמו C, להדר ולקשר אותו לתוך קובץ .mpy, ולאחר מכן לייבא קובץ זה כמו מודול Python רגיל. אפשר להשתמש בכך למימוש פונקציונליות קריטית מבחינת ביצועים, או לשם הכללה של ספרייה קיימת שנכתבה בשפה אחרת.

אחד היתרונות המרכזיים של שימוש בקבצי .mpy מקוריים הוא שניתן לייבא קוד מכונה מקורי על ידי סקריפט באופן דינמי, ללא צורך בבנייה מחדש של קושחת ה-MicroPython הראשית. בכך הם שונים מ-מודולי C חיצוניים של MicroPython, המאפשרים גם הם הגדרת מודולים מותאמים אישית ב-C, אך אלו חייבים להיות מהודרים לתוך דמות הקושחה הראשית.

ההתמקדות כאן היא בשימוש ב-C לבניית מודולים מקוריים, אך באופן עקרוני ניתן להכניס לקובץ .mpy כל שפה הניתנת להידור לקוד מכונה עצמאי.

מודול .mpy מקורי נבנה באמצעות הכלי mpy_ld.py, הנמצא בתיקייה tools/ של הפרויקט. כלי זה לוקח אוסף של קבצי אובייקט (קבצי .o) ומקשר אותם יחד כדי ליצור קובץ .mpy מקורי. הוא דורש CPython 3 ואת הספרייה pyelftools בגרסה v0.25 ומעלה.

תכונות נתמכות ומגבלות

קובץ .mpy יכול להכיל בייטקוד של MicroPython ו/או קוד מכונה מקורי. אם הוא מכיל קוד מכונה מקורי אזי לקובץ ה-.mpy משויכת ארכיטקטורה מסוימת. הארכיטקטורות הנתמכות כעת הן (אלו הן האפשרויות התקפות עבור המשתנה ARCH, ראו להלן):

  • x86 (32 סיביות)

  • x64 (x86 של 64 סיביות)

  • armv6m (ARM Thumb, למשל Cortex-M0)

  • armv7m (ARM Thumb 2, למשל Cortex-M3)

  • armv7emsp (ARM Thumb 2, נקודה צפה בדיוק יחיד, למשל Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, נקודה צפה בדיוק כפול, למשל Cortex-M7)

  • xtensa (ללא חלונות, למשל ESP8266)

  • xtensawin (עם חלונות בגודל חלון 8, למשל ESP32, ESP32S3)

  • rv32imc (RISC-V של 32 סיביות עם פקודות מכווצות, למשל ESP32C3, ESP32C6)

  • rv64imc (RISC-V של 64 סיביות עם פקודות מכווצות)

אם הפלטפורמה הנבחרת תומכת בדגלי ארכיטקטורה מפורשים וברצונכם שקובץ ה-.mpy שייווצר יישא את ערך הדגלים הללו, עליכם להעביר אותם למשתנה הדגלים ARCH_FLAGS בעת בניית קובץ ה-.mpy.

בעת הידור וקישור של קובץ ה-.mpy המקורי יש לבחור את הארכיטקטורה, והקובץ המתאים ניתן לייבוא רק בארכיטקטורה זו (ואם קיימים דגלי ארכיטקטורה, רק אם הם תואמים ליכולות היעד). לפרטים נוספים על קבצי .mpy ראו קובצי .mpy של MicroPython.

קוד מקורי חייב להיות מהודר כקוד בלתי תלוי מיקום (PIC) ולהשתמש בטבלת היסט גלובלית (GOT), אם כי הפרטים בעניין זה משתנים מארכיטקטורה לארכיטקטורה. בעת ייבוא קבצי .mpy עם קוד מקורי מנגנון הייבוא מסוגל לבצע הזזה (relocation) בסיסית של הקוד המקורי. זה כולל הזזה של מקטעי text,‏ rodata ו-BSS.

התכונות הנתמכות של המקשר (linker) והטוען הדינמי הן:

  • קוד בר-הרצה (text)

  • נתונים לקריאה בלבד (rodata), כולל מחרוזות ונתונים קבועים (מערכים, מבנים וכו«)

  • נתונים מאופסים (BSS)

  • מצביעים ב-text אל text,‏ rodata ו-BSS

  • מצביעים ב-rodata אל text,‏ rodata ו-BSS

המגבלות הידועות הן:

  • מקטעי נתונים (data) אינם נתמכים; פתרון עוקף: השתמשו בנתוני BSS ואתחלו את ערכי הנתונים במפורש

  • משתני BSS סטטיים אינם נתמכים; פתרון עוקף: השתמשו במשתני BSS גלובליים

  • משתני אחסון מקומי-תהליכון (thread-local storage) אינם נתמכים ב-rv32imc; פתרון עוקף: השתמשו במשתני BSS גלובליים או הקצו מעט מקום בערימה (heap) לאחסונם

לכן, אם קוד ה-C שלכם מכיל נתונים הניתנים לכתיבה, ודאו שהנתונים מוגדרים באופן גלובלי, ללא מאתחל, ונכתבים רק בתוך פונקציות.

המודול המקורי אינו מקושר אוטומטית כנגד הספריות הסטטיות הסטנדרטיות כמו libm.a ו-libgcc.a, מה שעלול להוביל לשגיאות undefined symbol. ניתן לקשר את ספריות זמן הריצה על ידי הגדרת LINK_RUNTIME = 1 ב-Makefile שלכם. ניתן לקשר גם ספריות סטטיות מותאמות אישית על ידי הוספת MPY_LD_FLAGS += -l path/to/library.a. שימו לב שאלו מקושרות לתוך המודול המקורי ולא ישותפו עם מודולים אחרים או עם המערכת.

מגבלת מקשר: המודול המקורי אינו מקושר כנגד טבלת הסמלים של קושחת ה-MicroPython המלאה. במקום זאת, הוא מקושר כנגד טבלה מפורשת של סמלים מיוצאים הנמצאת ב-mp_fun_table (בתוך py/nativeglue.h), אשר נקבעת בזמן בניית הקושחה. לפיכך לא ניתן פשוט לקרוא לפונקציית HAL/OS/RTOS/מערכת שרירותית כלשהי, למשל, אלא אם היא שוכנת בכתובת קבועה. במקרה כזה, ניתן להעביר ל-mpy_ld.py את הנתיב של linkerscript המכיל סדרה של שמות סמלים וכתובתם הקבועה, באמצעות ארגומנט שורת הפקודה --externs. כך סמלים המופיעים ב-linkerscript יקבלו עדיפות על פני מה שמסופק מקבצי האובייקט, אך כעת המימוש של קבצי האובייקט עדיין ישכון בקובץ ה-MPY הסופי. מנתח ה-linkerscript מוגבל ביכולותיו, וכרגע משמש רק לניתוח רשימת סמלי ה-ROM של פורט ה-ESP8266 (ראו ports/esp8266/boards/eagle.rom.addr.v6.ld).

ניתן להוסיף סמלים חדשים לסוף הטבלה ולבנות את הקושחה מחדש. יש להוסיף את הסמלים גם למילון fun_table שבקובץ tools/mpy_ld.py, באותו מיקום. הדבר מאפשר ל-mpy_ld.py לזהות את הסמלים החדשים ולספק עבורם הזזות (relocations) בעת ייבוא ה-mpy. לבסוף, אם הסמל הוא פונקציה, יש להוסיף מאקרו או stub לקובץ py/dynruntime.h כדי להקל על הקריאה לפונקציה.

הגדרת מודול מקורי

מודול .mpy מקורי מוגדר על ידי קבוצת קבצים המשמשים לבניית ה-.mpy. פריסת מערכת הקבצים מורכבת משני חלקים עיקריים, קבצי המקור וה-Makefile:

  • במקרה הפשוט ביותר נדרש רק קובץ מקור C יחיד, המכיל את כל הקוד שיהודר לתוך מודול ה-.mpy. קוד מקור C זה חייב לכלול את הקובץ py/dynruntime.h כדי לגשת ל-API הדינמי של MicroPython, וחייב להגדיר לכל הפחות פונקציה בשם mpy_init. פונקציה זו תהיה נקודת הכניסה של המודול, ותיקרא כאשר המודול מיובא.

    ניתן לפצל את המודול למספר קבצי מקור C במידת הצורך. ניתן גם לממש חלקים מהמודול ב-Python. יש לרשום את כל קבצי המקור ב-Makefile, על ידי הוספתם למשתנה SRC (ראו להלן). זה כולל הן קבצי מקור C והן קבצי Python כלשהם שייכללו בקובץ ה-.mpy שייווצר.

  • ה-Makefile מכיל את תצורת הבנייה של המודול ומפרט את קבצי המקור המשמשים לבניית מודול ה-.mpy. עליו להגדיר את MPY_DIR כמיקום מאגר ה-MicroPython (כדי למצוא קובצי כותרת, את שבר ה-Makefile הרלוונטי, ואת הכלי mpy_ld.py), את MOD כשם המודול, את SRC כרשימת קבצי המקור, לציין באופן אופציונלי את ארכיטקטורת המכונה באמצעות ARCH, יחד עם דגלי ארכיטקטורת מכונה אופציונליים המצוינים באמצעות ARCH_FLAGS, ולאחר מכן לכלול את py/dynruntime.mk.

דוגמה מינימלית

חלק זה מספק דוגמה עובדת באופן מלא של מודול פשוט בשם factorial. מודול זה מספק פונקציה יחידה factorial.factorial(x) המחשבת את העצרת של הקלט ומחזירה את התוצאה.

פריסת התיקייה:

factorial/
├── factorial.c
└── Makefile

הקובץ factorial.c מכיל:

// Include the header file to get access to the MicroPython API
#include "py/dynruntime.h"

// Helper function to compute factorial
static mp_int_t factorial_helper(mp_int_t x) {
    if (x == 0) {
        return 1;
    }
    return x * factorial_helper(x - 1);
}

// This is the function which will be called from Python, as factorial(x)
static mp_obj_t factorial(mp_obj_t x_obj) {
    // Extract the integer from the MicroPython input object
    mp_int_t x = mp_obj_get_int(x_obj);
    // Calculate the factorial
    mp_int_t result = factorial_helper(x);
    // Convert the result to a MicroPython integer object and return it
    return mp_obj_new_int(result);
}
// Define a Python reference to the function above
static MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial);

// This is the entry point and is called when the module is imported
mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
    // This must be first, it sets up the globals dict and other things
    MP_DYNRUNTIME_INIT_ENTRY

    // Make the function available in the module's namespace
    mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj));

    // This must be last, it restores the globals dict
    MP_DYNRUNTIME_INIT_EXIT
}

הקובץ Makefile מכיל:

# Location of top-level MicroPython directory
MPY_DIR = ../../..

# Name of module
MOD = factorial

# Source files (.c or .py)
SRC = factorial.c

# Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin, rv32imc, rv64imc)
ARCH = x64

# Include to get the rules for compiling and linking the module
include $(MPY_DIR)/py/dynruntime.mk

הידור המודול

כלי הקדם-דרישה הנחוצים לבניית קובץ .mpy מקורי הם:

  • מאגר ה-MicroPython (לכל הפחות התיקיות py/ ו-tools/).

  • CPython 3, והספרייה pyelftools (למשל pip install 'pyelftools>=0.25').

  • GNU make.

  • מהדר C עבור ארכיטקטורת היעד (אם נעשה שימוש במקור C).

  • באופן אופציונלי mpy-cross, הבנוי ממאגר ה-MicroPython (אם נעשה שימוש במקור .py).

הקפידו לבחור את ה-ARCH הנכון עבור היעד שעליו אתם עומדים להריץ. לאחר מכן בנו באמצעות:

$ make

ללא שינוי ה-Makefile ניתן לציין את ארכיטקטורת היעד באמצעות:

$ make ARCH=armv7m

הדבר חל גם על דגלי ארכיטקטורה אופציונליים באמצעות:

$ make ARCH=rv32imc ARCH_FLAGS=zba

שימוש במודול ב-MicroPython

לאחר בניית המודול אמור להיות קובץ בשם factorial.mpy. העתיקו אותו כך שיהיה נגיש במערכת הקבצים של מערכת ה-MicroPython שלכם וניתן יהיה למצוא אותו בנתיב הייבוא. כעת ניתן לגשת למודול ב-Python בדיוק כמו כל מודול אחר, למשל:

import factorial
print(factorial.factorial(10))
# should display 3628800

שימוש ב-Picolibc בעת בניית מודולים

השימוש ב-Picolibc כספריית ה-C הסטנדרטית שלכם לא רק נתמך, אלא שהוא למעשה ברירת המחדל עבור הפלטפורמות rv32imc ו-rv64imc. עם זאת, יש כמה דברים שכדאי להזכיר כדי לוודא שלא תיתקלו בבעיות מאוחר יותר בעת בניית קוד.

חלק מגרסאות Picolibc הבנויות מראש (למשל, אלו המסופקות על ידי Ubuntu Linux כחבילות picolibc-arm-none-eabi,‏ picolibc-riscv64-unknown-elf ו-picolibc-xtensa-lx106-elf) מניחות שאחסון מקומי-תהליכון (TLS) זמין בזמן ריצה, אך למרבה הצער מודולי MicroPython אינם תומכים בכך בחלק מהארכיטקטורות (דהיינו rv32imc ו-rv64imc). משמעות הדבר היא שחלק מהפונקציונליות שמספק Picolibc תשתמש כברירת מחדל ב-TLS, ותחזיר שגיאה בזמן ההידור או בזמן הקישור.

לדוגמה כיצד הדבר עשוי להשפיע עליכם, מודול הדוגמה examples/natmod/btree מכיל פתרון עוקף כדי לוודא ש-errno עובד (חפשו את __PICOLIBC_ERRNO_FUNCTION ב-Makefile ועקבו אחר העקבות משם).

דוגמאות נוספות

ראו examples/natmod/ לדוגמאות נוספות המדגימות רבות מהתכונות הזמינות של מודולי .mpy מקוריים. תכונות אלו כוללות:

  • שימוש במספר קבצי מקור C

  • הכללת קוד Python לצד קוד C

  • נתוני rodata ו-BSS

  • הקצאת זיכרון

  • שימוש בנקודה צפה

  • טיפול בחריגות

  • הכללת ספריות C חיצוניות