前言

最近在研究micropython的源码编译过程,简单记录下关于qstr部分内容,本篇文章基于micropython1.18版本源码,1.19版本及之后可能会略有差异。

标识符与相应对象的联系

Micropython中有很多标识符,例如lcd.py中出现的标识符有:import、lcd、init、print、“hello”。这些标识符最终都需要与某个对象或操作联系起来。那么这种联系是如何建立的呢?那就是通过QSTR。

QSTR是uniQue STRing的简称,是一种字符串内存驻留方法。我们知道同一个标识符可能在源代码中出现多次,如果我们在每个出现的地方都要保留一份这个标识符的拷贝,就会相当占用存储空间。
Micropython采取的方式是在存储空间内仅保留一份标识符主体,而每个标识符主体都有一个索引号,代码中凡是使用这个标识符的地方,都使用其索引号代替。而最终执行时就通过这个索引号去内存中寻找对应的标识符。这就是Micropython中所谓的QSTR。
Micropython的C代码中所有需要使用QSTR的地方都用MP_QSTR_xxx表示,比如lcd就用MP_QSTR_lcd表示,这个MP_QSTR_lcd就是lcd这个标识符的索引号。Micropython的Makefile会搜索C源文件中的所有MP_QSTR_xxx,并生成QSTR池,其中存放了QSTR对应的标识符的长度和哈希值。

以我编译unix移植为例Micropython的C代码中的QSTR定义最终会被放入unix/build-standard/genhdr/qstrdefs.generated.h
中。
分析下qstrdefs.generated.h的生成过程,可以看到改文件头记录着这样一句话

// This file was automatically generated by makeqstrdata.py

QDEF0(MP_QSTRnull, (const byte*)"\x00\x00\x00" "")
QDEF0(MP_QSTR_, (const byte*)"\x05\x15\x00" "")
QDEF0(MP_QSTR___dir__, (const byte*)"\x7a\x8f\x07" "__dir__")
...

其中是一系列的宏定义,格式为:

QDEF(MP_QSTR_xxx, (const byte*)"哈希值(2字节)长度(1字节)" "对应的字符串"

这个文件是由makeqstrdata.py生成的,该文件位于py/makeqstrdata.py,py目录下有几个python脚本用于代码预处理c代码,包括生成qstr池,注册c模组。

QSTR生成流程

  1. 首先通过py/makeqstrdefs.py查找C源码中的MP_QSTR_XXX关键字,然后提取到ports/unix/build-standard/genhdr/qstr目录下建立新文件,增加条目Q(XXX)
  2. 然后将qstr目录下若干文件整合成一个文件ports/unix/build-standard/genhdr/qstrdefs.collected.h
  3. py/makeqstrdata.py将根据ports/unix/build-standard/genhdr/qstrdefs.collected.h生成QSTR池文件unix/build-standard/genhdr/qstrdefs.generated.h

QSTR的hash生成设置

C源码中计算qstr hash的位置位于qstr.c中

// this must match the equivalent function in makeqstrdata.py
mp_uint_t qstr_compute_hash(const byte *data, size_t len) {
    // djb2 algorithm; see http://www.cse.yorku.ca/~oz/hash.html
    mp_uint_t hash = 5381;
    for (const byte *top = data + len; data < top; data++) {
        hash = ((hash << 5) + hash) ^ (*data); // hash * 33 ^ data
    }
    hash &= Q_HASH_MASK;
    // Make sure that valid hash is never zero, zero means "hash not computed"
    if (hash == 0) {
        hash++;
    }
    return hash;
}

这个计算方法跟makeqstrdata.py脚本中一致的。
micropython编译前可以通过宏定义设置,控制hash输出按1字节还是2字节,字符串长度值占用多少字节。

hash长度设置

在qstr.c中能看到MICROPY_QSTR_BYTES_IN_HASH宏判断,该值可赋1或2,但如果不设置则通过其他方式确定,在py/mpconfig.h文件中通过设置MICROPY_CONFIG_ROM_LEVEL来控制hash 长度是1 还是2,参见

// Number of bytes used to store qstr hash
#ifndef MICROPY_QSTR_BYTES_IN_HASH
#if MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES
#define MICROPY_QSTR_BYTES_IN_HASH (2)
#else
#define MICROPY_QSTR_BYTES_IN_HASH (1)
#endif
#endif

MICROPY_QSTR_BYTES_IN_HASH未被定义时参考MICROPY_CONFIG_ROM_LEVEL的设置,micropython给目标机划分了6个级别,用于适应不同硬件资源的编译。

// Disable all optional features (i.e. minimal port).
#define MICROPY_CONFIG_ROM_LEVEL_MINIMUM (0)
// Only enable core features (constrained flash, e.g. STM32L072)
#define MICROPY_CONFIG_ROM_LEVEL_CORE_FEATURES (10)
// Enable most common features (small on-device flash, e.g. STM32F411)
#define MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES (20)
// Enable convenience features (medium on-device flash, e.g. STM32F405)
#define MICROPY_CONFIG_ROM_LEVEL_EXTRA_FEATURES (30)
// Enable all common features (large/external flash, rp2, unix)
#define MICROPY_CONFIG_ROM_LEVEL_FULL_FEATURES (40)
// Enable everything (e.g. coverage)
#define MICROPY_CONFIG_ROM_LEVEL_EVERYTHING (50)

// Ports/boards should set this, but default to level=core.
#ifndef MICROPY_CONFIG_ROM_LEVEL
#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_CORE_FEATURES)
#endif

// Helper macros for "have at least this level".
#define MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES (MICROPY_CONFIG_ROM_LEVEL >= MICROPY_CONFIG_ROM_LEVEL_CORE_FEATURES)
#define MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES (MICROPY_CONFIG_ROM_LEVEL >= MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES)
#define MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES (MICROPY_CONFIG_ROM_LEVEL >= MICROPY_CONFIG_ROM_LEVEL_EXTRA_FEATURES)
#define MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES (MICROPY_CONFIG_ROM_LEVEL >= MICROPY_CONFIG_ROM_LEVEL_FULL_FEATURES)
#define MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING (MICROPY_CONFIG_ROM_LEVEL >= MICROPY_CONFIG_ROM_LEVEL_EVERYTHING)

由此可以得知,MICROPY_CONFIG_ROM_LEVEL决定的hash长度,在6个级别中除了MINIMUM长度为1外其他都为2。

对象字符串长度字段的字节长度

前面我们分析过qstr宏定义的组成

QDEF(MP_QSTR_xxx, (const byte*)"哈希值(2字节)长度(1字节)" "对应的字符串"

1字节的长度位表示的事后面对象或函数名字字符串的长度,1字节长度意味着我们对象名字符串最大不超过256,如果超过那就要用2字节来表示了。
可以看到mpconfig.h中有这样的宏定义

// Number of bytes used to store qstr length
// Dictates hard limit on maximum Python identifier length, but 1 byte
// (limit of 255 bytes in an identifier) should be enough for everyone
#ifndef MICROPY_QSTR_BYTES_IN_LEN
#define MICROPY_QSTR_BYTES_IN_LEN (1)
#endif

这样我们可以在mpconfigport.h中添加MICROPY_QSTR_BYTES_IN_LEN宏的设置就可以更改字节位长度。不过一般来说不会有超过256长度函数名,所以一般不用设置该项,只需留意有这个配置就行。