# Micropython_extend_example **Repository Path**: Mars.CN/micropython_extend_example ## Basic Information - **Project Name**: Micropython_extend_example - **Description**: 使用 C 扩展 Micropython 的教程和例子 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 0 - **Created**: 2024-01-10 - **Last Updated**: 2025-04-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 在 ESP-IDF 环境下,使用标准 C 扩展 Micropython 模块 ## 一、 安装 ESP-IDF 环境 在其他课程中讲过,这里不再赘述,有机会再出教程吧,但需要注意的是,截止到 2024年1月初,最稳定的 micropython 开发环境是 ESP-IDF_4.4.6,最新的 5.x 对 ESP32-S3 不是很友好,反正我是没有搞成功过,在 git 上提问也没有能得到满意的回答,建议大家还是用 idf 4.x + micropython 1.19.1开发,本教程也是围绕这两个版本讲解的。 本教程都是在 Ubuntu 上开发的,Window 搭建的 ESP-IDF 开发环境开发普通的程序还可以,但开发 micropython 的环境始终没有建好过,编译各种不通过。本人才疏学浅,希望有能力的大佬补充如何在 Window 开发 micropython。 ## 二、 下载 micropython 源码 micropython 的源码是从 github 上直接克隆下来的,目前最新的代码是 1.23,但这个版本我下载试过了,对 ESP32-S3 不是很友好,各种编译失败,我试验最稳定的版本是 1.19.1,但这个版本需要配合 ESP-IDF 4.x 开发,1.20 以后的版本可以用 ESP-IDF 5.x,ESP32 可以编译通过, S3 各种报错,所以不建议使用。 克隆 1.19.1 可以使用下面的命令: ```sh git clone -b v1.19.1 https://github.com/micropython/micropython.git ``` 按照官方文档的要求,第一次使用的时候先要对 micropython 进行核心的交叉编译: ```sh $ cd mpy-cross $ make ``` 第二步需要解决的是单片机相关的子模块依赖。 在 micropython/ports 文件夹中,就是所有支持的单片机和开发板了,我们用到的是 esp32 这个文件夹,不要对其进行直接修改,需要把 esp32 复制一份,我们起名为 moopi (这个名字大家随心,随便起啥都行,不要用中文),今后所有的教程我们都从这里开始。 进入到这个文件夹,并下载所有子模块的依赖: ```sh cd ports cp -r esp32 moopi cd moopi make submodules ``` `make submodules` 不是每次都需要运行,针对一个开发板,运行一次即可,后面在对 esp32 进行复制,就不必要重复执行了。 准备完毕后,用 VSCode 打开项目目录,然后 Ctrl+Shift+P,或者单击菜单的 查看 -> 命令面板 选项打开命令面板,在其中输入 `ESP-IDF: Add vscode configuration folder` ,这样会在项目目录中增加一个 .vscode 的文件夹,项目中用到的 ESP-IDF 的开发环境及头文件都会配置好,这样项目中就不会出现头文件红线的问题了。 接下来需要修改一下 micropython 头文件的路径问题,打开 .vscode/c_cpp_properties.json 文件,在 includePaht 项中增加以下内容: ``` "${workspaceFolder}/../../" ``` 此时大部分 micropython 的头文件都不会飘红了(但仍然有一小部分需要解决)。 ## 三、 编译原始 micropython 代码 再开始编译代码之前,必须要对 menuconfig 进行修改。 首先修改编译目标为 esp32-s3 ,可以在 VSCode 中单击 ESP-IDF Set Espressif device target,选择 esp32s3,然后再弹出的下拉选项中选择 ESP32-S3 chip (via ESP USB Bridge),或者使用命令行:`idf.py set-target esp32s3` 进行配置。 然后单击打开 ESP-IDF 的配置项,单击 ESP-IDF SDK Configuration Editor (menuconfig),稍等片刻会打开 menuconfig 配置项,或者使用命令行 `idf.py menuconfig` 打开 menuconfg 进行配置 根据官方的 ESP32-S3 开发板,需要配置如下项: 1. 选中 Serial flasher config -> Enable Octal Flash 选项,因为在官方的 ESP32-S3 开发板上,用的是 8 线 Flash ,如果不启用这个选项,启动的时候会出问题; 2. Serial flasher config -> Flash Sampling Mode 设置为 DTR Mode,这是设置 Flash 的取样模式,DTR 比 STR 模式要块一倍,我理解是 STR 模式下, SPI-Flash 只会在时钟的下降沿或上升沿做数据取样,而如果使用了 DTR,则会在上升沿和下降沿各取样一次,所以速度会比原来快一倍; 3. Serial flasher config -> Flash Spi speed 设置为 80MHz,之前我们自己做过一个产品,用的和官方 Flash 是同一个型号的,但是只支持 40MHz,这个取决于具体用的 Flash 型号是什么,官方开发板设置 80MHz 完全没问题; 4. Serial flasher config -> Flash size 设置为 32M,我用的开发板是 N32R8V 这个型号的,板载的是 32M Flash 5. 修改分区表,micropython 官方代码中给的分区表最大是 16M 的 Flash 支持,这里可以选择不修改,或者修成官方的 16M ,或者修改成我下面给出的 32M 的都可以,不太影响我们本次可成讲到的东西,如果需要配置自定义的分区表,则需要修改 Partition Table -> Partition Table 选择 Custom partition table CVS即可,此时会多出 Custom partition CSV file,这里我们可以选择文件夹下任意一个分区表配置文件(.cvs文件)即可,记得名称要写对,否则编译会报错,这里我选择的是我自己写的 32M 分区表 partitions-32MiB.csv; 6. 添加扩展 PSRAM,ESP32-S3 内置的 SRAM 大小是 512K,虽然已经不小了,但是对于我们上位机程序员来说,这就是个渣渣,而 N32R8V 开发板已经很贴心的给我们内置了 8M 的 PSRAM,但默认情况下是没有挂载的,需要我们通过 menuconfig 挂载,只需要选中 Component config -> ESP32S3-Specific -> Support for external, SPI-connected RAM 即可打开; 1. 选中后再本级菜单中会增加 SPI RAM config 选项; 2. 进入选项 将 Mode (QUAD/OCT) of SPI RAM chip in use 选项选择为 Octal Mode PSRAM,因为在这块板子中,用的扩展 PSRAM 也是八线的。 3. 关于 Cache fetch instructions from SPI RAM 和 Cache load read only data from SPI RAM 是否需要选中视情况而定,如果我们在代码运行过程中有可能操作 Flash 的代码区域或者数据区域,这两块最好选中,大概的意思是命令或数据的缓存预处理一类的; 4. Set RAM clock speed 设置为 80MHz ,40MHz 也能用,就是慢,无他 以上都设置完毕后,保存退出,最后一步,需要修改一下 ESP32-S3 的外设配置,这个芯片哪哪都好,就是缺少 DAC ,所以在编译的时候,需要把 DAC 外设关闭。 打开 mpconfigport.h 文件,大概在 103 行左右的地方,有个 MICROPY_PY_MACHINE_DAC 的配置,默认值是 1 ,改为 0 即可。 最后单击 ESP-IDF Build project 按钮,或者在命令行中执行 `idf.py build` 即可编译整个工程,理论上是不会出现任何错误的,如果有,则看看前面的配置是否正确。 编译完成后,单击 ESP-IDF Build,Flash and Monitor 下载查看工程,或者执行命令: ```sh idf.py flash idf.py monitor ``` 此时已经可以正常进入 micropython 的 REPL 环境了。 此时如果出现 ``` The filesystem appears to be corrupted. If you had important data there, you may want to make a flash snapshot to try to recover it. Otherwise, perform factory reprogramming of MicroPython firmware (completely erase flash, followed by firmware programming). ``` 的错误,则有可能是 Flash 的 FAT 分区出现了问题,只需要把 Flash 擦除一下在烧录就行了,Flash 擦除命令是`idf.py erase_flash`,等一两分钟就能擦除完毕了,然后再重新烧录一次即可。 我们可以尝试以一下输入 `help("modules")`,即可看到已经加载的 python 模块 ``` _boot gc ubinascii urandomrm _onewire inisetup ubluetooth ure _thread math ucollections uselect _uasyncio micropython ucryptolib usocket _webrepl neopixel uctypes ussl apa106 network uerrno ustruct btree ntptime uhashlib usys builtins onewire uheapq utime cmath uarray uio utimeq dht uasyncio/__init__ ujson uwebsocket ds18x20 uasyncio/core umachine uzlib esp uasyncio/event uos webrepl esp32 uasyncio/funcs upip webrepl_setup flashbdev uasyncio/lock upip_utarfile websocket_helper Plus any modules on the filesystem ``` 后面的操作中,我们最后使用 Thonny 软件,这个天然支持 ESP32 开发板,非常好用,我们自己的 IDE 也在开发中,功能设计是在 Thonny 基础上增加了更多支持 ESP32 和我们自己开发平台的功能。 ## 四、 添加自定义的 micropython 模块 我们本次课程的目的是教会大家如何在 micropython 环境中扩展自定义的模块,所以具体 micropython 如何使用,我们会放到其他课程中展示,这里就不多说了。 如果我们是做平台开发的,或者说我们的产品需要给另外一些同行做二次开发的,我们就需要把我们自己的一些功能封装起来,提供给第三方使用。最长见的方法是就是写 python 脚本,提供给客户 .pyc 的二进制文件,或者 .py 的源码文件。但对于一些保密性要求高的,或者说直接操作特有硬件的,或者是要求执行效率的代码,使用 python 写就不是那么舒服了,所以我们就需要使用扩展 micropython 类库的方式来做,这也是 python 高级编程的一部分。简单来说,就是使用 C/C++ 扩展 python 类库。 在 micropython 环境中,一切都会被封装到 模块中,也就是 module,module 又包含了方法和类,以及常量,类中又可以包含方法、属性和常量等等。 本小节,我们就从创建一个 module 开始,能够通过在 REPL 环境中执行 `help("modules")` 看到我们的模块。 我们知道,在python 中写一个 .py 的文件就是一个 module ,就可以使用 import 导入,但在 C 中开发,相对来说要复杂一些,好处就是可以提高执行效率。 创建模块一共分五步: 1. 定义模块的全局字典 gloabls table; 2. 将全局字典转换为 micropython 对象; 3. 定义模块原型; 4. 注册模块; 5. 将模块添加到编译的配置文件中。 在使用 C 扩展 micropython 之前,需要先引入几个头文件 ```C #include "py/builtin.h" #include "py/runtime.h" #include "py/obj.h" #include "py/binary.h" ``` 这些都是扩展 micropython 必要的。 在项目根目录新建一个 moopi_mod 文件夹(名字随意,不要用中文),用于存放我们接下来的课程代码。 在这个文件夹下新建一个文件,我的名字叫 modmoopi.c ### 4.1 定义全局字典 每个模块或者类都应该有一个全局字典,这个字典中定义了模块或者类中的所有成员,包括方法、常量、子类等,在扩展 micropython 中,用于全局自定用以下代码: ```C STATIC const mp_rom_map_elem_t moopi_globals_table[] = { {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_moopi)}, }; ``` 这行代码中需要修改的是 moopi 的部分,这是我模块的名字,你们自己的可以自行定义。 这个字典是一个 mp_rom_map_elem_t 类型的数组,每个成员有两个元素,分别是成员的名称和对应的对象。其中类似 `MP_ROM_QSTR` 这些宏定义其实是 micropython 内部做对象转换用的,我们不必深究是什么意思,拿过来用就行了,有机会我们再拆开讲。 在写字典的时候必须注意以下几点: 1. 字典中每个元素必须用 {} 包围,而且每个元素中包含 key 和 value 两个元素; 2. 每个元素的 key 值必须是 QSTR 类型的,也就是必须通过 MP_ROM_QSTR 对其进行转译; 3. key 的值必须使用 MP_QSTR_前缀加key的真实名称,MP_ROM_QSTR 宏会自动加入到注册表中(这个比扩展 python 省事的多,免去了我们计算字符串Hash的步骤); 4. 对象的值必须是通过转译的 micropython 对象,也就是通过 MP_ROM_XXX 转译的对象 5. 字典第一行必须是一个字符串类型的对象名,key 的值必须是 `__name__`,这是 python中规定的,也就是第一个成员的 key 必须是 MP_ROM_QSTR(MP_QSTR___name__),name 前面是三个下划线,别写错了,value 的值是 QSTR 类型的字符串,这里可以直接写,不要过多考虑注册表的问题,会自动完成注册。 如果我们想改变模块的名称,只需要改变成员第一行中 value 的 `MP_QSTR_` 后面的内容即可,这个值其实并不是 import 指令导入的名称,而是模块的实际名称,import 导入名称将在第四步中介绍,但这个值是区分大小写的。 ### 4.2 将全局字典转换为 micropython 对象 这一步比较简单,只需要调用 micropython 开发环境事先定义好的宏即可 `MP_DEFINE_CONST_DICT` ```C STATIC MP_DEFINE_CONST_DICT(moopi_globals, moopi_globals_table); ``` 这个宏会传入两个参数,第一个是转后的 micropython 对象指针,这不变量不需要我们自己定义,环境会帮我们定义好,第二个参数是上面刚刚定义的对象成员列表0这一步的转换是为下一步定义对象原型做准备。 ### 4.3/4.4 定义模块原型并注册 上面两步我们完成了模块成员的定义,下面完成对象原型的定义,代码如下: ```C const mp_obj_module_t mp_mod_moopi = { .base = {&mp_type_module}, .globals = (mp_obj_dict_t *)&moopi_globals, }; MP_REGISTER_MODULE(MP_QSTR_moopi, mp_mod_moopi); ``` 第一部分,首先定义一个 `mp_obj_module_t` 类型的对象,该对象只有两个成员,第一个是 .base 这个在后面的代码中我们会多次遇到,每个 micropython 对象的第一个成员总是他,第二 .globals 个是模块的成员字典表,这个表就是我们上面所定义的数组,可以在 micropython 环境中通过 `dir(moopi)` 查看到。 模块原型定义完成后,通过 micropython 开发环境提供的 MP_REGISTER_MODULE 宏将模块注册到列表中,该宏有两个参数,第一个是模块的导入名称,也就是通过 import 指令导入时候的名称,区分大小写,第二个就是上面创建的原型。 ### 4.5 将模块添加到编译的配置文件中 此时我们的代码已经写完,但无论这个文件的代码中是否有错误,编辑器都不会报错,因为我们写的这个代码压根就没有参与编译,如果需要自己的代码起作用,还必须修改 CMakeLists.txt 文件,确切的说是要修改 man/CMakeLists.txt 文件,把我们的代码加入进去。 #### 4.5.1 加入源文件列表 我们这里的源文件可能会分为两部分,分别是普通 c 代码,和参与 micropython 扩展的代码,所以在添加源文件的时候最好是能够将两部分代码分开。 在源文件大概 50 行左右(位置无所谓,只要是在 set(MICROPY_SOURCE_PORT 之前均可)添加我们自己的代码集合,可以使用以下两种方式任意添加 **暴力方式** ``` set(MOOPI_DIR ../moopi_mod) file(GLOB_RECURSE MOOPI_MOD_SRCS ${MOOPI_DIR}/*.c) ``` 这种方式我们会添加这个文件夹里的所有 .c 的文件,不便于区分,但如果这个文件夹内所有文件都属于这一组的,用这种方式添加最为省心。 **细心方式** ``` set(MOOPI_DIR ../moopi_mod) set(MOOPI_MOD_SRCS ${MOOPI_DIR}/modmoopi.c ) ``` 这种方式是按照单个文件添加的,在增加一个 .c 文件的时候需要记得修改这个变量,否则不会参与编译。 不论用那种方式,这两行代码都是一个意思,定义 MOOPI_DIR 变量,值是 ../moopi_mod 指向了 moopi_mod 文件夹;定义 MOOPI_MOD_SRCS 变量,内容是所有参与编译的源文件。 如果还有其他不参与 micropython 编译的源文件,建议再增加一个 MOOPI_SRCS 变量单独存放。 变量定义完毕后,需要在后面 MICROPY_SOURCE_PORT 变量定义的最后面,加上我们的源文件变量指向 `${MOOPI_MOD_SRCS}`。 MICROPY_SOURCE_PORT 存放的是参与 micropython 环境编译的代码,如果源文件中不存在与 micropython 扩展相关的代码,没必要放在这里,修改后如下: ``` set(MICROPY_SOURCE_PORT ${PROJECT_DIR}/main.c ${PROJECT_DIR}/uart.c ... ${PROJECT_DIR}/machine_rtc.c ${PROJECT_DIR}/machine_sdcard.c ${MOOPI_MOD_SRCS} ) ``` 再往下,找到 idf_component_register 部分,这里才是真正注册编译代码的部分,不论是否参与 micropython 的编译,我们的源码变量必须放在这里,头文件变量也必须加在这里。 在 SRCS 目录下,加入 `${MOOPI_MOD_SRCS}`,在 INCLUDE_DIRS 目录下,加入 `${MOOPI_DIR}`,如下: ``` idf_component_register( SRCS ${MICROPY_SOURCE_PY} ${MICROPY_SOURCE_EXTMOD} ${MICROPY_SOURCE_SHARED} ${MICROPY_SOURCE_LIB} ${MICROPY_SOURCE_DRIVERS} ${MICROPY_SOURCE_PORT} ${MICROPY_SOURCE_BOARD} ${MOOPI_MOD_SRCS} INCLUDE_DIRS ${MICROPY_INC_CORE} ${MICROPY_INC_USERMOD} ${MICROPY_PORT_DIR} ${MICROPY_BOARD_DIR} ${CMAKE_BINARY_DIR} ${MOOPIDIR} REQUIRES ${IDF_COMPONENTS} ) ``` 添加完毕后编译代码,无错误,烧录进开发板,进入 REPL 环境,输入 `help("modules")`,即可打印出我刚刚添加的 moopi 模块,通过 `import moopi` 指令可以正常导入模块,通过`dir(moopi)` 可以打印出这个模块的所有成员,目前只有默认的两个 `['__class__', '__name__']`。 执行 `moopi.__name__` 即可看到模块的名字,这个名字就是在上面第一部中定义成员字典时填入的名称。 执行 `moopi.__class__` 可以看到,这个对象的类型是 module。 到此为止,第一步,创建模块已经成功。 ## 五、 为模块添加方法 在上面一节中,我们已经在 micropython 环境中完成了自定义模块的添加,但此时模块中空空如也,什么也没有,这一节我们就为其添加一个方法。 模块中可以包含 方法、常量、类这些东西,最基础的就是方法,方法又分为无参数、有参数、可变参数(重载)、有返回值、无返回值这些,接下来的部分,会对其进行一一介绍。 ### 5.1 添加一个无参无返回值方法 在 micropython 方法原型中,不存在无返回值类型方法,所有方法的定义必须是一个包含有 mp_obj_t 类型返回值的,无参方法运行定义如下: ```C STATIC mp_obj_t func_name(){ return mp_const_none; } ``` 如果在 micropython 环境不需要返回值,在 C 扩展的时候,返回值恒定为 mp_const_none; 再此,我们定一个 say_hello 的方法: ```C STATIC mp_obj_t moopi_say_hello(){ printf("Hello Micropython !\n"); return mp_const_none; } ``` 方法定义完成之后,还需要将方法转换为 micropython 对象才可以使用,这个转换可以通过 micropython 开发环境提供的宏进行: ```C MP_DEFINE_CONST_FUN_OBJ_0(moopi_say_hello_obj, moopi_say_hello); ``` 这个宏有两个参数,第一个参数值被转换出来的 micropython 对象指针,第二个对象是要转换的方法指针,这行宏定义调用完之后,将会生成一个 moopi_say_hello_obj 的 micropython 对象,最后,将这个对象写入到上一节定义的成员字典中: ```C STATIC const mp_rom_map_elem_t moopi_globals_table[] = { {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_moopi)}, {MP_ROM_QSTR(MP_QSTR_sayHello), MP_ROM_PTR(&moopi_say_hello_obj)}, }; ``` 成员的 key 格式不变,仍然转换为 QSTR 类型的,在环境中使用的名称是 sayHello,而在转换 value 的时候,使用的是 MP_ROM_PTR,内容是对转换出来的 micropython 方法对象地址。 此时,第一个方法已经构建完成,编译烧录运行,进入 REPL 环境,执行: ```python import moopi moopi.sayHello() ``` 即可验证方法有效性。 ### 5.2 带参数的方法 上一小节中,我们为模块创建了一个不带参数的方法,细心的小盆友可能已经注意到了,在调用 `MP_DEFINE_CONST_FUN_OBJ_0` 宏的时候,还有一堆相似的宏: ```C MP_DEFINE_CONST_FUN_OBJ_0 MP_DEFINE_CONST_FUN_OBJ_1 MP_DEFINE_CONST_FUN_OBJ_2 MP_DEFINE_CONST_FUN_OBJ_3 MP_DEFINE_CONST_FUN_OBJ_VAR MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN MP_DEFINE_CONST_FUN_OBJ_KW ``` 这就为我们创建不同数量参数的方法提供了多种可能性: * MP_DEFINE_CONST_FUN_OBJ_0 : 创建一个没有参数的方法 * MP_DEFINE_CONST_FUN_OBJ_1 : 创建一个带有1个参数的方法 * MP_DEFINE_CONST_FUN_OBJ_2 : 创建一个带有2个参数的方法 * MP_DEFINE_CONST_FUN_OBJ_3 : 创建一个带有3个参数的方法 * MP_DEFINE_CONST_FUN_OBJ_VAR : 创建一个带有指定个最少参数的方法 * MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN : 创建一个带有从 x 到 y 个参数的方法 * MP_DEFINE_CONST_FUN_OBJ_KW : 创建一个使用字典传值的方法 #### 5.2.1 MP_DEFINE_CONST_FUN_OBJ_1 在 micropython 的方法扩展开发中,所有传入的参数类型必须是 mp_obj_t 类型的,在 micropython 代码环境中,无论传入的是什么类型的数据,都将会被封装为 mp_obj_t 类型,进入函数后,可以通过一些方法从其中将真实数据转换出来。 | 方法 | 转换类型 | |-|-| | mp_obj_str_get_str | const char * | | mp_obj_get_int | int32 | | mp_obj_get_float | float | 另外,还可以通过 mp_obj_get_type_str 和 mp_obj_get_type 查看是什么类型的,前者返回一个类型的字符串表达形式,后者则是返回一个 mp_obj_type_t 类型的对象用于内部比较用。 为代码增加一个 sayHi 的方法,传入一个字符串型参数,并打印输出 ```C STATIC mp_obj_t moopi_say_hi(mp_obj_t name){ const char *n = mp_obj_str_get_str(name); printf("Hi %s\n", n); return mp_const_none; } MP_DEFINE_CONST_FUN_OBJ_1(moopi_say_hi_obj, moopi_sya_hi); ``` **(记得把方法添加到字典中)** 调用这个方法的时候,使用 `moopi.sayHi("Mars.CN")` 即可在控制台进行输出,但如果传入一个非字符串值,则会报错。 ```python import moopi moopi.sayHi(1) raceback (most recent call last): File "", line 1, in TypeError: can't convert 'int' object to str implicitly ``` 所以,我最好在方法中对值的内容进行校验,当值类型不对的时候直接告诉用户要比报错有好得多。 ```C STATIC mp_obj_t moopi_say_hi(mp_obj_t name){ if(mp_obj_get_type(name) == &mp_type_str){ const char *n = mp_obj_str_get_str(name); printf("Hi %s\n", n); }else{ printf("Please enter a value of string type !\n"); } return mp_const_none; } MP_DEFINE_CONST_FUN_OBJ_1(moopi_say_hi_obj, moopi_sya_hi); ``` #### 5.2.2 MP_DEFINE_CONST_FUN_OBJ_2 以及返回值 知道一个参数方法如何定义了,接下来是 2 个参数或多个参数的,使用方法一样,只不过是在定义方法是MP_DEFINE_CONST_FUN_OBJ_2,参数表中多几个变量罢了。 而对于返回值,同样,进入的时候是 mp_obj_t 类型的,返回的时候也必须是 mp_obj_t 类型的,如果需要将 C 对象转换为 micropython 对象,则需要调用以下方法: | C数据类型 | 转换函数 | |-|-| | int | mp_obj_new_int | | bool | mp_obj_new_bool | | float | mp_obj_new_float | | double | mp_obj_new_float | | char * | mp_obj_new_str | 转换后可以直接返回但最好通过 micropython 开发环境提供的宏在进行一次封装转换 `MP_OBJ_FROM_PTR`, 这个宏的意思其实就是强制转换类型为 mp_obj_t 类型,没有其他别的意思,所以带不带都问题不大,带上之后在一些特殊场合不会报警告。 ```C STATIC mp_obj_t moopi_add(mp_obj_t va, mp_obj_t vb){ int32_t a = mp_obj_get_int(va); int32_t b = mp_obj_get_int(vb); int32_t c = a+b; return MP_OBJ_FROM_PTR(mp_obj_new_int(c)); } MP_DEFINE_CONST_FUN_OBJ_2(moopi_add_obj, moopi_add); ``` **(记得把方法添加到字典中)** #### 5.2.3 MP_DEFINE_CONST_FUN_OBJ_VAR 在上面提供的转换宏中,只提供了 0~3 个参数的转换,对于 3 个以上参数的方法,不可能每一个都定义一个宏,那太啰嗦了,所以就有了 MP_DEFINE_CONST_FUN_OBJ_VAR ,对于3个以上方法的转换方式,而这个转换宏对应的函数定义格式也发生了改变: ```C STATIC mp_obj_t func_name(size_t n_args, const mp_obj_t *args); ``` 这个函数原型中有两个参数,第一个参数表示了函数传入真实参数的数量,第二个参数表示参数的列表。 在使用 **MP_DEFINE_CONST_FUN_OBJ_VAR** 对函数进行对象化转换的时候,需要输入三个参数,第一个参数仍然是转换后的 micropython 对象变量名,第二个参数是这个函数要求最小传入的参数数量(最少0个参数),最后一个参数是方法的名称。 利用这一类的函数,可以创造出参数数量可变的函数,以下是定义的一个求和函数的举例: ```C STATIC mp_obj_t moopi_sum(size_t n_args, const mp_obj_t *args){ int32_t sum = 0; for(int i=0;i", line 1, in TypeError: function missing 1 required positional arguments ``` 这就要用到了 micropython 的通知机制,micropython 开发环境中,已经贴心的为我们提供了一组专门用于向外抛出异常的函数: * mp_raise_TypeError : 类型异常 * mp_raise_ValueError : 值异常 * mp_raise_OSError : 系统异常 * mp_raise_NotImplementedError : 功能异常 抛出异常的时候需使用 `MP_ERROR_TEXT` 宏对字符串进行类型转换。 ```C STATIC mp_obj_t moopi_average(size_t n_args, const mp_obj_t *args) { if (n_args <= 5) { int32_t sum = 0; for (int i = 0; i < n_args; i++) { sum += mp_obj_get_int(args[i]); } float average = sum * 1.0f / n_args; return MP_OBJ_FROM_PTR(mp_obj_new_float(average)); } else { mp_raise_TypeError(MP_ERROR_TEXT("function expected at most 5 arguments")); } } MP_DEFINE_CONST_FUN_OBJ_VAR(moopi_average_obj, 2, moopi_average); ``` **(记得把方法添加到字典中)** 另外一种方式就是通过`mp_arg_check_num` 函数对参数进行检测,当不满足要求的时候他会自动抛出错误,这个函数原型如下: ```C static inline void mp_arg_check_num(size_t n_args, size_t n_kw, size_t n_args_min, size_t n_args_max, bool takes_kw) ``` | 参数 | 含义 | |-|-| | n_args | 实际传入参数数量 | | n_kw | 实际用字典传入的参数数量 | | n_args_min | 最小要求传入的参数数量 | | n_args_max | 最大要求传入的参数数量 | | takes_kw | 是否支持字典传值 | 最后一种方案,也是最为正规的方案,就是使用官方提供的 `MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN` 宏做函数类型转换,这个宏一共接收四个参数,依次是: 转换后的 micropython 对象变量名,最小接受参数数量,最大接受参数数量,函数名称。 ```C STATIC mp_obj_t moopi_max(size_t n_args, const mp_obj_t *args) { int32_t max = 0x80000000; for (int i = 0; i < n_args; i++) { int32_t num = mp_obj_get_int(args[i]); max = num>max?num:max; } return MP_OBJ_FROM_PTR(mp_obj_new_int(max)); } MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(moopi_max_obj, 2, 5, moopi_max); ``` **(记得把方法添加到字典中)** 以上三种方式具体使用哪种方式,需要根据具体环境决定。 #### 5.2.5 MP_DEFINE_CONST_FUN_OBJ_KW python 开发中,除了可以通过按位传值之外,还可以按照字典传值,这也是 python 特有的传值方式,现在很多开发语言争相效仿。 ```python moopi.achieve(name="Mars.CN",score=100); Mars.CN's score is 100 . ``` 对于此类函数,micropython 开发环境也已经设置好了注册方法。首先,函数原型是: ```C STATIC mp_obj_t func_name(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) ``` 参数依次是,传入参数的数量,按位传值的参数列表,按字典传值的参数列表 注册使用: ```C MP_DEFINE_CONST_FUN_OBJ_KW(obj_name, n_args_min, fun_name) ``` 按参数传值相对于按位置传值的取值方式要复杂很多,但大概分为五步: 1. 定于字典 key 的枚举 2. 定义字典取值模板 3. 声明字典数组 4. 利用 mp_arg_parse_all 函数解析字典 5. 从字典中取值 首先需要将可能按字典传入的 key 做一个枚举序列: ```C enum {ARG_name, ARG_score}; ``` **这里需要注意的是,如果函数在 python 环境中被调用是,如果参数列表中没有这个 key ,会抛出一个`extra keyword arguments given` 的异常。** 第二步,构建字典取值模板,取值模板其实是一个 `mp_arg_t` 类型的数组,该结构体中共三个值: 1. qst : 第一个值,表示字典中 key 的字符串表新形势(这个字符串是经过转码的, 具体怎么转的不需要关心) 2. flags : 第二个值,值的类型,这里只支持 bool 和 int 另种基础数据类型,其他类型的都用 mp_obj_t 类型代替 3. defval : 都三个值,默认值,是一个结构体,根据第二个参数(flags)的不同,设置不同的值类型。 ```C static const mp_arg_t allowed_args[] = { { MP_QSTR_name, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_score, MP_ARG_INT, {.u_int = 0} }, }; ``` **这里需要注意的是,字典模板中元素的排列方式要严格和第一步中枚举的顺序一致,否则有可能出错。** 第一个 key 值前缀必须是 `MP_QSTR_`;第二个参数类型可以选择 MP_ARG_OBJ 、MP_ARG_INT、MP_ARG_BOOL,除了 bool 和 int 之外,都归为 OBJ 类型;第三个参数根据第二个参数不同,可以选择.u_int,.u_bool,.u_obj三个选项,另外还有个 .u_rom_obj 应该表示的是常来常量对象,比如方法等(具体没有研究过,可能理解有误)。 对于 int 和 bool 类型,可以直接只用 C 类型常量写,但如果是其他类型的,则需要使用类型转换方式获取,MP_OBJ_NULL 和 C 中的 NULL 不同,和 mp_const_none 也不同,他表示是一个空的 micropython 对象,mp_const_none 表示的是一个空值,NULL 表示的是空指针。 第三步,声明一个参数接收数组 ```C mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; ``` 数组类型是 mp_arg_val_t 格式,使用 MP_ARRAY_SIZE 测量数组的大小。 最后,通过内置的 mp_arg_parse_all 函数将参数从列表中解析出来。 ```C mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); // 原型 void mp_arg_parse_all(size_t n_pos, const mp_obj_t *pos, mp_map_t *kws, size_t n_allowed, const mp_arg_t *allowed, mp_arg_val_t *out_vals); ``` 这个函数传入的参数表示如下: | 参数 | 含义 | |-|-| | n_pos | 位置参数的数量 | | pos | 指向位置参数的指针 | | kws | 指向关键字参数指针 | | n_allowed | 允许的参数的数量 | | allowed | 一个指向 mp_arg_t 结构体数组的指针,它描述了每个参数的类型和默认值 | | out_vals |一个指向 mp_arg_val_t 结构体的指针数组,它将包含解析后的参数值 | 解析完毕之后,就可以通过之前定义的 args 从中取值了,但取值的时候需要注意,要根据值的类型获取 args 结构体不同的成员。 该部分的代码如下: ```C STATIC mp_obj_t moopi_achieve(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args){ enum {ARG_name, ARG_score}; static const mp_arg_t allowed_args[] = { { MP_QSTR_name, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_score, MP_ARG_INT, {.u_int = 0} }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); char res[200]; sprintf(res,"%s's score is %d .", args[ARG_name].u_obj==MP_OBJ_NULL?"":mp_obj_str_get_str(args[ARG_name].u_obj), args[ARG_score].u_int); return MP_OBJ_FROM_PTR(mp_obj_new_str(res,strlen(res))); } MP_DEFINE_CONST_FUN_OBJ_KW(moopi_achieve_obj, 0, moopi_achieve); ``` **(记得把方法添加到字典中)** 代码中,name 的默认值是空对象,在打印的过程中,如果对象为空,则输出空串,而对于数值类型的,则可以直接从对象的 .u_int 中获取值。 最后通过 `MP_DEFINE_CONST_FUN_OBJ_KW` 转换函数为 micropython 对象,第二个参数指的是按位置传值的最少参数个数。 ### 5.3 函数的重载(Overload) 在面向对象开发的过程中,经常会遇到函数重载的操作,就是一个函数名称,根据参数类型不同所执行的操作不同,在标准 C 语言开发中是不支持函数重载的,但是重载却是 Python 语言的一大特性。所以在使用 C 扩展 micropython 的过程中,我们可以巧妙的利用函数传值的数量、类型等对函数实现重载。 比如有一个需求:有一个函数名称为 size,当直接调用这个参数的时候,返回对象的宽高,但其可以携带一个参数,如果携带一个参数的时候,则同时设置对象的宽高,如果携带两个参数时候分别设置对象的宽高。 分析可知: 1. 参数名成为 size 2. 可能携带 0~2 个参数,可变的 3. 返回值为元祖类型 所以,我们设计函数的时候需要根据传入参数的数量进行判断,如果没有传入参数,什么都不做,如果 n_args ==1,同时设置对象的宽和高,如果 n_args ==2 这表示要分别设置对象的宽和高。 最后,不论是否传入参数,都返回一个元祖对象(Tuple),元祖中有两个数字值,表示对象的宽和高。 对于判断参数数量的方式,前面的代码中已经讲过,而之前函数中,我们返回的都是基础数据类型,对于 micropython 内置数据类型的返回是第一次遇到。 元组(Tuple)是一个不可变的序列,可以包含任意类型的数据,用圆括号 () 包围起来,简单来说,元祖其实就是一个不可变的数组。 在 C 扩展 micropython 的时候,可以通过 `mp_obj_new_tuple` 创建一个元祖,该函数有 2 个参数,第一个参数是元祖内数据的数量,第二个参数是一个 mp_obj_t 类型的数组,也就是或,即便是我们返回的是 int 类型数据,你也必须转换成 mp_obj_t 类型的数据,而基础数据类型的数据转换在之前的函数中已经讲到,这里不再重复。 ```C static uint32_t width=0,height=0; STATIC mp_obj_t moopi_size(size_t n_args, const mp_obj_t *args){ if(n_args==1){ width = height = mp_obj_get_int(args[0]); }else if(n_args==2){ width = mp_obj_get_int(args[0]); height = mp_obj_get_int(args[1]); } mp_obj_t res[2] = { mp_obj_new_int(width), mp_obj_new_int(height), }; return MP_OBJ_FROM_PTR(mp_obj_new_tuple(2,res)); } MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(moopi_size_obj, 0, 2, moopi_size); ``` **(记得把方法添加到字典中)** 以上函数根据参数数量的不同执行不同的操作,在 python 环境中,我们可以使用以下代码实验函数重载: ```python import moopi moopi.size() (0,0) moopi.size(100) (100,100) moopi.size(100,200) (100,200) ``` **注意,这个函数有可能会报错** 在编译的时候,有可能会报一些关键字被占用的错误: ```C /home/mars/esp/micropython/ports/moopi/build/frozen_content.c:316:5: error: redeclaration of enumerator 'MP_QSTR_size' MP_QSTR_size, ``` 这是因为 size 这个关键字初次已经被 micropython 自己的一些函数注册过了,我们自定义的类或者模块中使用这些字段的时候会重复注册,所以报这个错。 解决方案分两种,一种是换一个关键字用,当然,这种方法我们肯定不愿意妥协,所以大多时候采用第二种方法。 使用 idf.py fullclean 清理整个项目,或者单击 ESP-IDF Full Clean 按钮清理,但清理项目后记得要重新配置 menuconfig 这也挺麻烦的。 还有中方法就是只删除 build 文件夹中的 frozen_content.c 文件,重新编译即可。 另外,还可以根据函数的类型不同,实现不同类型的重载。 下面函数中我们做一个设置或查询对象位置的函数 location ,这个函数除了可以接受向 size 一样的两种参数中之外,还可以接受通过元祖(Tuple)或者列表(List)的参数设置。 ```C static int32_t x=0,y=0; STATIC mp_obj_t moopi_location(size_t n_args, const mp_obj_t *args){ if(n_args==1){ const mp_obj_type_t *type = mp_obj_get_type(args[0]); if(type == &mp_type_int){ x = y = mp_obj_get_int(args[0]); }else if(type == &mp_type_tuple){ mp_obj_tuple_t *t = MP_OBJ_TO_PTR(args[0]); if(t->len==2){ x = mp_obj_get_int(t->items[0]); y = mp_obj_get_int(t->items[1]); } }else if(type == &mp_type_list){ mp_obj_list_t *t = MP_OBJ_TO_PTR(args[0]); if(t->len==2){ x = mp_obj_get_int(t->items[0]); y = mp_obj_get_int(t->items[1]); } } }else if(n_args==2){ x = mp_obj_get_int(args[0]); y = mp_obj_get_int(args[1]); } mp_obj_t res[2] = { mp_obj_new_int(x), mp_obj_new_int(y), }; return MP_OBJ_FROM_PTR(mp_obj_new_tuple(2,res)); } MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(moopi_location_obj, 0, 2, moopi_location); ``` **(记得把方法添加到字典中)** 上面参数中出现了一个新的方法 `MP_OBJ_TO_PTR` 和 `MP_OBJ_FROM_PTR` 是对应关系,前者是将任意的 mp_obj_t 类型对象转化能成为 void * 类型的对象,后者是将任意指针对象转换为 mp_obj_t 类型的对象,其实这两个宏有没有不会影响代码的运行,但是在编译过程中有可能报警告。 ## 六、 为模块添加常量 在有些场合,模块或者类中都需要用到预设的常量,本小节,我们将为模块加入两种基础类型的常量,在类中加入常量的方式也是一样的。 常量的加入直接修改字典列表即可,在字典列表中,key 和方法的添加方式是一样的,常量的值一般是 int 类型的,或者字符串类型的,所以可以通过 MP_ROM_INT 或者 MP_ROM_QSTR 转译值,用 MP_ROM_QSTR 做简单的字符串常量还可以,如果稍微复杂的就不行了,具体如何做复杂字符串,希望其他大佬们能给个方案。 ```C {MP_ROM_QSTR(MP_QSTR_ROTATE_0), MP_ROM_INT(0)}, {MP_ROM_QSTR(MP_QSTR_ROTATE_90), MP_ROM_INT(1)}, {MP_ROM_QSTR(MP_QSTR_ROTATE_180), MP_ROM_INT(2)}, {MP_ROM_QSTR(MP_QSTR_ROTATE_270), MP_ROM_INT(3)}, {MP_ROM_QSTR(MP_QSTR_STR), MP_ROM_QSTR(MP_QSTR_My_string)}, ``` 可以通过 `dir(moopi)` 查看已经存在的函数及添加的常量。 ## 七、 为模块添加类 类是面向对象编程的基本概念之一。它允许你创建具有特定属性和方法的自定义对象。类是对象的蓝图,它定义了对象的行为和状态。在 C 扩展 micropython 的中,类不仅可以包含函数和常量,还可以包含有静态函数、属性,以及特殊方法,比如构造函数、析构函数、打印函数、子集等等。 ### 7.1 构造基础类 构造类和构造模块非常类似,按顺序共分为五步: 1. 定义类的类型结构体 2. 构建全局成员字典 3. 将全局字典转换为 micropython 对象; 4. 定义类原型; 5. 将类添加到模块中; **记得将 .c 文件加入到编译列表** 为了增强代码可读性,建议每个类一个 .c 文件,并把公共的部分放在 .h 文件中。所以本次我们需要增加两个文件,moopi.h 和 modobject.c #### 7.1.1 定义类的类型结构体 在 C 扩展 micropython 过程中,一切对象都始于一个 C 的传统结构体,通过这个结构体,得以让 C 和 micropython 进行数据交互,所以每个自定义的类都要包含一个这样的结构体,要求是结构体的第一个成员为 mp_obj_base_t 类型数据,在 mp_obj_base_t 中存放了该类实例的类型、构造函数、析构函数、call函数、打印函数等等,同时这个成员还是递归的, mp_obj_base_t 首个成员仍然是他自己。 除此之外,结构体中就是存放我们这个类所需的一些用于驻留内存的数值了,比如我们例程中需要创建一个名字叫 Object 的类,包含 x、y、width、height、parent 几个成员。 ```C typedef struct moopi_object moopi_object_t; struct moopi_object{ mp_obj_base_t base; int16_t x; int16_t y; uint16_t width; uint16_t height; moopi_object_t *parent; }; ``` *这段代码定义完了,但暂时我们还用不到* #### 7.1.2 定义类成员字典并转换成 micropython 对象 我们把所有的类型结构体放在 moopi.h 中,这样方便其他文件调用。 在 micropython 中,类的成员字典和模块的成员字典定义方式类似,都是定义一个 mp_rom_map_elem_t 类型的数组,然后通过 `MP_DEFINE_CONST_DICT` 宏将其转换为 micropython 对象,不同之处在于,定义类成员字典的时候不用写 __name__ 属性。 ```C const mp_rom_map_elem_t moopi_object_local_dict_table[] = { }; STATIC MP_DEFINE_CONST_DICT(moopi_object_local_dict, moopi_object_local_dict_table); ``` #### 7.1.3 定义类原型 在上面几节的函数测试中已经说明,在 C 扩展 Micropython 的过程中,一切传值都是 mp_obj_t 类型的,而这个类型的数据都有一个类型字段,可以通过 mp_obj_get_type 函数获得,所以我们自定义的类也必须有这样一个原型,就像定义模块一样。 ```C const mp_obj_type_t moopi_type_object = { {&mp_type_type}, .name = MP_QSTR_Object, .locals_dict = (mp_obj_dict_t *)&moopi_object_local_dict, }; ``` 定义类原型使用的是 mp_obj_type 类型,必填的有三个成员,第一个恒定为 `{&mp_type_type}` 不可修改,第二个是类的名称,就是通过 mp_obj_get_type_str 函数获取的物理名称;第三个成员是类的成员字典。 #### 7.1.4 加入模块 回到 modmoopi.c 文件中,在类成员中加入新创建的类。 ```C {MP_ROM_QSTR(MP_QSTR_Object), MP_ROM_PTR(&moopi_type_object)}, ``` 此时编译烧录已经可以看到类成员了,但成员还没有办法实例化。 ```python import moopi dir(moopi) ['__class__', '__name__', 'sum', 'Object', 'ROTATE_0', 'ROTATE_180', 'ROTATE_270', 'ROTATE_90', 'STR', 'achieve', 'add', 'average', 'location', 'max', 'sayHello', 'sayHi', 'size'] ``` 到此为止,类打添加就已经完成了,但此时如果尝试实例化类,系统则会崩溃: ```python obj = moopi.Ojbect() ``` ### 7.2 为类增加构造函数 构造函数不是必须的,我们可以通过其他方式构造类,但如果没有构造函数,用户在尝试实例化对象的时候会导致系统崩溃,所以,如果我们可以不提直接构造方式,但必须保证存在构造函数,要不然就别加到模块字典中,不加到模块的字典中这个类也是存在的,只是不能显示的实例化而已。 上一节中,在定义类原型的时候,我们只给原型添加了三个必要参数,第四个必要参数就是 `make_new` ,这是一个函数,当用户尝试构造一个类实例的时候,系统会调用该函数并返回对应的实例,或者返回空对象(禁止构造)。 该函数原型是: ```C STATIC mp_obj_t make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) ``` 其中 make_new 是构造函数的名字,没有严格要求,随意写即可,但建议采用 `模块名_类名_make_new` 这样的方式命名。 参数均为构造时被动传入,第一个是构造该类类型,也就是之前我们定义的 `moopi_type_object` 本身,n_args 是调用构造方法时候按位置传入的参数数量,n_kw 是按字典传入的参数数量, args 是参数列表,其中包含了按位置传入的参数和按字典传入的参数。 如果自定义类不允许实例化,或者是参数不正确不能实例化,那么在这个函数中直接返回 mp_const_none 即可,但如果允许实例化,那么在这个函数中必要做的几件事如下: 1. 为类结构开辟空间 2. 设置实例的对象类型 3. 返回这个对象 ```C STATIC mp_obj_t moopi_object_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { moopi_object_t *self = (moopi_object_t *)m_new_obj(moopi_object_t); self->base.type = &moopi_type_object; return MP_OBJ_FROM_PTR(self); } ``` 创建对象的方式有很多种,这里我们选择了最基础的 m_new_obj 的方式,另外还有其他几种方式: * m_new_obj : 创建一个对象实例,在对象弃用时需要手动调用析构函数 * m_new_obj_with_finaliser : 创建一个对象实例,对象弃用时会自动调用析构函数释放空间 * m_new_obj_var : 用于创建具有可变大小的对象,也称为变长对象(variable-sized object),需要手动析构 * m_new_obj_var_with_finaliser : 用于创建具有可变大小的对象,也称为变长对象(variable-sized object),自动释放空间 * m_new_obj_maybe : 创建一个空对象 * m_new_obj_var_maybe : 创建一个可变大小的空对象。 说实在的,除了前两个,后面这些我基本也没有深研究是干啥用的,希望其他大佬能够补充一下,最常用的就是上面两个,第二个等下面讲到析构函数的时候再给大家细讲。 函数中第二行也是必须的,创建完对象之后,必须显式的为这个对象设置类型,否则在实例化阶段一样会报错。 最后,通过 MP_OBJ_FROM_PTR 宏将对象强制转换为 mp_obj_t 类型对象返回。 此时在通过实例化的方式获得对象,就已经可以获得成功了,并且通过 `dir` 去查看这个类实例的时候,可以看到他存在一个 __class__ 的成员,这个成员的值是 Object,也就是我们类的物理名称。 ### 7.2 类的析构函数 micropython 有严格的内存管理机制,当在 Micropython 环境下使用变量对对象进行一次引用后,对象引用计数器会加一,当失去一次引用后,引用计数器会减一,当引用计数为0的时候,会进入系统回收状态,但此时不会进行及时回收,而是当系统 gc 线程调用 gc.collect() 的时候进行回收。 >obj = moopi.Ojbect() 及对对象产生了一次引用 >a = obj 引用加一 >obj = 1 引用减一 如果在创建对象的时候使用了 `m_new_obj_with_finaliser` ,则系统会管理对象的应用与空间释放,但如果使用 `m_new_obj` 创建对象,则需要我们手动释放空间。 在 C 扩展 micropython 的时候,环境并没有像提供构造函数那样提供析构函数的注入方式,需要我们自己给成员字典增加一个 __del__ 的成员才可以,但这个成员使用过程中会存在一些问题,后面会讲到。该函数的原型如下: ```C STATIC mp_obj_t destructor(mp_obj_t self_in) ``` 其中 self_in 及当前对象的指针,直接返回 mp_const_none 即可。 在这个函数中,需要通过 `m_del_obj` 函数释放对象: ```C STATIC mp_obj_t moopi_object_destructor(mp_obj_t self_in){ moopi_object_t *self = MP_OBJ_TO_PTR(self_in); m_del_obj(self->base.type,self); return mp_const_none; } MP_DEFINE_CONST_FUN_OBJ_1(moopi_object_destructor_obj, moopi_object_destructor); ``` 该函数需要注册到类的成员列表中: ```C const mp_rom_map_elem_t moopi_object_local_dict_table[] = { {MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&moopi_object_destructor_obj)}, }; ``` 此时我们其实是无法直观感受到对象是何时被回收的,按照 ESP32 及 FreeRTOS 的规定,当系统空间不足的时候会触发自动回收线程,但实际在测试过程中会发现,这个功能貌似并没有被打开,只有我们手动调用 gc.collect() 函数的时候系统才会自动回收。 下面我们在析构函数中加一行输出,测试一下。 ```python import moopi obj = moopi.Object() obj = moopi.Object() import gc gc.coloect() ``` 上面的代码中我们构造了两次 Object 对象,当第二次构建的时候, obj 变量指向了新的对象,原来的对象引用减一,此时已经没有任何变量应用它,所以调用 gc.collect() 的时候会被回收掉。 但这也引发了一个问题:无论是用 m_obj_new 还是 m_new_obj_with_finaliser 定义的实例,micropython 只关注micropython 环境的引用,如果我们在 C 环境中对该实例增加引用时候,micropython 环境其实并不知道,这就会导致,在 C 中引用的失效,从而直接导致系统的崩溃。 在其他版本的 Micropython 中(比如在 RT-Thread 中扩展 micropython 的时候,或者在 X86 环境中扩展Micropython的时候),都可以通过 类似 Py_INCREF 和 Py_DECREF 的方式手动增减对象的引用次数,但是在 micropython 的环境中并没有发现有类似函数或宏可用。 所以如果我们的对象在 C 和 micropython 环境中混用,最好使用 m_new_obj 方式为对象开辟空间,并且不要使用 __del__ 析构函数,而是提供一个手动析构函数。 ### 7.3 为对象增加方法 类对象的方法和函数与库的方法定义方式基本相同,不同之处在于,类方法至少有一个参数,并且第一个参数永远是类实例自身,从第二个参数开始才是真正调用方法时候传入的参数。 ```C STATIC mp_obj_t moopi_object_size(size_t n_args, const mp_obj_t *args){ moopi_object_t *self = MP_OBJ_TO_PTR(args[0]); if(n_args==2){ self->width = self->height = mp_obj_get_int(args[1]); }else if(n_args==3){ self->width = mp_obj_get_int(args[1]); self->height = mp_obj_get_int(args[2]); }else{ mp_obj_t res[2]={ mp_obj_new_int(self->width), mp_obj_new_int(self->height) }; return MP_OBJ_FROM_PTR(mp_obj_new_tuple(2,res)); } return MP_OBJ_FROM_PTR(self); } MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(moopi_object_size_obj, 1, 3, moopi_object_size); ``` **(记得把方法添加到字典中)** 以上的例程代码中,函数第一行首先从传入参数列表的第一个参数中获取了类实例自身(就是通过 moopi.Object()创建的类实例),这个类实例就是在 make_new 方法中通过 m_new_obj 创建的结构体指针,该结构体携带了该类的所有自定义成员,包括x,y,width,height,parent。 从代码中可以得知,size 函数其实是一个玩出花来的重载函数。首先,这个函数可以接受 0 参数(其实是1个参数)传入,用于查询对象的宽度和高度,此时返回的是对象宽高的元祖;但如果携带一个或者2个参数的时候,则会设置对象的宽高,不过设置完毕后并不像在模块中 size 函数那样返回对象的大小,而是返回了对象自身,这种方式可以对对象进行链式操作,灵感来自于 JQuery 操作起来十分方便。 ```python import moopi obj = moopi.Object() obj.size(100,200).location(10,20) ``` ### 7.4 类的静态方法 上一小节中定义了 size 和location 函数,这些函数可以通过类实例直接的方式调用,但实际上内部调用仍然是通过对象进行调用的(这个是 Python 的成员函数调用机制),即通过以下方式调用: ```python moopi.Object.size(obj,100,200) ``` 这样可以直观的感受到,为什么在实际转到 C 函数的时候会多出来一个参数。 利用这个特性,我们可以给类创建一些静态方法,即直接通过类调用的方法,但事先声明,这是比较危险的操作,不建议大家用。 ```C STATIC mp_obj_t moopi_object_new(){ moopi_object_t *self = (moopi_object_t *)m_new_obj(moopi_object_t); self->base.type = &moopi_type_object; return MP_OBJ_FROM_PTR(self); } MP_DEFINE_CONST_FUN_OBJ_0(moopi_object_new_obj, moopi_object_new); ``` **(记得把方法添加到字典中)** 该方法添加后,通过 `dir(moopi.Object)` 可以看到,该对象已经增加了一个 new 方法,通过这个 new 方法也可以创建一个对象(如果想用单态的话,这是一个不错的方法,但还是建议用),通过 `moopi.Object.new()` 可以返回一个对象实例,所以这是一个基于对象的静态方法,理论上只能通过对象调用这个方法,但理论是理论,现实就很打脸了,不论是通过 new 方法创建的对象,还是通过构造函数创建的对象,通过 `dir(obj)` 查看的到时候发现,类实例尽然也有 new 方法,这就比较悲催了,如果用户不小心调用了 `obj.new()` 系统就抛出参数个数不对的错误,所以这是一个伪静态方法。 不过仍然可以通过一些方案解决,但终归不是很舒服,有种破坏封装的遗憾。 ### 7.5 打印函数 当我们实例化一个对象后,在 REPL 环境下尝试打印这个对象的时候,发现输出的结果是 `` ,但如果去看其他的类,比如 machine.Pin 生成的实例,直接输入对象名打印的时候是 Pin(1),那这是如何实现的呢? 其实这个方法就藏在了类的原型中。 上面章节中提到过,所有类的原型都是一个 mp_obj_type_t 结构体,这个结构体中有一些特殊成员,有一些我也没用过,也不咋认识,我挑一些认识的讲一下: | 标志 | 成员 | 说明 | |-|-|-| | √ | base | 类型的头,所有类都包含这个,这里我们恒定为 {&mp_type_type} | | | flags | 该类型相关的标志位,用于指示类型的特性和行为 ,这个没用到过 | | √ | name | 类的实际名字,不是模块中显示的名字,而是通过 obj.__name__ 打印出来的名字,这两个名称实际上是可以不同的 | | * | print | 打印函数指针,指向实现__repr__和__str__特殊方法的函数,用于打印对象的字符串表示形式,通过在 REPL 环境下直接输入变量名,或者通过 pirnt() 函数打印出来的内容 | | √ | make_new | 初始化函数指针,指向实现__new__和__init__特殊方法的函数,用于创建该类型的实例对象 | | * | call | 指向实现__call__特殊方法的函数,允许以类似函数调用的方式使用该类型的实例对象 | | * | unary_op | 指向实现一元操作的函数,用于支持对象的运算操作 | | * | binary_op | 指向实现二元操作的函数,用于支持对象的运算操作 | | * | attr | 指向实现属性的加载、存储和删除操作的函数 | | * | subscr | 指向实现下标运算的加载、存储和删除操作的函数 | | * | getiter | 指向迭代器获取函数 | | * | iternext | 指向迭代器的下一个元素的函数 | | | buffer_p | 如果该类型支持缓冲区协议,指向实现缓冲区操作的函数,没怎么用到过,不过应该挺有用的 | | | protocol | 指向其他特定协议或接口的结构体或函数指针,也没用到过 | | * | parent | 指向父类型的指针,可以是单个父类型的指针,也可以是包含多个父类型的元组对象 | | √ | locals_dict | 一个字典对象,用于存储类型的局部方法、常量等 | *上面表格中标注 √ 的是已经讲过的,标注 * 的是接下来会讲到的,没有做任何标注的是我也没用过的,不能拿出来误导大家* 本小节着重讲打印输出函数,其函数原型是: ```C void (*mp_print_fun_t)(const mp_print_t *print, mp_obj_t o, mp_print_kind_t kind); ``` 该函数有三个参数,第一个是一个指向 mp_print_t 结构体的指针,用于控制打印行为。mp_print_t 结构体包含了打印函数的指针和其他相关的数据。通过这个参数,可以访问打印函数及其关联的数据。 第二个参数是触发打印的对象。 第三个参数是一个枚举类型的值,用于指定打印的类型。mp_print_kind_t 定义了不同的打印类型,例如正常的打印、调试信息的打印等。根据打印类型的不同,可以在打印函数中实现不同的行为逻辑。这个参数可以判断实在什么情况下做的输出,比如直接在 REPL 环境下输出变量,该值是1,也就是 PRINT_REPR,如果使用 print() 函数输出,该值是 0 ,也就是 PRINT_STR,等等,其他的大家可以试一下,通过这个值,可以控制在不同环境下可以输出不同内容。 注意,这个函数没有返回值,不用再返回 mp_const_none 了。 在这个函数中,我们是不能直接使用 C 的 printf 输出的,因为那个没有意义,虽然也可以输出内容,但并不是在真正的 python 环境下输出的,这里需要使用 mp_print 函数,这个函数的使用方式和 sprintf 相似,接收2个及以上参数,第一个参数是输出的通道, 这里直接写 print 参数即可, 第二个是格式化字符串,后面的值格式化字符参数。 ```C STATIC void moopi_object_print(const mp_print_t *print, const mp_obj_t self_in, mp_print_kind_t kind) { moopi_object_t *self = MP_OBJ_TO_PTR(self_in); if(self!=MP_OBJ_NULL){ const char *type = mp_obj_get_type_str(self_in); mp_printf(print, "(x:%d, y:%d, width:%d, height:%d)", type, self->x, self->y, self->width, self->height); }else{ mp_printf(print, "(null object)"); } } ``` 打印函数中,首先从 self_in 中获取对象,如果对象不为空,则打印出对象的 x,y,width,height 信息。 最后,记得给 moopi_type_object 原型加上 .print 成员即可。 ```C const mp_obj_type_t moopi_type_object = { {&mp_type_type}, .name = MP_QSTR_Object, .locals_dict = (mp_obj_dict_t *)&moopi_object_local_dict, .make_new = moopi_object_make_new, .print = moopi_object_print }; ``` ### 7.6 直接调用函数 这个翻译好像不是很贴切,这个函数和直接在成员中加入 __call__ 效果是相同的,这个函数的作用是能够让对象变得像函数一样能够直接被调用,比如: ```python obj = moopi.Object() obj() ``` 这个函数的原型是: ```C mp_obj_t (*mp_call_fun_t)(mp_obj_t fun, size_t n_args, size_t n_kw, const mp_obj_t *args); ``` 共接收四个参数,第一个参数是调用的对象本身,也就是 self_in,第二个是传入的按位传值的参数数量,第三个参数是按字典传值的参数数量,最后一个是参数列表,注意,args 中先存储的是按位置传值的参数,如果需要取出按字典传值的参数,可以参考 5.2.5 小节。 ```C STATIC mp_obj_t moopi_object_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { printf("Called moopi_object_call\n"); return mp_const_none; } ``` 最后记得修改修改原型,加上 .call 指向。 该方法的用法是使用类的对象加括号调用,而不是直接类名加括号,类名加括号调用的是构造函数: ```python import moopi obj = moopi.Object() obj() ``` ### 7.7 类的属性 在网上很多资料中,都是在 micropython 中没有办法给类添加属性,所以只能使用 set/get 方法 实现属性的修改,而且在 ESP32 官方代码中也没有使用属性,用的都是方法实现的。但经过对 micropython 源码的分析,其实是可以通过原型中的 attr 元素直接(并不是间接)实现属性的,也就是本来人家 micropython 就提供了属性是扩展方式,有可能是早起版本中不能直接使用,导致了大家对这部分有所谓误解。 属性元素的函数原型如下: ```C void (*mp_attr_fun_t)(mp_obj_t self_in, qstr attr, mp_obj_t *dest); ``` 第一个参数是调用属性的类实例,第二个元素是调用属性的字符串转码Hash值,当给属性赋值的时候,第三个值是属性的值,如果只是取值,第三个则作为返回对象用。 attr 是一个被序列化后的值,这个值在整个 micropython 环境中表示唯一的一个字符串(可以理解为 Hash 值,实际上也是),所以我们比较的时候直接用 MP_QSTR_XXX 进行比较即可,并且编译系统会很贴心的帮我们进行转换。dest 用于向内或向外传值,这个参数是一个数组,有两个值,dest[0] 表示返回的值,所以如果需要查询一个属性值,通过 dest[0] 返回即可,dest[1] 表示要设置某个属性值。 如果 dest[0] == MP_OBJ_SENTINEL 的时候,表示调用的是 setter ;如果 dest[0] == MP_OBJ_NULL 的时候,表示是 getter 函数。 我翻阅其他函数库的一些代码,有的判断是 dest[1] != MP_OBJ_NULL 表示调用了 setter 函数(LVGL函数库竟然也有这样的错误),但这样是不严谨的,正常情况下这样做没问题,但是恰巧我们设置的是 是 None 的时候,就会报错了,所以我们还是判断 dest[0] 是否为 MP_OBJ_SENTINEL 最为稳妥。 所以我们可以为其增加一个属性函数: ```C STATIC void moopi_object_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest){ moopi_object_t *self = MP_OBJ_TO_PTR(self_in); switch (attr) { case MP_QSTR_width : if(dest[0] == MP_OBJ_SENTINEL){ self->width = mp_obj_get_int(dest[1]); dest[0] = MP_OBJ_NULL; }else{ dest[0] = mp_obj_new_int(self->width); } break; case MP_QSTR_height : if(dest[0] == MP_OBJ_SENTINEL){ self->height = mp_obj_get_int(dest[1]); dest[0] = MP_OBJ_NULL; }else{ dest[0] = mp_obj_new_int(self->height); } break; case MP_QSTR_x : if(dest[0] == MP_OBJ_SENTINEL){ self->x = mp_obj_get_int(dest[1]); dest[0] = MP_OBJ_NULL; }else{ dest[0] = mp_obj_new_int(self->x); } break; case MP_QSTR_y : if(dest[0] == MP_OBJ_SENTINEL){ self->y = mp_obj_get_int(dest[1]); dest[0] = MP_OBJ_NULL; }else{ dest[0] = mp_obj_new_int(self->y); } break; default: break; } } ``` 最后在类原型中加入 .attr 的函数指向: ```C const mp_obj_type_t moopi_type_object = { {&mp_type_type}, .name = MP_QSTR_Object, .locals_dict = (mp_obj_dict_t *)&moopi_object_local_dict, .make_new = moopi_object_make_new, .print = moopi_object_print, .call = moopi_object_call, .attr = moopi_object_attr, }; ``` 测试一下: ```python import moopi obj = moopi.Object() obj.width 0 obj.width=10 obj.width 10 ``` 还是非常好使的,但是这时候会出现一个问题,我们使用`dir(obj)` 查看的时候,得到的结果令人意外: ``` ['__class__', 'height', 'width', 'x', 'y'] ``` 我们没有对 Object 做任何属性的操作,只是加了个属性函数,开发环境竟然贴心的帮我们把所有的属性字段都提取了出来,这是非常喜人的。 但你是否也发现了另外一个问题呢? 我们之前的 size 和 location 函数哪去了? 这是因为我们添加完 .attr 属性后,这个属性所提取出来的属性序列覆盖了我们之前的成员字典,不知道这是否是个 bug ,不管官方如何解释的,我们还有补救的机会,只要在 moopi_object_attr 函数开头加入以下代码即可: ```C const mp_obj_type_t *type = mp_obj_get_type(self_in); mp_map_t *locals_map = &type->locals_dict->map; mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP); if (elem != NULL) { mp_convert_member_lookup(self_in, type, elem->value, dest); } ``` 这几行代码的意思是,从类定义中提取本地字典,然后将本地字典加入到序列中。这样在使用 `dir(obj)` 的时候,就会发现,之前丢失的元素都回来了。 ``` ['__class__', '__del__', 'height', 'location', 'new', 'size', 'width', 'x', 'y'] ``` ### 7.8 运算符重载 做过 C++ 开发的同学们都应该有印象, C++ 中有个非常便捷的功能,就是对类的运算符进行重载,比如我们写了一个类叫 Object,这个类的实例名叫 obj,重载运算符后可以使用 a = obj+1, 或者 obj<<3 这样的操作,非常方便,作为胶水语言的 Python 自然不能丢弃这么优秀的编程方式,所以在 micropython 中也非常完美的继承了这一特性,并且可以通过原型的 .unary_op 和 .binary_op 分别对一元运算符和二元运算符进行重载。 一元运算符重载函数原型如下: ```C mp_obj_t (*mp_unary_op_fun_t)(mp_unary_op_t op, mp_obj_t); ``` 第一个参数是运算符类型枚举值,第二个参数是与本对象运算的数据,可以是任意类型的。 一元运算符一共有 10 个,相对来说比较简单: | 常量 | 表示运算符 | 含义 | 建议返回值类型 | |-|-|-|-| | MP_UNARY_OP_POSITIVE | + | 正号修饰 | 任意 | | MP_UNARY_OP_NEGATIVE | - | 负号修饰,负值 | 任意 | | MP_UNARY_OP_INVERT | ~ | 按位取反 | 任意 | | MP_UNARY_OP_NOT | not | 否操作 | 任意 | | MP_UNARY_OP_BOOL | if() | 逻辑判断 | bool | | MP_UNARY_OP_LEN | len() | 测量长度 | int | | MP_UNARY_OP_HASH | hash() | 获取 Hash 值 | int | | MP_UNARY_OP_ABS | abs() | 绝对值 | number | | MP_UNARY_OP_INT | int() | 取整 | int | | MP_UNARY_OP_SIZEOF | sys.getsizeof() | 获取大小 | int | 这个函数的返回值对于前面四个可以是任意类型的值,其实后面几个也可以是任意类型的,但建议还是要遵循 Python 环境的设定,返回指定类型的值。 ```C STATIC mp_obj_t moopi_object_unary(mp_unary_op_t op, mp_obj_t self_in){ moopi_object_t *self = MP_OBJ_TO_PTR(self_in); switch (op) { case MP_UNARY_OP_POSITIVE: // + return MP_OBJ_FROM_PTR(mp_obj_new_int(self->x)); case MP_UNARY_OP_NEGATIVE: // - return MP_OBJ_FROM_PTR(mp_obj_new_int(-self->x)); case MP_UNARY_OP_INVERT: // ~ return MP_OBJ_FROM_PTR(mp_obj_new_int(~self->byte_val)); case MP_UNARY_OP_NOT: // not return MP_OBJ_FROM_PTR(mp_obj_new_bool(!self->bool_val)); case MP_UNARY_OP_BOOL: // if(obj) return mp_obj_new_bool(self->bool_val); case MP_UNARY_OP_LEN: // len() return MP_OBJ_NEW_SMALL_INT(123); case MP_UNARY_OP_HASH: // hash() return MP_OBJ_NEW_SMALL_INT(qstr_compute_hash((const byte *)"12345",5)); case MP_UNARY_OP_ABS: // abs() return MP_OBJ_FROM_PTR(mp_obj_new_int(self->x)); case MP_UNARY_OP_INT: // int() return MP_OBJ_NEW_SMALL_INT(self->x); case MP_UNARY_OP_SIZEOF: // sizeof() return MP_OBJ_FROM_PTR(sizeof(*self)); } return mp_const_none; } ``` **这里需要注意,如果在 switch 中没有把所有的枚举值列完,最后一定要加一个 default: break; 否则会出错。** 函数设置好后,可以使用代码进行测试: ```pyton -obj len(obj) hash(obj) ``` Micropython 的函数重载明显比 C++ 的要强大,仅是一元运算符就这么多了(可惜没有 ++ -- 的操作),二元运算符就更多了,而且非常复杂和繁琐,我认识的大概有 34 个,其他还有很多,但都没找到相关资料,没法给大家讲解了。 二元运算符操作函数原型是: ```C mp_obj_t (*mp_binary_op_fun_t)(mp_binary_op_t op, mp_obj_t, mp_obj_t); ``` 第一个参数仍然是运算符的枚举,第二个参数是 self_in ,也就是前面那个操作数(大概率是把自身写在前面的),第二个是操作数。 二元运算符: 9个关系运算,应该返回一个bool: | 常量 | 表示运算符 | 含义 | |-|-|-| | MP_BINARY_OP_LESS | < | 小于运算 | | MP_BINARY_OP_MORE | > | 大于运算 | | MP_BINARY_OP_EQUAL | = | 等于运算 | | MP_BINARY_OP_LESS_EQUAL | <= | 小于等于运算 | | MP_BINARY_OP_MORE_EQUAL | >= | 大于等于运算 | | MP_BINARY_OP_NOT_EQUAL | != | 不等于运算 | MP_BINARY_OP_IN | in | in 运算 | | MP_BINARY_OP_IS | is | is 运算 | | MP_BINARY_OP_EXCEPTION_MATCH | ? | ?| 13个赋值算数运算符: | 常量 | 表示运算符 | 含义 | |-|-|-| | MP_BINARY_OP_INPLACE_OR | \|= | 或等运算 | | MP_BINARY_OP_INPLACE_XOR | ^= | 异或等运算 | | MP_BINARY_OP_INPLACE_AND | &= | 且等运算 | | MP_BINARY_OP_INPLACE_LSHIFT | <<= | 左移等运算 | | MP_BINARY_OP_INPLACE_RSHIFT | >>= | 右移等运算 | | MP_BINARY_OP_INPLACE_ADD | += | 加等运算 | | MP_BINARY_OP_INPLACE_SUBTRACT | -= | 减等运算 | | MP_BINARY_OP_INPLACE_MULTIPLY | \*= | 乘等运算 | | MP_BINARY_OP_INPLACE_MAT_MULTIPLY | @= | 矩阵乘法 | | MP_BINARY_OP_INPLACE_FLOOR_DIVIDE | //= | 整除等运算 | | MP_BINARY_OP_INPLACE_TRUE_DIVIDE | /= | 除法等运算 | | MP_BINARY_OP_INPLACE_MODULO | %= | 取模等运算 | | MP_BINARY_OP_INPLACE_POWER | \*\*= | 幂等运算 | 13个算数运算符: | 常量 | 表示运算符 | 含义 | |-|-|-| | MP_BINARY_OP_OR | \| | 按位或运 | | MP_BINARY_OP_XOR | ^ | 按位异或运算 | | MP_BINARY_OP_AND | & | 按位与运算 | | MP_BINARY_OP_LSHIFT | << | 左移运算 | | MP_BINARY_OP_RSHIFT | >> | 右移运算 | | MP_BINARY_OP_ADD | + | 加运算 | | MP_BINARY_OP_SUBTRACT | - | 减运算 | | MP_BINARY_OP_MULTIPLY | \* | 乘运算 | | MP_BINARY_OP_MAT_MULTIPLY | @ | 矩阵乘法运算 | | MP_BINARY_OP_FLOOR_DIVIDE | // | 整除运算 | | MP_BINARY_OP_TRUE_DIVIDE | / | 除法运算 | | MP_BINARY_OP_MODULO | % | 取模运算 | | MP_BINARY_OP_POWER | \*\* | 幂运算 | 其他的暂时用不到就不讲了(重点是我也不懂……) 这里我们只简单的举几个例子,就不全部写完了,所以记得 switch 最后一定是 default: breakl; 否则编译不过去。 ```C STATIC mp_obj_t moopi_object_binary(mp_binary_op_t op, mp_obj_t self_in, mp_obj_t value){ moopi_object_t *self = (moopi_object_t *)MP_OBJ_TO_PTR(self_in); switch (op) { case MP_BINARY_OP_LESS : // < { int32_t val = mp_obj_get_int(value); return MP_OBJ_FROM_PTR(mp_obj_new_bool(self->xbyte_val |= val; return MP_OBJ_FROM_PTR(self); } break; case MP_BINARY_OP_OR : // | { uint8_t val = mp_obj_get_int(value); return MP_OBJ_FROM_PTR(mp_obj_new_int(self->byte_val | val)); } break; default: break; } return mp_const_none; } ``` 测试: ```python import moopi obj = moopi.Object() obj<10 True obj.x=100 obj<10 False o|0xAA 255 obj | = 0xAA obj (x:0, y:0, width:0, height:0, byte_val:0xFF) ``` ### 7.9 下标运算符 众所周知,Python 在数据处理方面有极大的优势,不仅在于他有非常强大的三方函数库,他还具有非常人性化的操作手法,比对于字典的操作,可以使用类似 `dict['key']` 这种方式直接存取数据,相比之下,比 JAVA 和 C# 中的字典都好用的多。这种方式叫做下标操作,中括号中的内容及可以是字符串,也可以是数字,甚至可以是任何类型,简直爽的一批。 而在 C 扩展 micropython 的过程中,为类增加下标操作也非常简单,只要为原型添加 .subscr 属性即可,该属性对应的函数原型是: ```C mp_obj_t (*mp_subscr_fun_t)(mp_obj_t self_in, mp_obj_t index, mp_obj_t value); ``` 第一个参数表示操作对象本身,第二个参数表示操作的下表,可以是任意类型的,最后一个是操作数,如果 value == MP_OBJ_SENTINEL 表示 getter 操作,如果是其他的表示 setter 操作。 所以,这个函数可以写的极为简单: ```C STATIC mp_obj_t moopi_object_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) { moopi_object_t *self = (moopi_object_t *)MP_OBJ_TO_PTR(self_in); if (value !=MP_OBJ_SENTINEL) { mp_obj_dict_store(self->dict,index,value); } mp_map_t *map = mp_obj_dict_get_map(self->dict); if(mp_map_lookup(map,index,MP_MAP_LOOKUP)!=NULL){ return mp_obj_dict_get(self->dict,index); } return mp_const_none; } ``` 我们为我们的对象结构体增加了一个 mp_obj_ditc_t 类型的字典字段,把下表内容都存放在这个字段中,通过 mp_obj_dict_XXX 一系列函数对字典进行读写,mp_obj_dict_store 为保存一个值, mp_obj_dict_get 表示写入一个值,通过获得字典的 map 字段,可以查看所对应的值是否存在。 最后让我们来一波疯狂的测试: ```python import moopi obj = moopi.Object() obj['key'] = 10 obj['key'] 10 obj[10] = 100 obj[10] 100 obj[1] = moopi.Object() obj[1] (x:0, y:0, width:0, height:0, byte_val:0x55) obj[obj[1]]=123 obj[obj[1]] 123 ``` 相当的完美! ### 7.10 迭代器 上一阶段,我们将 Object 类武装成了一个具有字典功能的对象,如果想查询 Object 中一共存储了多少个键值对,可以用 len() 函数,结合前面学到的运算符重载功能即可实现,但如果想使用 iter() 函数遍历这个这个对象呢?目前还不能实现。 我们看下面这段代码: ```python d = {'a':1,'b':2,'c':3} i = iter(d) next(i) 'b' next(i) 'c' next(i) 'a' ``` 在 Python 中,是可以通过 iter 和 next 函数来遍历元祖、列表、字典等这些对象的,同样我们如果实现 __iter__ 函数的话其实也是可以完成这样功能的,并且 Micropython 开发环境已经贴心的为我们准备了, .getiter 和 .iternext 两个元素,只要在对象原型中加入这两个函数即可,前者用于返回一个迭代器,后者用于对迭代器进行 next 操作,两个函数原型长这样: ```C mp_obj_t (*mp_getiter_fun_t)(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf); mp_obj_t (*mp_fun_1_t)(mp_obj_t); ``` 第一个是 .getiter 的函数原型,一共接收两个参数,第一个参数是调用 iter 函数的对象,第二个是一个迭代器缓冲区,用于存储迭代器状态和数据,暂时用不到。 第二个原型其实就是一个单参数函数,传入的是 self_in,但需要返回一个迭代的值。 按照常理,通过 .getiter 返回的对象应该是一个可以进行 next 的对象,然后这个对象具有 .iternext 成员,但为了方便演示,我都写到一个函数中,让 .getiter 返回自身,并且在每次调用的时候将其中 iter_index 值设置为 0 从头开始遍历,**注意,这种方式仅用于演示,尽量不要用来正式开发中,因为每次调用 iter 都会影响到其他迭代器的值输出,正式开发的时候一定要返回一个可迭代的对象,并且保证对象是独立的。** 每次调用 next 的时候,查看当前指向的值是否为空,并且判断是否超出了遍历范围,如果超出遍历范围,说明已经遍历结束了,我们需要返回一个 MP_OBJ_STOP_ITERATION 的值,标志着遍历结束,如果获得对象为空(key 和 value 都为空),说明这不是我们想要的值(具体为什么会出现这个,我猜想应该是在字典中存储以 NULL 结束导致的存在一个空值),继续下一个。 我们上一节中给结构体加了个 dict 元素,是一个字典元素,字典中有个值是 map,存放了字典的值和一些属性,我们可以通过 mp_obj_dict_get_map 获得这个 map ,或者为了效率,直接 self->dict->map 也是可以的。 map 中有两个值我们需要关注,alloc 表示元素的数量(包括那个空值),table 表示存放内容的表,是 mp_map_elem_t 类型的,mp_map_elem_t 中只存在一个 key 和一个 value。 所以我们程序设计的时候,通过 iter() 获取迭代器的时候,将计数器(self->iter_index)归零,通过 next() 获取元素的时候,让迭代器累加,如果超出范围则返回 MP_OBJ_STOP_ITERATION 表示迭代结束,否则返回这个键值对的元祖。 ```C /** * @brief 迭代器获取函数 */ STATIC mp_obj_t moopi_object_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf){ moopi_object_t *self = (moopi_object_t *)MP_OBJ_TO_PTR(self_in); self->iter_index = 0; return self_in; } /** * @brief 迭代器下一个元素 */ STATIC mp_obj_t moopi_object_iternext(mp_obj_t self_in){ moopi_object_t *self = (moopi_object_t *)MP_OBJ_TO_PTR(self_in); mp_map_t *map = mp_obj_dict_get_map(self->dict); mp_map_elem_t *elem = NULL; do{ if(self->iter_indexalloc){ elem = map->table+self->iter_index; }else{ return MP_OBJ_STOP_ITERATION; } self->iter_index++; }while(elem->key == MP_OBJ_NULL && elem->key == MP_OBJ_NULL); mp_obj_t res[2]={elem->key,elem->value}; return MP_OBJ_FROM_PTR(mp_obj_new_tuple(2,res)); } ``` ### 7.11 类的继承 到此为止,霍霍类原型的成员们已经讲的大差不差了,buffer_p 和 protocol 还没来得及研究, LVGL 中道是用到这个了,带我研究完毕之后再向各位做汇报。 本小细节收个尾,讲一下类的继承。 类的继承在 github 上跟开发组交流了好长时间也没搞明白,可能是语言障碍(我用谷歌翻译的),也可能就本人是单纯的理解能力差,他们给的答案,以及 GPT 个的答案都是写 .parent 成员,这个咱也写了,效果是有一点的,但是差点意思,我把这个叫名义上的继承,但实际上并没有达成。 按照 7.1 章节中构建类的五个步骤,新添加一个 Label 子类: **定义类的类型结构体** ```C struct moopi_label{ moopi_object_t base; char *text; }; ``` 这个类的结构体第一个元素并不是 mp_obj_base_t 而是 moopi_object_t ,而 moopi_object_t 第一个元素是 mp_obj_base_t ,所以从严格意义上来讲, moopi_label 第一个元素仍然是 mp_obj_base_t,这就是在 C 环境下做 struct 继承的方式。(绕口令结束) **构建全局成员字典 并转换为 micropython 对象** ```C const mp_rom_map_elem_t moopi_label_local_dict_table[] = { }; STATIC MP_DEFINE_CONST_DICT(moopi_label_local_dict, moopi_label_local_dict_table); ``` **加入 make_new 函数** ```C STATIC mp_obj_t moopi_label_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { mp_arg_check_num(n_args, 0, 0 ,1, false); // 创建对象 moopi_label_t *self = (moopi_label_t *)m_new_obj(moopi_label_t); // 设置对象实例类型 self->base.base.type = &moopi_type_label; self->text = NULL; if(n_args==1 && mp_obj_get_type(args[0]) == &mp_type_str){ const char *t = mp_obj_str_get_str(args[0]); size_t len = strlen(t)+1; self->text = m_malloc(len); if(self->text!=NULL){ memcpy(self->text,t,len); } } // 返回对象 return MP_OBJ_FROM_PTR(self); } ``` 这个 make_new 函数加了一些料,第一行中使用 mp_arg_check_num 检查参数情况,这个函数参数依次是:输入参数总数量,按字典传值参数总数量,最小允许参数个数,最大允许参数个数,是否允许使用字段传值。如果没有按照要传值,函数会帮我们抛出一个参数类型错误的异常。 这个构造函数既可以不传参,也可以传入一个字符串参数作为 label 标签的内容。 **定义类原型** ```C const mp_obj_type_t moopi_type_label = { {&mp_type_type}, .name = MP_QSTR_Label, .locals_dict = (mp_obj_dict_t *)&moopi_label_local_dict, .make_new = moopi_label_make_new, }; ``` **将类添加到模块中** 这里需要注意一下,如果添加文件后直接编译,不会出任何问题,但其实此时并没有把我们下加入的文件捎带上,因为 make 环境首先编译 CMakeLists.txt 文件成 mk 文件,mk 文件中把所有的文件都列出来了,这里面用的都是文件的绝对路径,没有用任何通配符,所以在没有修改 CMakeLists.txt 的情况下编译,其实我们刚才加入的文件是没有被放到编译列表的,所以,如果我们在 CMakeLists.txt 文件中使用的是 file 函数创建的变量,很有可能就会出现不编译的问题,建议重新保存一下这个文件即可。 **为 Label 类加入 text 成员** ```C STATIC mp_obj_t moopi_label_text(size_t n_args, const mp_obj_t *args){ moopi_label_t *self = MP_OBJ_TO_PTR(args[0]); if(n_args>1){ const char *t = mp_obj_str_get_str(args[1]); size_t len = strlen(t)+1; if(self->text!=NULL){ m_free(self->text); self->text=NULL; } self->text = m_malloc(len); if(self->text!=NULL){ memcpy(self->text,t,len); } } mp_obj_t text = mp_obj_new_str("",0); if(self->text !=NULL){ text = mp_obj_new_str(self->text,strlen(self->text)); } return MP_OBJ_FROM_PTR(text); } MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(moopi_label_text_obj,1,2,moopi_label_text); ``` 测试一下,类可以正常看到,成员除了其他正常的,只有一个我们自己添加的 text ,尝试实例化这个类,也能看到。 按照我们设想的,Label 继承自 Object,那只要填写 .parent 应该就能够获得 Object 的所有属性才对,我们试一下: ```C const mp_obj_type_t moopi_type_label = { {&mp_type_type}, .name = MP_QSTR_Label, .locals_dict = (mp_obj_dict_t *)&moopi_label_local_dict, .make_new = moopi_label_make_new, .parent = &moopi_type_object }; ``` 烧录测试: ```python import moopi dir(moopi.Label) ['__class__', '__name__', '__bases__', '__del__', '__dict__', 'location', 'new', 'size', 'text'] ``` 看,除了我们自己添加的这些成员之外,还新增加了 Object 的成员,事情看似完美,但老天就非得在你得意的时候给你一棒槌! 我们继续测试: ```python lab = moopi.Label("ABC") dir(lab) ['__class__', 'text'] ``` 实例化对象后发现,进存在自身的成员,父类的成员丢的一干二净。 通过阅读其他 Micropython 的代码,以及翻阅 Micropython 的源码,还是找到了解决方案,就是为原型增加 .attr 成员,在 .attr 中把父类的属性都列出来,就像我们给 Object 增加属性的时候想法是一样的: ```C void call_parent_methods(mp_obj_t obj, qstr attr, mp_obj_t *dest) { const mp_obj_type_t *type = mp_obj_get_type(obj); while (type->locals_dict != NULL) { mp_map_t *locals_map = &type->locals_dict->map; mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP); if (elem != NULL) { // 添加当前类自己的所有成员 mp_convert_member_lookup(obj, type, elem->value, dest); break; } if (type->parent == NULL) { break; } // 递归搜索父类成员 type = type->parent; } } // 定义类的类型结构 const mp_obj_type_t moopi_type_label = { {&mp_type_type}, .name = MP_QSTR_Label, .locals_dict = (mp_obj_dict_t *)&moopi_label_local_dict, .make_new = moopi_label_make_new, .parent = &moopi_type_object, .attr = call_parent_methods }; ``` 这样在执行 `dir(lab)` 的时候,所有的成员就都出来了,而且都可以正常使用,但仍有些小瑕疵,就是父类的属性以及 print 方法并没有同步继承过来,因为在调用子类 attr 方法的时候,父类并不会自动调用 attr 方法,而父类的属性都是在父类的 atrr 方法中构建的,所以并没有带过来。这还需要我们进一步构建更强大的 通用 attr 函数。 到此为止,C 扩展 Micropython 的教程就都结束了,接下来我会结合外设,写一个综合用例。 并且在后续的教程中也会持续更新成员字典中一些其他的魔术方法,比如 \_\_init\_\_,\_\_enter\_\_,\_\_delitem\_\_ 等等。