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日。