Mã máy gốc trong các tệp .mpy

Phần này mô tả cách xây dựng và làm việc với các tệp .mpy chứa mã máy gốc từ ngôn ngữ khác ngoài Python. Điều này cho phép bạn viết mã bằng ngôn ngữ như C, biên dịch và liên kết nó thành một tệp .mpy, sau đó nhập tệp này như một module Python thông thường. Điều này có thể được dùng để triển khai các chức năng đòi hỏi hiệu suất cao, hoặc để tích hợp một thư viện hiện có được viết bằng ngôn ngữ khác.

Một trong những ưu điểm chính của việc sử dụng các tệp .mpy gốc là mã máy gốc có thể được nhập bởi một tập lệnh một cách động, mà không cần phải biên dịch lại firmware MicroPython chính. Điều này trái ngược với Các module C ngoài của MicroPython cũng cho phép định nghĩa các module tùy chỉnh bằng C, nhưng chúng phải được biên dịch vào ảnh firmware chính.

Trọng tâm ở đây là sử dụng C để xây dựng các module gốc, nhưng về nguyên tắc bất kỳ ngôn ngữ nào có thể biên dịch thành mã máy độc lập đều có thể được đưa vào tệp .mpy.

Một module .mpy gốc được xây dựng bằng công cụ mpy_ld.py, nằm trong thư mục tools/ của dự án. Công cụ này nhận một tập hợp các tệp đối tượng (.o) và liên kết chúng lại để tạo ra một tệp .mpy gốc. Nó yêu cầu CPython 3 và thư viện pyelftools phiên bản 0.25 trở lên.

Các tính năng được hỗ trợ và hạn chế

Một tệp .mpy có thể chứa bytecode MicroPython và/hoặc mã máy gốc. Nếu nó chứa mã máy gốc thì tệp .mpy có một kiến trúc cụ thể được liên kết với nó. Các kiến trúc hiện được hỗ trợ là (đây là các tùy chọn hợp lệ cho biến ARCH, xem bên dưới):

  • x86 (32 bit)

  • x64 (64 bit x86)

  • armv6m (ARM Thumb, ví dụ Cortex-M0)

  • armv7m (ARM Thumb 2, ví dụ Cortex-M3)

  • armv7emsp (ARM Thumb 2, dấu phẩy động đơn độ chính xác, ví dụ Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, dấu phẩy động kép độ chính xác, ví dụ Cortex-M7)

  • xtensa (không có cửa sổ, ví dụ ESP8266)

  • xtensawin (có cửa sổ với kích thước cửa sổ 8, ví dụ ESP32, ESP32S3)

  • rv32imc (RISC-V 32 bit với lệnh nén, ví dụ ESP32C3, ESP32C6)

  • rv64imc (RISC-V 64 bit với lệnh nén)

Nếu nền tảng được chọn hỗ trợ các cờ kiến trúc tường minh và bạn muốn tệp .mpy đầu ra mang giá trị của các cờ đó, bạn phải truyền chúng vào biến ARCH_FLAGS khi xây dựng tệp .mpy.

Khi biên dịch và liên kết tệp .mpy gốc, kiến trúc phải được chọn và tệp tương ứng chỉ có thể được nhập trên kiến trúc đó (và nếu có các cờ kiến trúc, chỉ khi chúng khớp với khả năng của mục tiêu). Để biết thêm chi tiết về tệp .mpy, xem Các tệp .mpy của MicroPython.

Mã gốc phải được biên dịch như mã độc lập vị trí (PIC) và sử dụng bảng offset toàn cục (GOT), mặc dù chi tiết của điều này khác nhau giữa các kiến trúc. Khi nhập các tệp .mpy có mã gốc, cơ chế nhập có thể thực hiện một số định vị lại cơ bản của mã gốc. Điều này bao gồm định vị lại các phần text, rodata và BSS.

Các tính năng được hỗ trợ của trình liên kết và bộ tải động là:

  • mã thực thi (text)

  • dữ liệu chỉ đọc (rodata), bao gồm các chuỗi và dữ liệu hằng số (mảng, struct, v.v.)

  • dữ liệu được khởi tạo bằng không (BSS)

  • các con trỏ trong text trỏ đến text, rodata và BSS

  • các con trỏ trong rodata trỏ đến text, rodata và BSS

Các hạn chế đã biết là:

  • các phần dữ liệu không được hỗ trợ; cách khắc phục: sử dụng dữ liệu BSS và khởi tạo các giá trị dữ liệu một cách tường minh

  • các biến BSS tĩnh không được hỗ trợ; cách khắc phục: sử dụng biến BSS toàn cục

  • các biến lưu trữ cục bộ theo luồng không được hỗ trợ trên rv32imc; cách khắc phục: sử dụng biến BSS toàn cục hoặc cấp phát một khoảng không gian trên heap để lưu trữ chúng

Vì vậy, nếu mã C của bạn có dữ liệu có thể ghi, hãy đảm bảo dữ liệu được định nghĩa toàn cục, không có bộ khởi tạo, và chỉ được ghi vào trong các hàm.

Module gốc không được tự động liên kết với các thư viện tĩnh chuẩn như libm.alibgcc.a, điều này có thể dẫn đến lỗi undefined symbol. Bạn có thể liên kết các thư viện runtime bằng cách đặt LINK_RUNTIME = 1 trong Makefile của mình. Các thư viện tĩnh tùy chỉnh cũng có thể được liên kết bằng cách thêm MPY_LD_FLAGS += -l path/to/library.a. Lưu ý rằng chúng được liên kết vào module gốc và sẽ không được chia sẻ với các module khác hoặc hệ thống.

Hạn chế của trình liên kết: module gốc không được liên kết với bảng ký hiệu của firmware MicroPython đầy đủ. Thay vào đó, nó được liên kết với một bảng tường minh các ký hiệu được xuất ra trong mp_fun_table (trong py/nativeglue.h), được cố định tại thời điểm xây dựng firmware. Do đó, không thể đơn giản gọi một hàm HAL/OS/RTOS/hệ thống tùy ý, chẳng hạn, trừ khi hàm đó nằm tại một địa chỉ cố định. Trong trường hợp đó, đường dẫn của một linkerscript chứa một loạt tên ký hiệu và địa chỉ cố định của chúng có thể được truyền vào mpy_ld.py thông qua đối số dòng lệnh --externs. Theo cách đó, các ký hiệu xuất hiện trong linkerscript sẽ được ưu tiên hơn những gì được cung cấp từ các tệp đối tượng, nhưng hiện tại việc triển khai của các tệp đối tượng vẫn sẽ nằm trong tệp MPY cuối cùng. Trình phân tích cú pháp linkerscript bị hạn chế về khả năng và hiện chỉ được sử dụng để phân tích danh sách ký hiệu ROM của cổng ESP8266 (xem ports/esp8266/boards/eagle.rom.addr.v6.ld).

Có thể thêm các ký hiệu mới vào cuối bảng và xây dựng lại firmware. Các ký hiệu cũng cần được thêm vào từ điển fun_table của tools/mpy_ld.py ở cùng vị trí. Điều này cho phép mpy_ld.py có thể nhận các ký hiệu mới và cung cấp các định vị lại cho chúng khi mpy được nhập. Cuối cùng, nếu ký hiệu là một hàm, một macro hoặc stub nên được thêm vào py/dynruntime.h để dễ dàng gọi hàm.

Định nghĩa một module gốc

Một module .mpy gốc được định nghĩa bởi một tập hợp các tệp được sử dụng để xây dựng .mpy. Bố cục hệ thống tệp bao gồm hai phần chính, các tệp nguồn và Makefile:

  • Trong trường hợp đơn giản nhất, chỉ cần một tệp nguồn C duy nhất, chứa tất cả mã sẽ được biên dịch vào module .mpy. Mã nguồn C này phải bao gồm tệp py/dynruntime.h để truy cập API động của MicroPython, và phải ít nhất định nghĩa một hàm gọi là mpy_init. Hàm này sẽ là điểm vào của module, được gọi khi module được nhập.

    Module có thể được chia thành nhiều tệp nguồn C nếu muốn. Các phần của module cũng có thể được triển khai bằng Python. Tất cả các tệp nguồn phải được liệt kê trong Makefile, bằng cách thêm chúng vào biến SRC (xem bên dưới). Điều này bao gồm cả các tệp nguồn C cũng như bất kỳ tệp Python nào sẽ được bao gồm trong tệp .mpy kết quả.

  • Makefile chứa cấu hình xây dựng cho module và liệt kê các tệp nguồn được sử dụng để xây dựng module .mpy. Nó phải định nghĩa MPY_DIR là vị trí của kho lưu trữ MicroPython (để tìm các tệp header, phân mảnh Makefile liên quan, và công cụ mpy_ld.py), MOD là tên của module, SRC là danh sách các tệp nguồn, tùy chọn chỉ định kiến trúc máy thông qua ARCH, cùng với các cờ kiến trúc máy tùy chọn được chỉ định thông qua ARCH_FLAGS, và sau đó bao gồm py/dynruntime.mk.

Ví dụ tối giản

Phần này cung cấp một ví dụ hoạt động đầy đủ về một module đơn giản có tên factorial. Module này cung cấp một hàm duy nhất factorial.factorial(x) để tính giai thừa của đầu vào và trả về kết quả.

Bố cục thư mục:

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

Tệp factorial.c chứa:

// 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
}

Tệp Makefile chứa:

# 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

Biên dịch module

Các công cụ tiên quyết cần thiết để xây dựng một tệp .mpy gốc là:

  • Kho lưu trữ MicroPython (ít nhất là các thư mục py/tools/).

  • CPython 3 và thư viện pyelftools (ví dụ pip install 'pyelftools>=0.25').

  • GNU make.

  • Trình biên dịch C cho kiến trúc mục tiêu (nếu sử dụng nguồn C).

  • Tùy chọn mpy-cross, được xây dựng từ kho lưu trữ MicroPython (nếu sử dụng nguồn .py).

Hãy chắc chắn chọn ARCH đúng cho mục tiêu bạn sẽ chạy trên đó. Sau đó xây dựng với:

$ make

Mà không cần sửa đổi Makefile, bạn có thể chỉ định kiến trúc mục tiêu thông qua:

$ make ARCH=armv7m

Tương tự áp dụng cho các cờ kiến trúc tùy chọn thông qua:

$ make ARCH=rv32imc ARCH_FLAGS=zba

Sử dụng module trong MicroPython

Sau khi module được xây dựng, sẽ có một tệp gọi là factorial.mpy. Sao chép tệp này sao cho có thể truy cập trên hệ thống tệp của hệ thống MicroPython của bạn và có thể tìm thấy trong đường dẫn nhập. Module này giờ đây có thể được truy cập trong Python giống như bất kỳ module nào khác, ví dụ:

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

Sử dụng Picolibc khi xây dựng module

Sử dụng Picolibc làm thư viện chuẩn C của bạn không chỉ được hỗ trợ, mà thực tế đây là mặc định cho các nền tảng rv32imc và rv64imc. Tuy nhiên, có một vài điều đáng lưu ý để đảm bảo bạn không gặp vấn đề sau này khi xây dựng mã.

Một số phiên bản Picolibc được xây dựng sẵn (ví dụ, những phiên bản được cung cấp bởi Ubuntu Linux dưới dạng các gói picolibc-arm-none-eabi, picolibc-riscv64-unknown-elfpicolibc-xtensa-lx106-elf) giả định rằng lưu trữ cục bộ theo luồng (TLS) có sẵn tại thời điểm chạy, nhưng tiếc là các module MicroPython không hỗ trợ điều đó trên một số kiến trúc (cụ thể là rv32imcrv64imc). Điều này có nghĩa là một số chức năng được cung cấp bởi Picolibc sẽ mặc định sử dụng TLS, trả về lỗi trong quá trình biên dịch hoặc trong quá trình liên kết.

Để có ví dụ về cách điều này có thể ảnh hưởng đến bạn, module ví dụ examples/natmod/btree chứa một cách giải quyết để đảm bảo errno hoạt động (tìm kiếm __PICOLIBC_ERRNO_FUNCTION trong Makefile và theo dõi từ đó).

Các ví dụ thêm

Xem examples/natmod/ để biết thêm các ví dụ cho thấy nhiều tính năng có sẵn của các module .mpy gốc. Các tính năng đó bao gồm:

  • sử dụng nhiều tệp nguồn C

  • bao gồm mã Python cùng với mã C

  • dữ liệu rodata và BSS

  • cấp phát bộ nhớ

  • sử dụng dấu phẩy động

  • xử lý ngoại lệ

  • bao gồm các thư viện C bên ngoài