在 MicroPython 中调用 ujson 内置模块的功能的实例,讲点 MicroPython C 层面的解释执行、异常捕获、内存回收、内置对象等内容。

oh!!!想起来了,刚好可以写一篇关于深度使用 micropython 的文章!会有一些额外的知识讲解哒。

起因

这次的起因是需要一个 C 与 Python 层面共用的配置模块,就是想在 micropython 中准备一个配置项的功能,刚好也讨论到了不妨直接使用 ujson 的模块功能。(ujson 就是 json),然后这个模块的 Python 代码通常是这样的。

#!/usr/bin/python3
import json

# Python 字典类型转换为 JSON 对象
data1 = {
    'no' : 1,
    'name' : 'Runoob',
    'url' : 'http://www.runoob.com'
}
 
json_str = json.dumps(data1)
print ("Python 原始数据:", repr(data1))
print ("JSON 对象:", json_str)
 
# 将 JSON 对象转换为 Python 字典
data2 = json.loads(json_str)
print ("data2['name']: ", data2['name'])
print ("data2['url']: ", data2['url'])

这个代码,在 MicroPython 中本来就实现了,也同样可以直接使用。

但有个问题,我要如何在 C 语言的层面调用它呢?我 Python 代码的层面和 C 代码的层面,它们彼此间是如何共通的呢?

这个就涉及到对 Python 解释器的理解了,刚好上次介绍了 MicroPython 后,说好要给源码分析的一直没给,那趁着这次机会,稍微提及提及。

理解 MicroPython 的 bytecode (bc) 结构

事实上,很多人都理不清 Python 的解释执行的机制,但实际上我们只需要理清几个节点就可以帮助我们构造整个 MicroPython 的架构了。

说点 micropython 的接口调用关系来说明内部的调用关系,但事实上在调用各类模块的过程中是动态加载到内存的,并非直接的函数调用过程,而是动态的从 globals dict 中提取模块的指针和参数进行执行,这个部分就不能在静态分析中表达出来了。

(以后把图放上来,现在生成的图基本没法看,诺,就跟下面这个一样)

我还是简单画个图说明一下吧。

这是第一层,当我们使用 execute_from_lexer 进行执行的载体可能有 file \ repl \ str 这三类输入源,mp_parse_compile_execute 进行 Python 代码的解释(Python)、编译(bytecode)、执行(bytecode)。

其中,我们平时所认知的层次只在解释这一层,这次我就多说一点了。

在代码中是这样的关系。

基本上你看到这里已经算是知道一点基础的内容了,现在我们直接开始看代码。

MicroPython 中的 C 函数关系

自上次介绍添加 C 层面的看门狗模块,我就没有再提及和 MicroPython 核心有关的内容,这里不讲解 QSTR 的编译生成,我假设你已经具备一些基本的 MicroPython 基础。

我们要知道 Python 层面可以实现的功能,在 C 层面一定也可以实现,它实际上就是一堆结构体的传递和对象的类型的执行,为此我直接代码举例说明吧。

对 import ujson 调用实例

我们知道 json 是 Python 的一个内置模块,其定义如下。

而它被添加到 mp_builtin_module_table 这个静态定义的内置模块表上,方便 load 函数的查找和加载。

我们知道这是在编译器决定的操作,同时因为有这种动态表,所以代码中不会存在直接调用的方法去对其执行,所以我们要在 C 层面执行如下 Python 代码操作。

import josn
nresult = josn.loads({"a":1,"b":2,"c":3,"d":4,"e":"helloworld"})
print(result)

为了调用该 Json 模块的 C 函数功能,我们可以拆分成如下三个操作。

  • call import josn

获取 json 模块,得到该对象的结构,对该对象结构调用其成员函数 josn.loads ,那么如何获取 json 模块呢?

需要注意的是,如果存在头文件的接口,那我们可以直接调用目标 C 函数即可完成对 Json 功能模块的调用,但事实上并没有这样的头文件接口引出,要修改源码才能暴露接口出来,假设在确保代码最小改动的情况下,我们可以走 micropython 核心的接口去获取我们想要的函数指针了,看如下代码。

mp_obj_t module_obj = (mp_obj_t)mp_module_get(MP_QSTR_ujson);
if (module_obj != MP_OBJ_NULL) {
    // import josn
}

通过 mp_module_get 查询表中的 MP_QSTR_ujson 模块,这样就可以得到这个 json 模块的变量。

  • call josn.loads

得到了 json 模块后,我们就要进一步判断该模块是否有我们想要的函数 josn.loads ,有如下代码。

mp_obj_t dest[3];
mp_load_method_maybe(module_obj, MP_QSTR_loads, dest);
if (dest[0] != MP_OBJ_NULL) {
    // get josn.loads
}

假设存在,则从这里我们可以获取到 json 模块的 loads 函数(method),同时还在 dest 变量中拥有了 loads 的函数指针,以及可以填充的形参列表。

接着我们构造函数实参如 {"a":1,"b":2,"c":3,"d":4,"e":"helloworld"} 通过 mp_call_method_n_kw 执行对应函数功能,接收其返回值到 result 中,代码则如下。

const char json[] = "{\"a\":1,\"b\":2,\"c\":3,\"d\":4,\"e\":\"helloworld\"}";

dest[2] = mp_obj_new_str(json, sizeof(json) - 1);

mp_obj_t result = mp_call_method_n_kw(1, 0, dest);

实际上在这里我们需要知道的就是填入参数的方式,假设调用的是 json.loads() 是需要一个 str 的对象传入的函数,则它对应的调用方法为 mp_call_method_n_kw(1, 0, dest),事实上可以可以直接用内部的接口,但这个接口是可以传递任意参数的函数,所以使用它就足够了。

其函数原型如下。

// args contains: fun  self/NULL  arg(0)  ...  arg(n_args-2)  arg(n_args-1)  kw_key(0)  kw_val(0)  ... kw_key(n_kw-1)  kw_val(n_kw-1)
// if n_args==0 and n_kw==0 then there are only fun and self/NULL
mp_obj_t mp_call_method_n_kw(size_t n_args, size_t n_kw, const mp_obj_t *args) {
    DEBUG_OP_printf("call method (fun=%p, self=%p, n_args=" UINT_FMT ", n_kw=" UINT_FMT ", args=%p)\n", args[0], args[1], n_args, n_kw, args);
    int adjust = (args[1] == MP_OBJ_NULL) ? 0 : 1;
    return mp_call_function_n_kw(args[0], n_args + adjust, n_kw, args + 2 - adjust);
}

具体你想如何去调用函数,就需要你自己去查阅 runtime.h 中提供的接口了,事实上这里还要分清 function 和 method 的关系。

通常我们说的 function 指的是独立的函数或方法,而 method 虽然也翻译成同样的意思,但它是特指有所属模块(module)或对象(self)的,这也就造就了在调用和理解的时候存在不同的意义。

  • print dict = josn.loads()

调用 mp_obj_print_helper 打印 mp_obj 对象内容,代码只需要 mp_obj_print_helper(&mp_plat_print, result, PRINT_STR); 即可执行 print(result) 的 Python 代码,但这个方法会去判断和识别该对象的字符串化的方法,从而调用对应的 print 回调函数去打印内部的内容出来。

至此,以后你想要对其他功能模块进行操作,是不是就知道怎么做了?

当然,你也可以像我前面所说的,修改头文件暴露 C 的接口函数 直接调用,而非我这样的动态查找调用,前提是代码足够的解耦,毕竟解释器的输入接口就是这样设计的。

对 nlr (no local return)的使用

在前面的代码示例中有对代码进行一个异常机制的保护,而事实上 json 模块是会抛出 mp_raise_ValueError("syntax error in JSON") 异常的,表示字符串解析成 json 失败。

如果我们期望代码是写成下面这样,以提高代码的对异常的抵抗性。

import josn
try:
    nresult = josn.loads({"a":1,"b":2,"c":3,"d":4,"e":"helloworld"})
    print(result)
except Exception as e:
    print(e)

那我们就要在执行的代码之前载入 try 的地址,以便发生异常的时候转移到 except 语句块当中,则 C 代码如下。

if (nlr_push(&nlr) == 0) {
	mp_obj_t result = mp_call_method_n_kw(1, 0, dest);
	mp_printf(&mp_plat_print, "print(result)\r\n");
	mp_obj_print_helper(&mp_plat_print, result, PRINT_STR);
	mp_printf(&mp_plat_print, "\r\n");
	nlr_pop();
}
else {
	mp_obj_print_exception(&mp_plat_print, (mp_obj_t)nlr.ret_val);
}

当它在 if (nlr_push(&nlr) == 0) {} (try)语句块中触发异常的时候 通过 nlr_raise(nlr_jump) 直接跳转到 esle {} 语句块当中,也就是所谓的不在此处返回(no-local-return),从而用户在此处进行 except 的后续处理。

但我把这个过程说的详细一点就是,这里 nlr 是借用了 setjump 和 longjump 的思想,在 nlr_push 的时候将当前的语句位置存储下来,然后通过 setjmp 记录地址,并默认返回结果为 0 ,进入保护范围({),如果期间出现了 nlr_raise(nlr_jump) 则回到 setjmp 处返回 1 ,从而回到进入前的入口,并离开现场,此时就可以实现其他操作,值得注意的是 nlr_pop 表示异常捕获边界结束(})。

关于这个的 nlr 设计思想,中文的资料讲得不是很好,我在 wiki 找个标准 C 实现的说明给放这了 http://web.eecs.utk.edu/~mbeck/classes/cs560/360/notes/Setjmp/lecture.html

最后我们只需要知道,我们把这个 nlr 的使用就看作是 C 层面的异常机制就可以了。

对 gc 回收内存的机制重建缓存

设计了这个存取 json 配置的模块以后,为了更好的结合到 micropython 环境当中,我就把存储的结果 dict 对象的节点(mp_obj_t)保存起来了,但事实上这些节点(mp_obj_t)是有可能因为 Python 层面上没有做出标记而被回收,因为是直接从 C 层面产生的。

在 MicroPython 这种设计为具备回收内存的语言当中,大多数时候的对象都存储着对内存的标记,这个在 C# CLR 中也是一样的设计,所以我们应当在使用前,判断当前的 mp_obj_t cache; 对象是否还可以继续使用。

就像下面的代码这样。

if (false == mp_obj_is_type(config_obj->cache, &mp_type_dict)) {
	// maybe gc.collect()
	if (mp_const_false == maix_config_cache()) {
	    return def_value;
	}
}

如果是内置的结构则可以使用类似于 mp_obj_is_type 这样的接口,而我所用的是用来处理非 micropytho 核心的结构判断。

这样的好处就是,假设不用了就放心的回收,也不用担心数据是否还占用着内存,反正用的时候发现没有了就重建。

同样的,你也不必害怕,这个变量到底还能不能用了,如果出现了什么 core dump 的情况,多半要仔细检查检查有没有可能被回收了。

对 dict 对象(map)的操作

由于 micropython 没有将这个 dict 对象相关的功能函数暴露出来,导致在遍历接口的时候,只能跳过 Py 核心层的接口,直接将目标对象当作 map 对象执行 C 的 mp_map_lookup 操作。

如果想要获取 dict 中 key 为 your_find_key 的对象,就如下 C 代码操作。

const char goal[] = "your_find_key";
mp_obj_dict_t *self = MP_OBJ_TO_PTR(result);
mp_map_elem_t *elem = mp_map_lookup(&self->map, mp_obj_new_str(goal, sizeof(goal) - 1), MP_MAP_LOOKUP);
mp_obj_t value;
if (elem == NULL || elem->value == MP_OBJ_NULL) {
	// not exist
}
else {
	value = elem->value;
	//mp_check_self(mp_obj_is_str_type(value));
	mp_printf(&mp_plat_print, "print(result.get('%s'))\r\n", goal);
	mp_obj_print_helper(&mp_plat_print, value, PRINT_STR);
	mp_printf(&mp_plat_print, "\r\n");
}

如果想要遍历 mp_obj_dict_t 对象,则使用如下原型函数进行迭代器遍历操作。


mp_map_elem_t *dict_iter_next(mp_obj_dict_t *dict, size_t *cur) {
    size_t max = dict->map.alloc;
    mp_map_t *map = &dict->map;

    for (size_t i = *cur; i < max; i++) {
        if (mp_map_slot_is_filled(map, i)) {
            *cur = i + 1;
            return &(map->table[i]);
        }
    }

    return NULL;
}

mp_obj_dict_t *self = MP_OBJ_TO_PTR(tmp);
size_t cur = 0;
mp_map_elem_t *next = NULL;
bool first = true;
while ((next = dict_iter_next(self, &cur)) != NULL) {
		if (!first) {
				mp_print_str(&mp_plat_print, ", ");
		}
		first = false;
		mp_obj_print_helper(&mp_plat_print, next->key, PRINT_STR);
		mp_print_str(&mp_plat_print, ": ");
		mp_obj_print_helper(&mp_plat_print, next->value, PRINT_STR);
}

这样操作实际上就直接跳过了对 DICT 对象的函数操作,也不需要像前面 json 模块那样去请求一个模块和调用模块函数,直接操作指针就行,但那样代码会呈现冗余,看个人的选择吧。

对接 K210 的特定功能函数

这是由于我们在编写 MicroPython 函数的时候,实际芯片的移植可能会和我们所期待的接口有所差异导致的,运气好的是,我需要的 String 对象可以通过 fs_info_t *cfg = vfs_internal_open("/flash/config.json", "rb", &err); 得到。

这个 json.load 函数是允许用户传递一个 StringIO 的对象(mp_obj_stringio_t)进来直接代理执行 read 操作的 C 函数,StringIO 函数通常会提供 read 操作,就像下面这样。

只要我们能够提供一个同类对象进去即可,而 open 得到的 file 对象就是一个底层具备 StringIO 基础协议的对象(_mp_stream_p_t)。

所以我们可以直接将读取的文件对象直接导入 json.load 函数当作,类似于如下 Python 代码。

# 写入 JSON 数据
with open('data.json', 'w') as f:
    json.dump(data, f)
 
# 读取数据
with open('data.json', 'r') as f:
    data = json.load(f)

在 k210 实现的特殊 file 读写函数如下

这时将与 json 模块当作的操作对象保持一致

即可实现上述 Python 代码同样效果的 C 代码。

后记

看完后是不是更理解了 MicroPython 了呢?是不是没有想象的那么难呢?

这里提及的完整代码已经合并入 MaixPy 仓库,以后有兴趣的可以自己去了解。

这篇文章以后会单独拆分的,因为最后写完,发现信息量过大,而且一些不同类型的内容,也不应该出现在一篇文章当中。

留个痕迹 junhuanchen@qq.com 2020年6月10日。

posted @ 2020-06-09 18:57  Juwan  阅读(1542)  评论(1编辑  收藏  举报