源码地址 : https://gitee.com/Mars.CN/micropython_extend_example
在其他课程中讲过,这里不再赘述,有机会再出教程吧,但需要注意的是,截止到 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 的源码是从 github 上直接克隆下来的,目前最新的代码是 1.23,但这个版本我下载试过了,对 ESP32-S3 不是很友好,各种编译失败,我试验最稳定的版本是 1.19.1,但这个版本需要配合 ESP-IDF 4.x 开发,1.20 以后的版本可以用 ESP-IDF 5.x,ESP32 可以编译通过, S3 各种报错,所以不建议使用。
克隆 1.19.1 可以使用下面的命令:
git clone -b v1.19.1 https://github.com/micropython/micropython.git
按照官方文档的要求,第一次使用的时候先要对 micropython 进行核心的交叉编译:
$ cd mpy-cross
$ make
第二步需要解决的是单片机相关的子模块依赖。
在 micropython/ports 文件夹中,就是所有支持的单片机和开发板了,我们用到的是 esp32 这个文件夹,不要对其进行直接修改,需要把 esp32 复制一份,我们起名为 moopi (这个名字大家随心,随便起啥都行,不要用中文),今后所有的教程我们都从这里开始。
进入到这个文件夹,并下载所有子模块的依赖:
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 的头文件都不会飘红了(但仍然有一小部分需要解决)。
再开始编译代码之前,必须要对 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 开发板,需要配置如下项:
以上都设置完毕后,保存退出,最后一步,需要修改一下 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 下载查看工程,或者执行命令:
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 如何使用,我们会放到其他课程中展示,这里就不多说了。
如果我们是做平台开发的,或者说我们的产品需要给另外一些同行做二次开发的,我们就需要把我们自己的一些功能封装起来,提供给第三方使用。最长见的方法是就是写 python 脚本,提供给客户 .pyc 的二进制文件,或者 .py 的源码文件。但对于一些保密性要求高的,或者说直接操作特有硬件的,或者是要求执行效率的代码,使用 python 写就不是那么舒服了,所以我们就需要使用扩展 micropython 类库的方式来做,这也是 python 高级编程的一部分。简单来说,就是使用 C/C++ 扩展 python 类库。
在 micropython 环境中,一切都会被封装到 模块中,也就是 module,module 又包含了方法和类,以及常量,类中又可以包含方法、属性和常量等等。
本小节,我们就从创建一个 module 开始,能够通过在 REPL 环境中执行 help("modules")
看到我们的模块。
我们知道,在python 中写一个 .py 的文件就是一个 module ,就可以使用 import 导入,但在 C 中开发,相对来说要复杂一些,好处就是可以提高执行效率。
创建模块一共分五步:
在使用 C 扩展 micropython 之前,需要先引入几个头文件
#include "py/builtin.h"
#include "py/runtime.h"
#include "py/obj.h"
#include "py/binary.h"
这些都是扩展 micropython 必要的。
在项目根目录新建一个 moopi_mod 文件夹(名字随意,不要用中文),用于存放我们接下来的课程代码。
在这个文件夹下新建一个文件,我的名字叫 modmoopi.c
每个模块或者类都应该有一个全局字典,这个字典中定义了模块或者类中的所有成员,包括方法、常量、子类等,在扩展 micropython 中,用于全局自定用以下代码:
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 内部做对象转换用的,我们不必深究是什么意思,拿过来用就行了,有机会我们再拆开讲。
在写字典的时候必须注意以下几点:
__name__
,这是 python中规定的,也就是第一个成员的 key 必须是 MP_ROM_QSTR(MP_QSTR___name__),name 前面是三个下划线,别写错了,value 的值是 QSTR 类型的字符串,这里可以直接写,不要过多考虑注册表的问题,会自动完成注册。如果我们想改变模块的名称,只需要改变成员第一行中 value 的 MP_QSTR_
后面的内容即可,这个值其实并不是 import 指令导入的名称,而是模块的实际名称,import 导入名称将在第四步中介绍,但这个值是区分大小写的。
这一步比较简单,只需要调用 micropython 开发环境事先定义好的宏即可 MP_DEFINE_CONST_DICT
STATIC MP_DEFINE_CONST_DICT(moopi_globals, moopi_globals_table);
这个宏会传入两个参数,第一个是转后的 micropython 对象指针,这不变量不需要我们自己定义,环境会帮我们定义好,第二个参数是上面刚刚定义的对象成员列表0这一步的转换是为下一步定义对象原型做准备。
上面两步我们完成了模块成员的定义,下面完成对象原型的定义,代码如下:
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 指令导入时候的名称,区分大小写,第二个就是上面创建的原型。
此时我们的代码已经写完,但无论这个文件的代码中是否有错误,编辑器都不会报错,因为我们写的这个代码压根就没有参与编译,如果需要自己的代码起作用,还必须修改 CMakeLists.txt 文件,确切的说是要修改 man/CMakeLists.txt 文件,把我们的代码加入进去。
我们这里的源文件可能会分为两部分,分别是普通 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 环境中完成了自定义模块的添加,但此时模块中空空如也,什么也没有,这一节我们就为其添加一个方法。
模块中可以包含 方法、常量、类这些东西,最基础的就是方法,方法又分为无参数、有参数、可变参数(重载)、有返回值、无返回值这些,接下来的部分,会对其进行一一介绍。
在 micropython 方法原型中,不存在无返回值类型方法,所有方法的定义必须是一个包含有 mp_obj_t 类型返回值的,无参方法运行定义如下:
STATIC mp_obj_t func_name(){
return mp_const_none;
}
如果在 micropython 环境不需要返回值,在 C 扩展的时候,返回值恒定为 mp_const_none;
再此,我们定一个 say_hello 的方法:
STATIC mp_obj_t moopi_say_hello(){
printf("Hello Micropython !\n");
return mp_const_none;
}
方法定义完成之后,还需要将方法转换为 micropython 对象才可以使用,这个转换可以通过 micropython 开发环境提供的宏进行:
MP_DEFINE_CONST_FUN_OBJ_0(moopi_say_hello_obj, moopi_say_hello);
这个宏有两个参数,第一个参数值被转换出来的 micropython 对象指针,第二个对象是要转换的方法指针,这行宏定义调用完之后,将会生成一个 moopi_say_hello_obj 的 micropython 对象,最后,将这个对象写入到上一节定义的成员字典中:
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 环境,执行:
import moopi
moopi.sayHello()
即可验证方法有效性。
上一小节中,我们为模块创建了一个不带参数的方法,细心的小盆友可能已经注意到了,在调用 MP_DEFINE_CONST_FUN_OBJ_0
宏的时候,还有一堆相似的宏:
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
这就为我们创建不同数量参数的方法提供了多种可能性:
在 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 的方法,传入一个字符串型参数,并打印输出
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")
即可在控制台进行输出,但如果传入一个非字符串值,则会报错。
import moopi
moopi.sayHi(1)
raceback (most recent call last):
File "" , line 1, in <module>
TypeError: can't convert 'int' object to str implicitly
所以,我最好在方法中对值的内容进行校验,当值类型不对的时候直接告诉用户要比报错有好得多。
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);
知道一个参数方法如何定义了,接下来是 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 类型,没有其他别的意思,所以带不带都问题不大,带上之后在一些特殊场合不会报警告。
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);
(记得把方法添加到字典中)
在上面提供的转换宏中,只提供了 0~3 个参数的转换,对于 3 个以上参数的方法,不可能每一个都定义一个宏,那太啰嗦了,所以就有了 MP_DEFINE_CONST_FUN_OBJ_VAR ,对于3个以上方法的转换方式,而这个转换宏对应的函数定义格式也发生了改变:
STATIC mp_obj_t func_name(size_t n_args, const mp_obj_t *args);
这个函数原型中有两个参数,第一个参数表示了函数传入真实参数的数量,第二个参数表示参数的列表。
在使用 MP_DEFINE_CONST_FUN_OBJ_VAR 对函数进行对象化转换的时候,需要输入三个参数,第一个参数仍然是转换后的 micropython 对象变量名,第二个参数是这个函数要求最小传入的参数数量(最少0个参数),最后一个参数是方法的名称。
利用这一类的函数,可以创造出参数数量可变的函数,以下是定义的一个求和函数的举例:
STATIC mp_obj_t moopi_sum(size_t n_args, const mp_obj_t *args){
int32_t sum = 0;
for(int i=0;i
(记得把方法添加到字典中)
如有想写一个函数,这个函数要求最少输入 2 个参数,最多可接受 5 个参数,又应该如何写呢?
一种最直接的方式就是在函数中对 n_args 进行判断,因为这个参数说明了传入参数的数量,只要判断这个参数值是否小于等于 5 即可,最小传入 2 个参数在函数对象化转换的时候已经规定了,这里就用做过多判断了。
当参数数量在 2~5 之间的时候计算所有参数的平均值,但如果参数数量大于 5 时候将抛出一个异常,但这个有个问题, 之前函数中抛出异常用的都是 printf ,这个这样会破坏 micropython 的封装,即便抛出了异常,作为二次开发者的用户,也是无法捕获的,所以我们需要一种能够让二开人员捕获到的异常方式,就像是如果参数数量小于 2 时的那种抛异常式。
Traceback (most recent call last):
File "", line 1, in
TypeError: function missing 1 required positional arguments
这就要用到了 micropython 的通知机制,micropython 开发环境中,已经贴心的为我们提供了一组专门用于向外抛出异常的函数:
抛出异常的时候需使用 MP_ERROR_TEXT
宏对字符串进行类型转换。
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
函数对参数进行检测,当不满足要求的时候他会自动抛出错误,这个函数原型如下:
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 对象变量名,最小接受参数数量,最大接受参数数量,函数名称。
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);
(记得把方法添加到字典中)
以上三种方式具体使用哪种方式,需要根据具体环境决定。
python 开发中,除了可以通过按位传值之外,还可以按照字典传值,这也是 python 特有的传值方式,现在很多开发语言争相效仿。
moopi.achieve(name="Mars.CN",score=100);
Mars.CN's score is 100 .
对于此类函数,micropython 开发环境也已经设置好了注册方法。首先,函数原型是:
STATIC mp_obj_t func_name(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args)
参数依次是,传入参数的数量,按位传值的参数列表,按字典传值的参数列表
注册使用:
MP_DEFINE_CONST_FUN_OBJ_KW(obj_name, n_args_min, fun_name)
按参数传值相对于按位置传值的取值方式要复杂很多,但大概分为五步:
首先需要将可能按字典传入的 key 做一个枚举序列:
enum {ARG_name, ARG_score};
这里需要注意的是,如果函数在 python 环境中被调用是,如果参数列表中没有这个 key ,会抛出一个extra keyword arguments given
的异常。
第二步,构建字典取值模板,取值模板其实是一个 mp_arg_t
类型的数组,该结构体中共三个值:
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 表示的是空指针。
第三步,声明一个参数接收数组
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
数组类型是 mp_arg_val_t 格式,使用 MP_ARRAY_SIZE 测量数组的大小。
最后,通过内置的 mp_arg_parse_all 函数将参数从列表中解析出来。
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 结构体不同的成员。
该部分的代码如下:
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 对象,第二个参数指的是按位置传值的最少参数个数。
在面向对象开发的过程中,经常会遇到函数重载的操作,就是一个函数名称,根据参数类型不同所执行的操作不同,在标准 C 语言开发中是不支持函数重载的,但是重载却是 Python 语言的一大特性。所以在使用 C 扩展 micropython 的过程中,我们可以巧妙的利用函数传值的数量、类型等对函数实现重载。
比如有一个需求:有一个函数名称为 size,当直接调用这个参数的时候,返回对象的宽高,但其可以携带一个参数,如果携带一个参数的时候,则同时设置对象的宽高,如果携带两个参数时候分别设置对象的宽高。
分析可知:
所以,我们设计函数的时候需要根据传入参数的数量进行判断,如果没有传入参数,什么都不做,如果 n_args ==1,同时设置对象的宽和高,如果 n_args ==2 这表示要分别设置对象的宽和高。
最后,不论是否传入参数,都返回一个元祖对象(Tuple),元祖中有两个数字值,表示对象的宽和高。
对于判断参数数量的方式,前面的代码中已经讲过,而之前函数中,我们返回的都是基础数据类型,对于 micropython 内置数据类型的返回是第一次遇到。
元组(Tuple)是一个不可变的序列,可以包含任意类型的数据,用圆括号 () 包围起来,简单来说,元祖其实就是一个不可变的数组。
在 C 扩展 micropython 的时候,可以通过 mp_obj_new_tuple
创建一个元祖,该函数有 2 个参数,第一个参数是元祖内数据的数量,第二个参数是一个 mp_obj_t 类型的数组,也就是或,即便是我们返回的是 int 类型数据,你也必须转换成 mp_obj_t 类型的数据,而基础数据类型的数据转换在之前的函数中已经讲到,这里不再重复。
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 环境中,我们可以使用以下代码实验函数重载:
import moopi
moopi.size()
(0,0)
moopi.size(100)
(100,100)
moopi.size(100,200)
(100,200)
注意,这个函数有可能会报错
在编译的时候,有可能会报一些关键字被占用的错误:
/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)的参数设置。
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 做简单的字符串常量还可以,如果稍微复杂的就不行了,具体如何做复杂字符串,希望其他大佬们能给个方案。
{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 的中,类不仅可以包含函数和常量,还可以包含有静态函数、属性,以及特殊方法,比如构造函数、析构函数、打印函数、子集等等。
构造类和构造模块非常类似,按顺序共分为五步:
记得将 .c 文件加入到编译列表
为了增强代码可读性,建议每个类一个 .c 文件,并把公共的部分放在 .h 文件中。所以本次我们需要增加两个文件,moopi.h 和 modobject.c
在 C 扩展 micropython 过程中,一切对象都始于一个 C 的传统结构体,通过这个结构体,得以让 C 和 micropython 进行数据交互,所以每个自定义的类都要包含一个这样的结构体,要求是结构体的第一个成员为 mp_obj_base_t 类型数据,在 mp_obj_base_t 中存放了该类实例的类型、构造函数、析构函数、call函数、打印函数等等,同时这个成员还是递归的, mp_obj_base_t 首个成员仍然是他自己。
除此之外,结构体中就是存放我们这个类所需的一些用于驻留内存的数值了,比如我们例程中需要创建一个名字叫 Object 的类,包含 x、y、width、height、parent 几个成员。
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;
};
这段代码定义完了,但暂时我们还用不到
我们把所有的类型结构体放在 moopi.h 中,这样方便其他文件调用。
在 micropython 中,类的成员字典和模块的成员字典定义方式类似,都是定义一个 mp_rom_map_elem_t 类型的数组,然后通过 MP_DEFINE_CONST_DICT
宏将其转换为 micropython 对象,不同之处在于,定义类成员字典的时候不用写 name 属性。
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);
在上面几节的函数测试中已经说明,在 C 扩展 Micropython 的过程中,一切传值都是 mp_obj_t 类型的,而这个类型的数据都有一个类型字段,可以通过 mp_obj_get_type 函数获得,所以我们自定义的类也必须有这样一个原型,就像定义模块一样。
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 函数获取的物理名称;第三个成员是类的成员字典。
回到 modmoopi.c 文件中,在类成员中加入新创建的类。
{MP_ROM_QSTR(MP_QSTR_Object), MP_ROM_PTR(&moopi_type_object)},
此时编译烧录已经可以看到类成员了,但成员还没有办法实例化。
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']
到此为止,类打添加就已经完成了,但此时如果尝试实例化类,系统则会崩溃:
obj = moopi.Ojbect()
构造函数不是必须的,我们可以通过其他方式构造类,但如果没有构造函数,用户在尝试实例化对象的时候会导致系统崩溃,所以,如果我们可以不提直接构造方式,但必须保证存在构造函数,要不然就别加到模块字典中,不加到模块的字典中这个类也是存在的,只是不能显示的实例化而已。
上一节中,在定义类原型的时候,我们只给原型添加了三个必要参数,第四个必要参数就是 make_new
,这是一个函数,当用户尝试构造一个类实例的时候,系统会调用该函数并返回对应的实例,或者返回空对象(禁止构造)。
该函数原型是:
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 即可,但如果允许实例化,那么在这个函数中必要做的几件事如下:
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 的方式,另外还有其他几种方式:
说实在的,除了前两个,后面这些我基本也没有深研究是干啥用的,希望其他大佬能够补充一下,最常用的就是上面两个,第二个等下面讲到析构函数的时候再给大家细讲。
函数中第二行也是必须的,创建完对象之后,必须显式的为这个对象设置类型,否则在实例化阶段一样会报错。
最后,通过 MP_OBJ_FROM_PTR 宏将对象强制转换为 mp_obj_t 类型对象返回。
此时在通过实例化的方式获得对象,就已经可以获得成功了,并且通过 dir
去查看这个类实例的时候,可以看到他存在一个 class 的成员,这个成员的值是 Object,也就是我们类的物理名称。
micropython 有严格的内存管理机制,当在 Micropython 环境下使用变量对对象进行一次引用后,对象引用计数器会加一,当失去一次引用后,引用计数器会减一,当引用计数为0的时候,会进入系统回收状态,但此时不会进行及时回收,而是当系统 gc 线程调用 gc.collect() 的时候进行回收。
obj = moopi.Ojbect() 及对对象产生了一次引用
a = obj 引用加一
obj = 1 引用减一
如果在创建对象的时候使用了 m_new_obj_with_finaliser
,则系统会管理对象的应用与空间释放,但如果使用 m_new_obj
创建对象,则需要我们手动释放空间。
在 C 扩展 micropython 的时候,环境并没有像提供构造函数那样提供析构函数的注入方式,需要我们自己给成员字典增加一个 del 的成员才可以,但这个成员使用过程中会存在一些问题,后面会讲到。该函数的原型如下:
STATIC mp_obj_t destructor(mp_obj_t self_in)
其中 self_in 及当前对象的指针,直接返回 mp_const_none 即可。
在这个函数中,需要通过 m_del_obj
函数释放对象:
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);
该函数需要注册到类的成员列表中:
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() 函数的时候系统才会自动回收。
下面我们在析构函数中加一行输出,测试一下。
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 析构函数,而是提供一个手动析构函数。
类对象的方法和函数与库的方法定义方式基本相同,不同之处在于,类方法至少有一个参数,并且第一个参数永远是类实例自身,从第二个参数开始才是真正调用方法时候传入的参数。
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 操作起来十分方便。
import moopi
obj = moopi.Object()
obj.size(100,200).location(10,20)
上一小节中定义了 size 和location 函数,这些函数可以通过类实例直接的方式调用,但实际上内部调用仍然是通过对象进行调用的(这个是 Python 的成员函数调用机制),即通过以下方式调用:
moopi.Object.size(obj,100,200)
这样可以直观的感受到,为什么在实际转到 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()
系统就抛出参数个数不对的错误,所以这是一个伪静态方法。
不过仍然可以通过一些方案解决,但终归不是很舒服,有种破坏封装的遗憾。
当我们实例化一个对象后,在 REPL 环境下尝试打印这个对象的时候,发现输出的结果是 ,但如果去看其他的类,比如 machine.Pin 生成的实例,直接输入对象名打印的时候是 Pin(1),那这是如何实现的呢?
其实这个方法就藏在了类的原型中。
上面章节中提到过,所有类的原型都是一个 mp_obj_type_t 结构体,这个结构体中有一些特殊成员,有一些我也没用过,也不咋认识,我挑一些认识的讲一下:
标志 | 成员 | 说明 |
---|---|---|
√ | base | 类型的头,所有类都包含这个,这里我们恒定为 {&mp_type_type} |
flags | 该类型相关的标志位,用于指示类型的特性和行为 ,这个没用到过 | |
√ | name | 类的实际名字,不是模块中显示的名字,而是通过 obj.name 打印出来的名字,这两个名称实际上是可以不同的 |
* | 打印函数指针,指向实现__repr__和__str__特殊方法的函数,用于打印对象的字符串表示形式,通过在 REPL 环境下直接输入变量名,或者通过 pirnt() 函数打印出来的内容 | |
√ | make_new | 初始化函数指针,指向实现__new__和__init__特殊方法的函数,用于创建该类型的实例对象 |
* | call | 指向实现__call__特殊方法的函数,允许以类似函数调用的方式使用该类型的实例对象 |
* | unary_op | 指向实现一元操作的函数,用于支持对象的运算操作 |
* | binary_op | 指向实现二元操作的函数,用于支持对象的运算操作 |
* | attr | 指向实现属性的加载、存储和删除操作的函数 |
* | subscr | 指向实现下标运算的加载、存储和删除操作的函数 |
* | getiter | 指向迭代器获取函数 |
* | iternext | 指向迭代器的下一个元素的函数 |
buffer_p | 如果该类型支持缓冲区协议,指向实现缓冲区操作的函数,没怎么用到过,不过应该挺有用的 | |
protocol | 指向其他特定协议或接口的结构体或函数指针,也没用到过 | |
* | parent | 指向父类型的指针,可以是单个父类型的指针,也可以是包含多个父类型的元组对象 |
√ | locals_dict | 一个字典对象,用于存储类型的局部方法、常量等 |
上面表格中标注 √ 的是已经讲过的,标注 * 的是接下来会讲到的,没有做任何标注的是我也没用过的,不能拿出来误导大家
本小节着重讲打印输出函数,其函数原型是:
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 参数即可, 第二个是格式化字符串,后面的值格式化字符参数。
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 成员即可。
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 效果是相同的,这个函数的作用是能够让对象变得像函数一样能够直接被调用,比如:
obj = moopi.Object()
obj()
这个函数的原型是:
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 小节。
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 指向。
该方法的用法是使用类的对象加括号调用,而不是直接类名加括号,类名加括号调用的是构造函数:
import moopi
obj = moopi.Object()
obj()
在网上很多资料中,都是在 micropython 中没有办法给类添加属性,所以只能使用 set/get 方法 实现属性的修改,而且在 ESP32 官方代码中也没有使用属性,用的都是方法实现的。但经过对 micropython 源码的分析,其实是可以通过原型中的 attr 元素直接(并不是间接)实现属性的,也就是本来人家 micropython 就提供了属性是扩展方式,有可能是早起版本中不能直接使用,导致了大家对这部分有所谓误解。
属性元素的函数原型如下:
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 最为稳妥。
所以我们可以为其增加一个属性函数:
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 的函数指向:
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,
};
测试一下:
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 函数开头加入以下代码即可:
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']
做过 C++ 开发的同学们都应该有印象, C++ 中有个非常便捷的功能,就是对类的运算符进行重载,比如我们写了一个类叫 Object,这个类的实例名叫 obj,重载运算符后可以使用 a = obj+1, 或者 obj<<3 这样的操作,非常方便,作为胶水语言的 Python 自然不能丢弃这么优秀的编程方式,所以在 micropython 中也非常完美的继承了这一特性,并且可以通过原型的 .unary_op 和 .binary_op 分别对一元运算符和二元运算符进行重载。
一元运算符重载函数原型如下:
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 环境的设定,返回指定类型的值。
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; 否则会出错。
函数设置好后,可以使用代码进行测试:
-obj
len(obj)
hash(obj)
Micropython 的函数重载明显比 C++ 的要强大,仅是一元运算符就这么多了(可惜没有 ++ – 的操作),二元运算符就更多了,而且非常复杂和繁琐,我认识的大概有 34 个,其他还有很多,但都没找到相关资料,没法给大家讲解了。
二元运算符操作函数原型是:
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; 否则编译不过去。
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;
}
测试:
import moopi
obj = moopi.Object()
obj<10
True
obj.x=100
obj<10
False
o|0xAA
255
obj | = 0xAA
obj
<MooPi:Object>(x:0, y:0, width:0, height:0, byte_val:0xFF)
众所周知,Python 在数据处理方面有极大的优势,不仅在于他有非常强大的三方函数库,他还具有非常人性化的操作手法,比对于字典的操作,可以使用类似 dict['key']
这种方式直接存取数据,相比之下,比 JAVA 和 C# 中的字典都好用的多。这种方式叫做下标操作,中括号中的内容及可以是字符串,也可以是数字,甚至可以是任何类型,简直爽的一批。
而在 C 扩展 micropython 的过程中,为类增加下标操作也非常简单,只要为原型添加 .subscr 属性即可,该属性对应的函数原型是:
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 操作。
所以,这个函数可以写的极为简单:
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 字段,可以查看所对应的值是否存在。
最后让我们来一波疯狂的测试:
import moopi
obj = moopi.Object()
obj['key'] = 10
obj['key']
10
obj[10] = 100
obj[10]
100
obj[1] = moopi.Object()
obj[1]
<MooPi:Object>(x:0, y:0, width:0, height:0, byte_val:0x55)
obj[obj[1]]=123
obj[obj[1]]
123
相当的完美!
上一阶段,我们将 Object 类武装成了一个具有字典功能的对象,如果想查询 Object 中一共存储了多少个键值对,可以用 len() 函数,结合前面学到的运算符重载功能即可实现,但如果想使用 iter() 函数遍历这个这个对象呢?目前还不能实现。
我们看下面这段代码:
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 操作,两个函数原型长这样:
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 表示迭代结束,否则返回这个键值对的元祖。
/**
* @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));
}
到此为止,霍霍类原型的成员们已经讲的大差不差了,buffer_p 和 protocol 还没来得及研究, LVGL 中道是用到这个了,带我研究完毕之后再向各位做汇报。
本小细节收个尾,讲一下类的继承。
类的继承在 github 上跟开发组交流了好长时间也没搞明白,可能是语言障碍(我用谷歌翻译的),也可能就本人是单纯的理解能力差,他们给的答案,以及 GPT 个的答案都是写 .parent 成员,这个咱也写了,效果是有一点的,但是差点意思,我把这个叫名义上的继承,但实际上并没有达成。
按照 7.1 章节中构建类的五个步骤,新添加一个 Label 子类:
定义类的类型结构体
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 对象
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 函数
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 标签的内容。
定义类原型
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 成员
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 的所有属性才对,我们试一下:
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
};
烧录测试:
import moopi
dir(moopi.Label)
['__class__', '__name__', '__bases__', '__del__', '__dict__', 'location', 'new', 'size', 'text']
看,除了我们自己添加的这些成员之外,还新增加了 Object 的成员,事情看似完美,但老天就非得在你得意的时候给你一棒槌!
我们继续测试:
lab = moopi.Label("ABC")
dir(lab)
['__class__', 'text']
实例化对象后发现,进存在自身的成员,父类的成员丢的一干二净。
通过阅读其他 Micropython 的代码,以及翻阅 Micropython 的源码,还是找到了解决方案,就是为原型增加 .attr 成员,在 .attr 中把父类的属性都列出来,就像我们给 Object 增加属性的时候想法是一样的:
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__ 等等。