
Flipper Zero 固件中的 M*LIB纯 C 语言的泛型类型安全容器库实战指南【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware导读本文围绕 Flipper Zero 固件仓库flipperzero-firmware中内置的 lib/mlib/README.md 展开系统讲解 M*LIBM star lib这一纯 C 语言的泛型、类型安全容器库的设计思想、OPLIST 核心机制、内存分配与错误处理策略以及它在当前固件中的真实落地方式。读完本文你将掌握如何用_DEF系列宏为任意 C 类型实例化出带构造/析构语义的数组、链表、字典、树、字符串等容器理解M_LET/M_EACH等语法增强宏的用法并能定位 M*LIB 在 Flipper Zero 源码如 GUI 的ViewDispatcher、底层FuriString中的实际调用链。M*LIB 是什么为 ISO C99/C11 打造的STLM*LIB 是一个只用头文件header-only实现的 C 语言容器库仓库中 lib/mlib 目录下全部是m-*.h头文件没有任何需要编译的 C 源文件。你只需要把对应头文件加入编译器的头文件搜索路径即可使用除其他 M*LIB 头文件和标准 C 库之外没有任何外部依赖。这一点在固件的构建脚本中也有体现lib/SConscript 将mlib作为env.BuildModules([...])的一个纯头文件模块直接编入CPPPATH。它的定位可以概括为C 语言版的 C STL但不是严格的映射M*LIB 与 STL 各自拥有独有的容器。其核心价值在于泛型与类型安全不是靠void* 强转而是让宏展开生成带正确原型的内联函数从而让编译器在传入错误类型时至少产生一条警告对象语义完整保留容器内的对象可以拥有自己的构造、析构以及其他方法因此可以构造出container-of-container-of-type-T这种完全递归的对象而不丢失类型信息零开销所有函数都以inline形式生成编译器可以对库调用进行完整优化性能与手写 C 底层访问相当安全性优先调试模式下大量使用防御式编程如对越界访问做边界检查、校验红黑树的固有性质库内部尽量减少 castcast 是安全性的敌人通用性不是直接由宏完成而是由宏定义出具有正确原型的内联函数间接完成保证用户调用能得到正常的警告检查。其它设计原则还包括不重写 C 库只在它的基础上包装例如不自己重写sort而是提供稳定排序不为用户用不到的功能买单按需生成代码。需要特别注意的是文档中的措辞约定shall表示用户必须遵守的约束违反将导致未定义行为should表示建议除特别说明外所有指针参数均期望非空。组件全景从序列容器到线程同步README 将 M*LIB 的组件按功能分成几大类所有头文件都可以独立包含彼此依赖保持最小。每个容器都定义了自己的迭代器且所有容器尽量暴露同一套接口方法名相同则语义相同、用法相同少数场景会按容器特性做适配。不需要修改用户结构体的普通容器头文件容器说明m-array.h数组可变大小、可按下标访问的可增长数组m-list.h单向链表变长支持LIST_DEF/LIST_DUAL_PUSH_DEFm-deque.h双端队列前后两端都可增删m-dict.h字典/集合多种实现链式哈希、缓存哈希、开放寻址m-rbtree.h红黑树有序二叉树m-bptree.hB 树更适合现代缓存架构的有序容器m-tuple.h元组任意类型元素的有限有序列表m-variant.h变体类似 C union但动态记录当前存储的元素类型m-prioqueue.h优先队列基于堆实现永远取出最小值用于线程同步的容器m-buffer.h固定大小的环形队列/栈面向多生产者/多消费者MPMC内部用互斥锁 条件变量实现还提供QUEUE_MPMC_DEF、QUEUE_SPSC_DEF等更快的变体m-snapshot.h快照缓冲让读写速度不同的线程之间无等待地交换最新数据实现上接近共享原子寄存器SPSC 用三缓冲实现m-shared.h共享指针类似std::shared_ptr原子跟踪引用计数、线程安全销毁m-concurrent.h把普通容器加锁包装成并发容器无迭代器m-c-mempool.hWIP 状态的快速并发内存分配头。侵入式容器需要修改自己的结构体m-i-list.h双向侵入式链表通过ILIST_INTERFACE在结构体内嵌入接口m-i-shared.h侵入式共享指针。其它功能头文件m-string.h动态变长字符串Flipper Zero 的FuriString就构建在它之上见下文m-bitset.h位集压缩的 bool 数组m-algo.h为容器生成通用算法find、count、sort、reduce 等m-funcobj.h函数对象m-mempool.h专用快速内存分配器m-worker.h线程池 / 并行任务m-serial-json.h 与 m-serial-bin.h容器的 JSON / 自定义二进制格式导入导出m-core.h基于 C 预处理器的元编程核心被其它所有头文件使用。兼容层为不支持 C11 的编译器提供m-atomic.h在 C 的stdatomic.h与 C 的atomic之间做兼容缺失时提供基于互斥锁的实现m-mutex.h对 C11/PTHREAD/WIN32 三种线程后端提供极薄封装。此外README 还在External Reference章节系统梳理了 C 语言泛型容器库的几大流派void* 回调、宏访问结构体、侵入式结构、多阶段包含头文件、上下文相关宏生成并明确指出M*LIB 主要属于最后一类宏生成上下文相关的 C 代码同时可选地提供少量通用宏和侵入式容器。其相比同类库最大的增量价值就是OPLIST 特性——它让容器可以层层嵌套、并可在容器中使用复杂类型list of array of dictionary of C objects都能被 M*LIB 完美支持。三分钟上手从 LIST 到 STL 的对照使用 M*LIB 的方式非常统一#include目标头文件 → 用_DEF宏为需要的类型实例化结构体与全部方法 → 像使用普通 C 函数一样调用生成的方法。README 给出的第一个例子是unsigned int 的链表#include stdio.h #include m-list.h LIST_DEF(list_uint, unsigned int) /* Define struct list_uint_t and its methods */ int main(void) { list_uint_t list ; /* list_uint_t has been define above */ list_uint_init(list); /* All type needs to be initialized */ list_uint_push_back(list, 42); /* Push 42 in the list */ list_uint_push_back(list, 17); /* Push 17 in the list */ list_uint_it_t it; /* Define an iterator to scan each one */ for(list_uint_it(it, list) /* Start iterator on first element */ ; !list_uint_end_p(it) /* Until the end is not reached */ ; list_uint_next(it)) { /* Set the iterator to the next element*/ printf(%d\n, /* Get a reference to the underlying */ *list_uint_cref(it)); /* data and print it */ } list_uint_clear(list); /* Clear all the list */ }编译时记得加-stdc99或c11。注意第LIST_DEF行没有分号——这是全程序中唯一的宏它会展开成正确的链表类型list_uint_t、迭代器类型list_uint_it_t以及操作它们所需的全部内联函数它可以被多次调用定义任意多个不同名字的链表。宏的三个参数分别是前缀名所有生成类型/函数的前缀、容器内嵌类型任意 C 类型、以及可选的 oplist见下一节。LIST_DEF的三个关键点生成的类型定义很特殊list_uint_t内部实现为大小为 1 的结构体数组。这带来三个好处——声明变量即完成空间预留、函数调用时自动按引用传递、无法通过赋值语句复制变量必须走 API。可无缝切换容器把LIST_DEF换成ARRAY_DEF下面的代码几乎不用改因为 list 与 array 生成的接口非常相似且两种变体的性能都与手写代码相当。与 C STL 逐行对照README 给出了等价的std::listunsigned int程序。区别在于M*LIB 需要显式定义容器实例方法名更长需要显式构造与销毁这正是纯 C 的惯例不返回值而是返回指向数据的指针——这是为了性能避免在栈上拷贝整个数据和通用性某些结构不允许拷贝。M_LET 与 M_EACH语法糖版写法如果你不排斥语法宏可以用M_LET定义并初始化变量离开作用域自动 clear与M_EACH遍历容器把代码压缩到很短#include stdio.h #include m-list.h LIST_DEF(list_uint, unsigned int) /* Define struct list_uint_t and its methods */ int main(void) { M_LET(list, LIST_OPLIST(uint)) { /* Define init list as list_uint_t */ list_uint_push_back(list, 42); /* Push 42 in the list */ list_uint_push_back(list, 17); /* Push 17 in the list */ for M_EACH(item, list, LIST_OPLIST(uint)) { printf(%d\n, *item); /* Print the item */ } } /* Clear of list will be done now */ }注意M_LET/M_EACH在使用上有约束一行内最多出现一个M_LET的代码块内不能使用return、goto、longjmp或 exit 类函数跳出作用域否则析构代码不会执行但允许用break退出块clear 仍会执行也支持链式嵌套创建多个变量。复杂类型如何让容器认识你的类型如果容器内的类型需要特殊的初始化/拷贝/析构README 用 GMP 大数mpz_t举例必须把该类型的oplist传给定义宏#include stdio.h #include gmp.h #include m-array.h ARRAY_DEF(array_mpz, mpz_t, (INIT(mpz_init), INIT_SET(mpz_init_set), SET(mpz_set), CLEAR(mpz_clear)) ) int main(void) { array_mpz_t array ; array_mpz_init(array); mpz_t z; mpz_init(z); mpz_set_ui (z, 42); array_mpz_push_back(array, z); mpz_set_ui (z, 17); array_mpz_push_back(array, z); array_it_mpz_t it; for(array_mpz_it(it, array) ; !array_mpz_end_p(it) ; array_mpz_next(it)) { gmp_printf(%Zd\n, *array_mpz_cref(it)); } mpz_clear(z); array_mpz_clear(array); }由于mpz_t需要正确的初始化、拷贝和销毁函数我们通过 oplist 告诉容器INIT用mpz_init、CLEAR用mpz_clear、SET用mpz_set、INIT_SET用mpz_init_set。容器有两条途径获知类型的 oplist每次定义容器以及LET/EACH时显式传入全局注册定义一个以M_OPL_开头、以类型名结尾的宏注意带括号例如#define M_OPL_mpz_t() M_CLASSIC_OPLIST(mpz)。定义容器与LET/EACH的宏会先检测这类宏是否存在存在则使用否则回退到默认方法。全局注册方式还能与容器自身的XXX_OPLIST宏组合实现递归容器。README 给出了一个从文本文件读取 section 定义的程序用TUPLE_DEF2ARRAY_DEFDICT_DEF2构建符号表 → 符号数组#include stdio.h #include m-array.h #include m-tuple.h #include m-dict.h #include m-string.h TUPLE_DEF2(symbol, (offset, long), (value, long)) #define M_OPL_symbol_t() TUPLE_OPLIST(symbol, M_DEFAULT_OPLIST, M_DEFAULT_OPLIST) ARRAY_DEF(array_symbol, symbol_t) #define M_OPL_array_symbol_t() ARRAY_OPLIST(array_symbol, M_OPL_symbol_t()) DICT_DEF2(sections, string_t, array_symbol_t) #define M_OPL_sections_t() DICT_OPLIST(sections, STRING_OPLIST, M_OPL_array_symbol_t()) int main(int argc, const char *argv[]) { if (argc 2) abort(); FILE *f fopen(argv[1], rt); if (!f) abort(); M_LET(sc, sections_t) { sections_in_str(sc, f); array_symbol_t *a sections_get(sc, STRING_CTE(.text)); if (a NULL) { printf(There is no .text section.); } else { printf(Section .text is :); array_symbol_out_str(stdout, *a); printf(\n); } } return 0; }每个容器都提供定义自身 oplist的宏如ARRAY_OPLIST、TUPLE_OPLIST、DICT_OPLIST这正是容器能够递归嵌套的机制。该程序甚至直接调用了_in_str/_out_str——因为底层类型都提供了 I/O 操作符字符串/数组/字典的格式化读写方法会自动生成。OPLISTM*LIB 的灵魂机制OPLIST 是 M*LIB独创的核心概念README 明确说任何其它库都没用过。C 编译器不像 C 那样知道如何处理任意类型所以 M*LIB 通过给需要处理该类型的宏提供一个操作符列表operator list来补充信息。从根本上说oplist 就是一个类型对外暴露的接口它用纯 C 预处理器实现的关联数组来表达——预定义的操作符是键方法函数是值格式为(OPERATOR1(method1), OPERATOR2(method2), ...)几个关键规则列表中操作符的顺序即优先级同一个操作符出现多次时左边者优先这实现了部分覆盖一个方法必须是不含逗号的预处理器表达式方法名后面可以跟M_IPTR标记如(INIT(init_func M_IPTR))表示函数的第一个参数是指向类型的指针而非类型本身oplist 从 C 语言角度看没有真实形态它只是预处理阶段的抽象在宏展开后消失。对象必须的四件套与常用操作符每个对象/容器通常需要以下四个操作符INIT(obj)把对象初始化为合法状态构造INIT_SET(obj, org)把未初始化的对象初始化为与org相同的状态拷贝构造SET(obj, org)把已初始化的对象设置为与org相同赋值CLEAR(obj)销毁已初始化的对象并释放其内存析构永不失败。INIT、INIT_SET、SET只允许因内存错误而失败。oplist 并不要求写全所有操作符——缺失的操作符会使用对应的默认实现。对于 C 的整数/浮点类型默认构造器完全够用可以直接省略 oplist 或用M_DEFAULT_OPLIST。其余重要操作符完整列表见 README 的 OPLIST 章节包括NAME/TYPE/SUBTYPE/OPLIST元信息、NEW/DEL/REALLOC/FREE分配与释放默认走M_MEMORY_ALLOC等宏、INC_ALLOC数组扩容策略默认取max(2*s, 16)、INIT_MOVE/MOVE资源窃取式移动默认假定对象平凡可移动、INIT_WITH用一组异构参数构造、SWAP、RESET、EMPTY_P、GET_SIZE、HASH、EQUAL、CMP全序比较、ADD/SUB/MUL/DIV、GET_KEY/SET_KEY/SAFE_GET_KEY/ERASE_KEY关联容器、PUSH/POP/PUSH_MOVE/POP_MOVE、迭代器系列IT_FIRST/IT_LAST/IT_END/IT_SET/IT_END_P/IT_LAST_P/IT_EQUAL_P/IT_NEXT/IT_PREVIOUS/IT_CREF/IT_REF/IT_INSERT/IT_REMOVE、拼接系列SPLICE_BACK/SPLICE_AT、I/O 系列OUT_STR/IN_STR/GET_STR/PARSE_STR、序列化OUT_SERIAL/IN_SERIAL、开放寻址哈希表需要的OOR_SET/OOR_EQUAL把整数信息存入未初始化对象、用于标记空位、REVERSE等。所有操作符名不得被用户定义为宏。API 类型变换与操作符的三种状态oplist 内还可以指定方法调用时的底层参数变换方式API type。假设方法名为method、操作符的第一个参数名为output则API_0method(output, ...)默认API_1method(oplist, output, ...)把 oplist 传给方法API_2method(output, ...)第一个参数按地址传递等价于M_IPTRAPI_3method(oplist, output, ...)API_4output method(...)第一个参数按返回值传递API_5output method(oplist, ...)API_6method(output, ...)前两个参数按地址传递API_7method(oplist, output, ...)。每个操作符OP还有三种定义状态(OP(f))表示以f为方法()表示省略该操作符、使用全局默认(OP(0))表示禁用该操作符此后绝不可用。该用哪个 OPLISTREADME 按类型给出了速查表这也是最容易踩坑的地方C 布尔M_BOOL_OPLISTC 整数 / 浮点M_DEFAULT_OPLIST也可省略C 枚举M_ENUM_OPLIST指向某物的指针容器不管理所指对象M_PTR_OPLIST可用 memset/memcpy/memcmp 完成初始化/拷贝/比较的普通结构体M_POD_OPLIST用[1]技巧按引用传递、且可 mem* 处理的普通结构体M_A1_OPLIST提供name_init、name_init_set、name_set、name_clear方法的类型M_CLASSIC_OPLIST常量字符串const char *不释放、不移动M_CSTR_OPLISTM*LIB 的string_tSTRING_OPLISTM*LIB 的容器对应容器的XXX_OPLIST其它情况需要自定义 oplist。注意oplist 导出的具体方法集合取决于 C 语言版本。C11 模式下M_DEFAULT_OPLIST能用_Generic导出 int/float 的通用 I/O 方法C99 下则不行——这也是 JSON 导入导出仅在 C11 模式可用的原因。全局注册与常见编译错误只有名字中不含空格的类型才能全局注册 oplist可用 typedef 解决。全局注册能显著简化 oplist 的使用。当 oplist 用错时M*LIB 通过静态断言产生如下诊断错误详见 README 的 ERRORS COMPILERS 章节M_LIB_NOT_AN_OPLIST给定的东西直接或间接不能归约为合法的 oplistM_LIB_ERROR(ARGUMENT_OF_*_OPLIST_IS_NOT_AN_OPLIST, ...)上一错误的子错误定位到某个*_OPLIST宏的构造根因M_LIB_MISSING_METHOD某个必需操作符没有定义方法M_LIB_TYPE_MISTMACH给定的 oplist 与类型不匹配M_LIB_NOT_A_DEFAULT_TYPEM_DEFAULT_OPLIST被用于一个非默认类型。典型诱因包括漏包含头文件用宏局部覆盖了操作符名如NEW、DEL、INIT、OPLIST缺少括号或括号双重嵌套名字写错如DEFAULT_OPLIST少了M_前缀oplist 的名字与定义方法所用的名字不一致在 OPLIST 定义里误用了类型而非 oplistOPLIST 定义中缺少子 oplist。调试时可以用cc -stdc99 -E test-file.c生成预处理文件再用-Wall编译它来定位问题。在生成的代码里出现编译器警告几乎必然意味着有一个必须修复的错误shadowed variables 除外。内存分配与内存耗尽策略M*LIB 内部使用malloc/realloc/free管理内存池可在不同层级覆写。全局层可以在使用任何_DEF宏之前覆写以下四个宏M_MEMORY_ALLOC(type)返回一个type新对象的指针M_MEMORY_DEL(ptr)释放ALLOC分配的单对象M_MEMORY_REALLOC(type, ptr, number)把旧数组重分配为number个对象ptr可为 NULL此时是新建M_MEMORY_FREE(ptr)释放REALLOC分配的数组。注意ALLOC/DEL 与 REALLOC/FREE 两对指针不能混用——分离的 free 操作符是为了让分配器可以根据单对象 / 数组的提示做专门优化。默认实现分别是malloc、free、realloc、freeALLOC/REALLOC在失败时应返回 NULL。此外也可以在给容器传入的 oplist 中覆写NEW、DEL、REALLOC、FREE从而只影响这一个容器。内存耗尽OOM策略当分配失败时M*LIB 会调用全局宏M_MEMORY_FULL随后函数立即返回此时对象保持之前合法的合法且不变的状态。默认行为是打印错误信息并abort程序——README 认为测试 OOM 很困难但又是避免安全漏洞所必需的因此默认策略相当保守。该宏可被覆写为其它策略抛异常、设置全局错误标志等它接收一个size_t参数表示尝试分配的内存字节数必须在实例化结构体之前定义。README 同时提醒抛异常策略尚未完全支持库需要协助清理被跳过的对象见上游 issue #15。M*LIB 在 Flipper Zero 固件中的落地从 FuriString 到 ViewDictM*LIB 不是只停留在文档里的概念它在 flipperzero-firmware 中是被大规模使用的底层基础设施。最直观的证据在furi 核心层furi/core/string.c 直接#include m-string.h并用它实现了固件主打的字符串类型#include string.h #include m-string.h struct FuriString { string_t string; };也就是说FuriString内部就是一个 M*LIB 的string_t固件上层大量的furi_string_*API 都是对string_init、string_set、string_clear等 M*LIB 方法的封装furi/core/string.h 中定义了完整的封装面。因此凡是你在应用层看到的FuriString*其容量管理、追加、查找、格式化等语义都直接继承自 M*LIB 的动态字符串实现。再看GUI 的 ViewDispatcherapplications/services/gui/view_dispatcher_i.h 中用m-dict.h定义了一个view_id → View*的字典#include m-dict.h DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST) // NOLINT这里正好演示了 oplist 的典型用法key 是uint32_t使用M_DEFAULT_OPLIST默认类型value 是View*指针使用M_PTR_OPLIST容器不管理指针所指对象。随后 applications/services/gui/view_dispatcher.c 中ViewDict_init、ViewDict_clear、ViewDict_get、ViewDict_set_at、ViewDict_erase等生成的方法构成了视图注册/查找的完整生命周期如第 159 行ViewDict_set_at(view_dispatcher-views, view_id, view)注册视图、第 188 行ViewDict_erase(...)注销视图。固件中的其它容器使用同样频繁ARRAY_DEF在 applications/services/gui/modules/submenu.c 附近用于子菜单项数组配API_2/API_6地址传递变换配合INIT/SET/INIT_SET/CLEAR四个对象操作符m-dict.h还出现在 applications/services/storage/storage_processing.c、applications/services/rpc/rpc.c、applications/main/infrared/infrared_cli.c 等处m-bptree.h/m-rbtree.h也被 applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.c 与 furi/core/event_loop_i.h 使用。从这些调用点可以推断M*LIB 在固件中同时承担了通用动态数组/字典和字符串实现基础两个角色是上层大量 C 代码得以安全、类型化地管理内存的基础。参考实现与测试官方示例仓库 lib/mlib/example 下有ex-array00.c、ex-list01.c、ex-dict01.c、ex-string01.c、ex11-json01.c、ex11-section.c等 30 余个可编译示例README 明确建议不要靠读源码学用法而是读示例与测试测试套件lib/mlib/tests 内含test-marray.c、test-mlist.c、test-mdict.c、test-mcore.c、test-malgo.c等单元测试以及fail-no-oplist.c、fail-incompatible.c等专门验证错误 oplist 能否被正确拒绝的负向用例构建目标lib/mlib/Makefile 提供make check先进入 tests 执行测试再编译 example 目录、make doc生成 HTML 文档与依赖图、make install PREFIX...安装头文件等目标。构建、安装与发布建议M*LIB 纯头文件、无构建、无链接依赖因此安装就是把头文件放进编译器搜索路径make install PREFIX/my/directory/where/to/install运行测试套件与生成文档make check make doc对嵌入式场景尤为重要的是 README 的发布建议M*LIB 的实现中包含大量断言以保证安全正式发布的程序强烈建议正确定义NDEBUG。另外内存策略宏如M_MEMORY_ALLOC、M_MEMORY_FULL、M_USE_*系列与全局自定义宏如M_USE_HASH_SEED默认值为 0 即哈希可预测可注入随机种子以缓解针对哈希表的 DoS 攻击都必须在包含任何 M*LIB 头文件之前定义。常用的全局定制宏包括M_USE_STDIO/M_USE_STDARG是否引入 stdio/stdarg 相关函数、M_USE_THREAD_BACKEND1C11、2WINDOWS、3PTHREAD默认自动检测、M_USE_SERIAL_MAX_DATA_SIZE序列化对象私有数据大小默认 4、M_USE_MEMPOOL_MAX_PER_SEGMENTmempool 每段元素数默认适配 16KB 页、M_USE_DEQUE_DEFAULT_SIZEdeque 每段默认 8 个元素、M_USE_CSTR_ALLOCM_CSTR临时字符串容量默认 256等完整清单见 README 末尾的 Global User Customization 章节。许可证M*LIB 以BSD-2 simplified license分发版权归 Patrick Pelissier2017-2021完整的 BSD 两条款许可文本见 lib/mlib/LICENSE 以及 README 末尾的 License 章节——这也是它可以被 flipperzero-firmware 这样的开源固件直接内置的前提之一。结语M*LIB 用宏生成上下文相关代码 OPLIST 接口注入的方式在纯 C 语言中复刻了 STL 的容器体验类型安全、零运行时开销、支持构造/析构语义与递归嵌套容器。对 Flipper Zero 开发者而言理解 M*LIB 就等于理解了固件字符串系统与大量 UI/存储/RPC 模块中容器代码的底层运行机制——当你看到FuriString的自动扩容、看到ViewDispatcher里的ViewDict、看到submenu.c里那段ARRAY_DEF时背后运行的正是本文所讲的这套 OPLIST 与_DEF宏体系。上手的最好方式正如 README 所建议的读 lib/mlib/example 的示例跑一遍 lib/mlib/tests 的测试然后在自己的模块里用ARRAY_DEF/DICT_DEF2替换手写的链表与哈希表。【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考