โค้ดเครื่องแบบ native ในไฟล์ .mpy¶
ส่วนนี้อธิบายวิธีการสร้างและทำงานกับไฟล์ .mpy ที่ประกอบด้วยโค้ดเครื่องแบบ native จากภาษาอื่นที่ไม่ใช่ Python ซึ่งช่วยให้คุณเขียนโค้ดในภาษาเช่น C คอมไพล์และลิงก์ลงในไฟล์ .mpy แล้วนำเข้าไฟล์นั้นเหมือนโมดูล Python ปกติ วิธีนี้สามารถใช้สำหรับการนำฟังก์ชันที่ต้องการประสิทธิภาพสูงมาใช้งาน หรือสำหรับการรวมไลบรารีที่มีอยู่แล้วซึ่งเขียนด้วยภาษาอื่น
ข้อได้เปรียบหลักอย่างหนึ่งของการใช้ไฟล์ .mpy แบบ native คือโค้ดเครื่องแบบ native สามารถนำเข้าโดยสคริปต์แบบไดนามิกได้ โดยไม่จำเป็นต้องสร้างเฟิร์มแวร์ MicroPython หลักขึ้นมาใหม่ ซึ่งแตกต่างจาก โมดูล C ภายนอกสำหรับ MicroPython ที่อนุญาตให้กำหนดโมดูลแบบกำหนดเองใน C เช่นกัน แต่ต้องคอมไพล์ลงในอิมเมจเฟิร์มแวร์หลัก
จุดเน้นที่นี่คือการใช้ C เพื่อสร้างโมดูล native แต่โดยหลักการแล้ว ภาษาใดก็ตามที่สามารถคอมไพล์เป็นโค้ดเครื่องแบบ stand-alone ได้ก็สามารถนำไปใส่ในไฟล์ .mpy ได้
โมดูล .mpy แบบ native สร้างขึ้นโดยใช้เครื่องมือ mpy_ld.py ซึ่งอยู่ในไดเรกทอรี tools/ ของโปรเจกต์ เครื่องมือนี้รับชุดของไฟล์ออบเจกต์ (.o files) และลิงก์เข้าด้วยกันเพื่อสร้างไฟล์ .mpy แบบ native ต้องการ CPython 3 และไลบรารี pyelftools เวอร์ชัน 0.25 หรือสูงกว่า
ฟีเจอร์ที่รองรับและข้อจำกัด¶
ไฟล์ .mpy สามารถประกอบด้วย MicroPython bytecode และ/หรือโค้ดเครื่องแบบ native หากมีโค้ดเครื่องแบบ native ไฟล์ .mpy จะมีสถาปัตยกรรมเฉพาะที่เชื่อมโยงอยู่ด้วย สถาปัตยกรรมที่รองรับในปัจจุบัน ได้แก่ (ตัวเลือกที่ถูกต้องสำหรับตัวแปร ARCH ดูด้านล่าง):
x86(32 บิต)x64(64 บิต x86)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 แบบ native จะต้องเลือกสถาปัตยกรรม และไฟล์ที่ได้จะนำเข้าได้บนสถาปัตยกรรมนั้นเท่านั้น (และหากมีแฟล็กสถาปัตยกรรม จะต้องตรงกับความสามารถของเป้าหมายด้วย) สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับไฟล์ .mpy ดูที่ ไฟล์ .mpy ของ MicroPython
โค้ด native จะต้องคอมไพล์เป็น position independent code (PIC) และใช้ global offset table (GOT) แม้ว่ารายละเอียดจะแตกต่างกันไปตามสถาปัตยกรรม เมื่อนำเข้าไฟล์ .mpy ที่มีโค้ด native กลไกการนำเข้าสามารถทำ relocation ขั้นพื้นฐานของโค้ด native ได้ ซึ่งรวมถึงการย้าย text, rodata และส่วน BSS
ฟีเจอร์ที่รองรับของลิงก์เกอร์และตัวโหลดแบบไดนามิก ได้แก่:
โค้ดที่ประมวลผลได้ (text)
ข้อมูลอ่านอย่างเดียว (rodata) รวมถึงสตริงและข้อมูลค่าคงที่ (อาร์เรย์, struct เป็นต้น)
ข้อมูลที่ถูกตั้งค่าเป็นศูนย์ (BSS)
พอยน์เตอร์ใน text ที่ชี้ไปยัง text, rodata และ BSS
พอยน์เตอร์ใน rodata ที่ชี้ไปยัง text, rodata และ BSS
ข้อจำกัดที่ทราบมีดังนี้:
ส่วน data ไม่รองรับ; วิธีแก้ไข: ใช้ข้อมูล BSS และกำหนดค่าข้อมูลอย่างชัดเจน
ตัวแปร BSS แบบ static ไม่รองรับ; วิธีแก้ไข: ใช้ตัวแปร BSS แบบ global
ตัวแปร thread-local storage ไม่รองรับบน rv32imc; วิธีแก้ไข: ใช้ตัวแปร BSS แบบ global หรือจัดสรรพื้นที่บน heap เพื่อเก็บตัวแปรเหล่านั้น
ดังนั้น หากโค้ด C ของคุณมีข้อมูลที่เขียนได้ ให้กำหนดข้อมูลแบบ global โดยไม่มีตัวกำหนดค่าเริ่มต้น และเขียนค่าเฉพาะภายในฟังก์ชัน
โมดูล native จะไม่ถูกลิงก์กับไลบรารีแบบ static มาตรฐานอัตโนมัติ เช่น libm.a และ libgcc.a ซึ่งอาจทำให้เกิดข้อผิดพลาด undefined symbol คุณสามารถลิงก์ไลบรารี runtime ได้โดยตั้งค่า LINK_RUNTIME = 1 ใน Makefile ของคุณ นอกจากนี้ ยังสามารถลิงก์ไลบรารี static แบบกำหนดเองได้โดยเพิ่ม MPY_LD_FLAGS += -l path/to/library.a โปรดทราบว่าไลบรารีเหล่านี้จะถูกลิงก์เข้าในโมดูล native และจะไม่ถูกแชร์กับโมดูลอื่นหรือระบบ
ข้อจำกัดของลิงก์เกอร์: โมดูล native จะไม่ถูกลิงก์กับตารางสัญลักษณ์ของเฟิร์มแวร์ MicroPython แบบเต็ม แต่จะถูกลิงก์กับตารางสัญลักษณ์ที่ส่งออกอย่างชัดเจนที่พบใน mp_fun_table (ใน py/nativeglue.h) ซึ่งถูกกำหนดตายตัวในช่วงเวลาสร้างเฟิร์มแวร์ ดังนั้นจึงเป็นไปไม่ได้ที่จะเรียกฟังก์ชัน HAL/OS/RTOS/ระบบที่กำหนดเองได้โดยตรง เว้นแต่ฟังก์ชันนั้นจะอยู่ที่แอดเดรสคงที่ ในกรณีนั้น เส้นทางของ linkerscript ที่มีชื่อสัญลักษณ์และแอดเดรสคงที่สามารถส่งไปยัง mpy_ld.py ผ่านอาร์กิวเมนต์บรรทัดคำสั่ง --externs วิธีนี้ทำให้สัญลักษณ์ที่ปรากฏใน linkerscript จะมีลำดับความสำคัญเหนือสิ่งที่ได้รับจากไฟล์ออบเจกต์ แต่ในขณะนี้การนำไปใช้ของไฟล์ออบเจกต์จะยังคงอยู่ในไฟล์ MPY สุดท้าย ตัวแยกวิเคราะห์ linkerscript มีความสามารถจำกัด และปัจจุบันใช้เพียงเพื่อแยกวิเคราะห์รายการสัญลักษณ์ ROM ของพอร์ต ESP8266 (ดู ports/esp8266/boards/eagle.rom.addr.v6.ld)
สัญลักษณ์ใหม่สามารถเพิ่มที่ท้ายของตารางและสร้างเฟิร์มแวร์ขึ้นมาใหม่ได้ นอกจากนี้ สัญลักษณ์ยังต้องถูกเพิ่มใน dict fun_table ของ tools/mpy_ld.py ในตำแหน่งเดียวกัน วิธีนี้ช่วยให้ mpy_ld.py สามารถรับสัญลักษณ์ใหม่และจัดเตรียม relocation สำหรับสัญลักษณ์เหล่านั้นเมื่อนำเข้า mpy ได้ สุดท้าย หากสัญลักษณ์เป็นฟังก์ชัน ควรเพิ่ม macro หรือ stub ใน py/dynruntime.h เพื่อให้เรียกฟังก์ชันได้ง่าย
การกำหนดโมดูล native¶
โมดูล .mpy แบบ native ถูกกำหนดโดยชุดของไฟล์ที่ใช้สร้าง .mpy โครงสร้างระบบไฟล์ประกอบด้วยสองส่วนหลัก ได้แก่ ไฟล์ต้นฉบับและ Makefile:
ในกรณีที่ง่ายที่สุด ต้องการเพียงไฟล์ต้นฉบับ C ไฟล์เดียว ซึ่งประกอบด้วยโค้ดทั้งหมดที่จะคอมไพล์ลงในโมดูล .mpy โค้ด C นี้ต้องรวมไฟล์
py/dynruntime.hเพื่อเข้าถึง MicroPython dynamic API และต้องกำหนดฟังก์ชันที่เรียกว่าmpy_initอย่างน้อยหนึ่งฟังก์ชัน ฟังก์ชันนี้จะเป็นจุดเริ่มต้นของโมดูล ซึ่งถูกเรียกเมื่อนำเข้าโมดูลโมดูลสามารถแยกออกเป็นไฟล์ต้นฉบับ C หลายไฟล์ได้ตามต้องการ บางส่วนของโมดูลยังสามารถนำไปใช้งานใน Python ได้ ไฟล์ต้นฉบับทั้งหมดควรระบุไว้ใน Makefile โดยเพิ่มลงในตัวแปร
SRC(ดูด้านล่าง) ซึ่งรวมถึงทั้งไฟล์ต้นฉบับ C และไฟล์ Python ที่จะรวมอยู่ในไฟล์ .mpy ที่ได้Makefileประกอบด้วยการกำหนดค่าการสร้างสำหรับโมดูลและแสดงรายการไฟล์ต้นฉบับที่ใช้สร้างโมดูล .mpy ควรกำหนดMPY_DIRเป็นตำแหน่งของ MicroPython repository (เพื่อค้นหาไฟล์ header, fragment ของ Makefile ที่เกี่ยวข้อง และเครื่องมือmpy_ld.py),MODเป็นชื่อของโมดูล,SRCเป็นรายการของไฟล์ต้นฉบับ, ระบุสถาปัตยกรรมเครื่องโดยตัวเลือกผ่านARCHพร้อมกับแฟล็กสถาปัตยกรรมเครื่องที่ระบุผ่านARCH_FLAGSและรวมpy/dynruntime.mk
ตัวอย่างขั้นต่ำ¶
ส่วนนี้ให้ตัวอย่างที่ใช้งานได้จริงของโมดูลง่าย ๆ ชื่อ factorial โมดูลนี้มีฟังก์ชันเดียวคือ factorial.factorial(x) ซึ่งคำนวณ factorial ของอินพุตและส่งคืนผลลัพธ์
โครงสร้างไดเรกทอรี:
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 แบบ native ได้แก่:
MicroPython repository (อย่างน้อยไดเรกทอรี
py/และtools/)CPython 3 และไลบรารี pyelftools (เช่น
pip install 'pyelftools>=0.25')GNU make
คอมไพเลอร์ C สำหรับสถาปัตยกรรมเป้าหมาย (หากใช้ต้นฉบับ C)
mpy-crossโดยตัวเลือก ซึ่งสร้างจาก MicroPython repository (หากใช้ต้นฉบับ .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) สันนิษฐานว่า thread-local storage (TLS) พร้อมใช้งาน ณ รันไทม์ แต่น่าเสียดายที่โมดูล MicroPython ไม่รองรับสิ่งนั้นบนสถาปัตยกรรมบางประเภท (ได้แก่ rv32imc และ rv64imc) ซึ่งหมายความว่าฟังก์ชันบางอย่างที่จัดเตรียมโดย Picolibc จะใช้ TLS เป็นค่าเริ่มต้น และส่งคืนข้อผิดพลาดในระหว่างการคอมไพล์หรือการลิงก์
สำหรับตัวอย่างว่าสิ่งนี้อาจส่งผลต่อคุณอย่างไร โมดูลตัวอย่าง examples/natmod/btree มีวิธีแก้ไขเพื่อให้ errno ทำงานได้ (ค้นหา __PICOLIBC_ERRNO_FUNCTION ใน Makefile และติดตามเส้นทางจากที่นั่น)
ตัวอย่างเพิ่มเติม¶
ดู examples/natmod/ สำหรับตัวอย่างเพิ่มเติมที่แสดงฟีเจอร์ต่าง ๆ ที่มีอยู่ของโมดูล .mpy แบบ native ฟีเจอร์เหล่านี้ได้แก่:
การใช้ไฟล์ต้นฉบับ C หลายไฟล์
การรวมโค้ด Python ควบคู่กับโค้ด C
ข้อมูล rodata และ BSS
การจัดสรรหน่วยความจำ
การใช้ floating point
การจัดการข้อยกเว้น
การรวมไลบรารี C ภายนอก