zoukankan      html  css  js  c++  java
  • 在 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)
    ", 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)
    ");
    	mp_obj_print_helper(&mp_plat_print, result, PRINT_STR);
    	mp_printf(&mp_plat_print, "
    ");
    	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'))
    ", goal);
    	mp_obj_print_helper(&mp_plat_print, value, PRINT_STR);
    	mp_printf(&mp_plat_print, "
    ");
    }
    

    如果想要遍历 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日。

  • 相关阅读:
    netty之微信-Netty 环境配置(四)
    netty之微信-Netty 是什么?(三)
    netty之微信-IM简介(二)
    netty之微信-效果展示(一)
    为什么选择netty?
    [转]Python调用(运行)外部程序
    聚会游戏
    JavaScript点击事件-一个按钮触发另一个按钮
    文本框输入事件:onchange 、onblur 、onkeyup 、oninput
    js如何使两个input里的内容实时变化
  • 原文地址:https://www.cnblogs.com/juwan/p/13079797.html
Copyright © 2011-2022 走看看