.mpy 文件中的原生机器码¶
本节介绍如何构建和使用包含来自 Python 以外语言的原生机器码的 .mpy 文件。这使你能够用 C 之类的语言编写代码,将其编译并链接成 .mpy 文件,然后像导入普通 Python 模块那样导入该文件。这可用于实现对性能要求严苛的功能,或用于引入以其他语言编写的现有库。
使用原生 .mpy 文件的主要优势之一是,脚本可以动态导入原生机器码,而无需重新构建主 MicroPython 固件。这与 MicroPython 外部 C 模块 形成对比,后者同样允许用 C 定义自定义模块,但它们必须被编译进主固件镜像。
这里的重点是使用 C 构建原生模块,但原则上任何能够被编译为独立机器码的语言都可以放入 .mpy 文件中。
原生 .mpy 模块使用 mpy_ld.py 工具构建,该工具位于项目的 tools/ 目录中。此工具接受一组目标文件(.o 文件)并将它们链接在一起,从而创建一个原生 .mpy 文件。它需要 CPython 3 以及 pyelftools v0.25 或更高版本的库。
支持的特性与限制¶
.mpy 文件可以包含 MicroPython 字节码和/或原生机器码。如果它包含原生机器码,那么该 .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(带压缩指令的 32 位 RISC-V,例如 ESP32C3、ESP32C6)rv64imc(带压缩指令的 64 位 RISC-V)
如果所选平台支持显式的架构标志,并且你希望输出的 .mpy 文件携带这些标志的值,那么在构建 .mpy 文件时必须将它们传递给 ARCH_FLAGS 标志变量。
在编译和链接原生 .mpy 文件时必须选定架构,相应的文件只能在该架构上导入(如果存在架构标志,则只有在它们与目标的能力相匹配时才能导入)。有关 .mpy 文件的更多细节,请参阅 MicroPython .mpy 文件。
原生代码必须被编译为位置无关代码(PIC)并使用全局偏移表(GOT),不过其具体细节因架构而异。在导入包含原生代码的 .mpy 文件时,导入机制能够对原生代码执行一些基本的重定位。这包括对 text、rodata 和 BSS 段的重定位。
链接器和动态加载器支持的特性有:
可执行代码(text)
只读数据(rodata),包括字符串和常量数据(数组、结构体等)
清零数据(BSS)
text 中指向 text、rodata 和 BSS 的指针
rodata 中指向 text、rodata 和 BSS 的指针
已知的限制有:
不支持 data 段;变通方法:使用 BSS 数据并显式初始化数据值
不支持静态 BSS 变量;变通方法:使用全局 BSS 变量
在 rv32imc 上不支持线程局部存储变量;变通方法:使用全局 BSS 变量,或在堆上分配一些空间来存储它们
因此,如果你的 C 代码有可写数据,请确保该数据被定义为全局的、不带初始化器,并且只在函数内部写入。
原生模块不会自动与诸如 libm.a 和 libgcc.a 之类的标准静态库链接,这可能导致 undefined symbol 错误。你可以通过在 Makefile 中设置 LINK_RUNTIME = 1 来链接运行时库。也可以通过添加 MPY_LD_FLAGS += -l path/to/library.a 来链接自定义静态库。请注意,这些库会被链接进原生模块,不会与其他模块或系统共享。
链接器限制:原生模块不会与完整 MicroPython 固件的符号表链接。相反,它会与 mp_fun_table(位于 py/nativeglue.h 中)里一份明确的导出符号表链接,该表在固件构建时即已固定。因此,除非某个任意的 HAL/OS/RTOS/系统函数位于固定地址,否则不可能简单地调用它。在这种情况下,可以通过 --externs 命令行参数将一个包含一系列符号名及其固定地址的链接脚本路径传递给 mpy_ld.py。这样,出现在链接脚本中的符号将优先于目标文件提供的内容,但目前目标文件的实现仍会驻留在最终的 MPY 文件中。链接脚本解析器的能力有限,目前仅用于解析 ESP8266 移植的 ROM 符号列表(参见 ports/esp8266/boards/eagle.rom.addr.v6.ld)。
可以将新符号添加到表的末尾并重新构建固件。这些符号还需要被添加到 tools/mpy_ld.py 的 fun_table 字典中的相同位置。这样 mpy_ld.py 就能够识别新符号,并在导入 mpy 时为它们提供重定位。最后,如果该符号是一个函数,则应在 py/dynruntime.h 中添加一个宏或桩函数,以便于调用该函数。
定义原生模块¶
一个原生 .mpy 模块由一组用于构建 .mpy 的文件定义。文件系统布局由两个主要部分组成:源文件和 Makefile:
在最简单的情况下,只需要一个 C 源文件,它包含将被编译进 .mpy 模块的所有代码。这个 C 源代码必须包含
py/dynruntime.h文件以访问 MicroPython 动态 API,并且至少要定义一个名为mpy_init的函数。该函数将是模块的入口点,在模块被导入时调用。如有需要,模块可以拆分为多个 C 源文件。模块的部分内容也可以用 Python 实现。所有源文件都应通过将它们添加到
SRC变量(见下文)中列在 Makefile 里。这既包括 C 源文件,也包括将被纳入最终 .mpy 文件的任何 Python 文件。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 能正常工作(在 Makefile 中查找 __PICOLIBC_ERRNO_FUNCTION 并从那里顺藤摸瓜)。
更多示例¶
请参阅 examples/natmod/ 以获取更多示例,它们展示了原生 .mpy 模块的许多可用特性。这些特性包括:
使用多个 C 源文件
在 C 代码旁包含 Python 代码
rodata 和 BSS 数据
内存分配
使用浮点数
异常处理
包含外部 C 库