Matter(connectedhomeip)项目内置 INI 解析器 IniPP 使用指南:头文件式解析、生成与插值详解
【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip
导读
IniPP 是一个仅由单个头文件构成的轻量级 C++ INI 文件解析与生成库,支持解析、重新生成、默认节合并以及${variable}插值替换,被 Matter(connectedhomeip)项目以三方库形式内置于 third_party/inipp 中,用于 Linux、NuttX、webOS 等平台的持久化存储读写。本文以 third_party/inipp/repo/inipp/README.md 为主线,结合仓库内的头文件实现、示例工程与单元测试,系统讲解 IniPP 的解析算法、默认节算法、插值算法、类型提取函数,以及它在 Matter 源码中的真实调用场景,读完即可在自己的 C++ 工程中直接使用,也能理解 Matter 配置存储模块的底层机制。
一、IniPP 是什么:单头文件、零依赖的 INI 解析与生成库
IniPP(inipp是 "INI Plus Plus" 的缩写)是一个**纯头文件(header-only)**的 C++ INI 解析器和生成器,源码作者为 Matthias C. M. Troffaes。整个实现集中在 third_party/inipp/repo/inipp/inipp/inipp.h 这一个文件中,核心类inipp::Ini<CharT>只有约 130 行逻辑代码,设计上强调简单、可移植与宽松授权。
从 README 声明的功能特性 看,它提供:
- Header-only:只需
#include "inipp.h",无需链接任何库; - 既能解析也能生成:
parse()读取配置、generate()输出规范化配置; - 宽字符支持:通过模板参数
CharT支持char与wchar_t,在 Windows 上可原生处理 Unicode; - 默认节支持:类似 Python
configparser的DEFAULT节语义; - 插值支持:支持
${variable}/${section:variable}形式的变量替换,同样参考 Pythonconfigparser; - 简单设计与实现:核心算法清晰、可读;
- MIT 宽松许可证:允许自由使用、修改与再分发(见 third_party/inipp/repo/inipp/LICENSE.txt)。
在 connectedhomeip 中的位置
在 Matter 仓库中,IniPP 被 vendored 进third_party/inipp,并提供了两条构建接入途径:
- GN 构建接入见 third_party/inipp/BUILD.gn:定义
source_set("inipp"),源文件仅为repo/inipp/inipp/inipp.h,同时通过config("inipp_config")把repo/inipp加入头文件搜索路径; - CMake 构建接入见 third_party/inipp/repo/inipp/CMakeLists.txt:
include_directories("inipp/"),install(FILES inipp/inipp.h DESTINATION include)把头文件安装到 include 目录,并启用测试子目录unittest/。
二、快速上手:解析、生成、默认节与插值
README 提供的官方示例位于 third_party/inipp/repo/inipp/example/example.cpp,对应输入文件是 third_party/inipp/repo/inipp/example/example.ini。完整代码及注释如下:
#include <fstream> #include "inipp.h" int main() { inipp::Ini<char> ini; std::ifstream is("example.ini"); ini.parse(is); // 1. 从流解析 INI std::cout << "raw ini file:" << std::endl; ini.generate(std::cout); // 2. 规范化输出 ini.default_section(ini.sections["DEFAULT"]); // 3. 应用默认节 ini.interpolate(); // 4. 执行变量插值 std::cout << "ini file after default section and interpolation:" << std::endl; ini.generate(std::cout); int compression_level = -1; inipp::extract(ini.sections["bitbucket.org"]["CompressionLevel"], compression_level); // 5. 类型提取 std::cout << "bitbucket.org compression level: " << compression_level << std::endl; return 0; }输入文件example.ini:
[DEFAULT] ServerAliveInterval = 45 Compression = yes CompressionLevel = 9 ForwardX11 = yes [bitbucket.org] User = hg [topsecret.server.com] Port = 50022 ForwardX11 = no这个示例展示了 IniPP 的完整工作流:
parse()从任意std::basic_istream<CharT>读取并解析;generate()把内部数据结构重新输出为规范化文本(=两侧空格被去除、节与键按字典序排序);default_section()将DEFAULT节中的变量注入其它所有节;interpolate()递归替换${...}占位符;extract()把字符串值安全转换为int等目标类型,转换失败时返回false并保持原值。
核心数据结构
从 inipp.h 源码 可以看到Ini<CharT>的公开接口与数据结构:
template<class CharT> class Ini { public: typedef std::basic_string<CharT> String; typedef std::map<String, String> Section; // 一个节 = 键值对映射 typedef std::map<String, Section> Sections; // 整个文件 = 节名到节的映射 Sections sections; std::list<String> errors; // 解析错误行收集 void generate(std::basic_ostream<CharT> & os) const; void parse(std::basic_istream<CharT> & is); void interpolate(); void default_section(const Section & sec); void clear(); // ... };值得注意的实现细节:
sections是std::map,因此节和键在生成时按字典序输出,这也是generate的输出顺序与手写输入顺序可能不一致的原因;errors是一个std::list<String>,parse不会因遇到坏行而抛出异常,而是把整行原文记录进errors,由调用方决定如何处理——这一点使 IniPP 在嵌入式等健壮性优先的场合更安全;- 语法相关的字符常量全部定义为静态常量:
[、]、=、;、$、{、:、},以及插值最大递归深度max_interpolation_depth = 10(见 inipp.h#L103-L112)。
三、解析算法逐行拆解
README 用伪代码形式给出了解析算法,源码实现位于Ini::parse()(见 inipp.h#L124-L159)。两者结合,完整的规则如下:
- 初始时,当前section被设为空字符串
""; - 逐行读取,并先做左右空白裁剪(
detail::ltrim/detail::rtrim,见 inipp.h#L43-L55);- 若行为空,或以
;开头,则该行被忽略(注释行); - 若行以
[开头,则section切换为[与]之间的字符串;若行尾不是],该行被记为错误(例如[badsec); - 否则,若行中含有
=号,则=之前为variable、之后为value,二者分别做rtrim/ltrim裁剪;如果该变量在本节内已被赋值,该行被记为错误(重复键报错); - 其余情况(既不是注释、不是节、也没有
=的行),该行被记为错误;
- 若行为空,或以
- 解析完成后,错误行累积在
ini.errors中。
代码中还有一个容易被忽略的细节:变量名部分只做rtrim(见 inipp.h#L146),因此变量名首部不会自动去除空白——即var = 1中的变量名会带上前导空格。若行以=开头(pos == 0)或整行没有=,也都会进入错误分支(inipp.h#L143-L156)。
四、默认节算法:把 DEFAULT 注入所有节
README 对默认节算法只有一句话描述:将默认节中的每个变量插入其它所有节,且不覆盖已有变量。其实现Ini::default_section()(见 inipp.h#L176-L180):
void default_section(const Section & sec) { for (auto & sec2 : sections) for (const auto & val : sec) sec2.second.insert(val); // std::map::insert 不覆盖已有键 }由于底层Section是std::map<String, String>,insert的语义天然保证:目标节中已存在的同名变量不会被默认节覆盖,而缺失的变量会被补入。这与 Pythonconfigparser中DEFAULT节的行为一致。
单元测试 TestDefault 验证了这一点:
Ini<char> ini; ini.sections["sec0"]["a"] = "0"; // sec0 作为默认节 ini.sections["sec0"]["b"] = "1"; ini.sections["sec1"]["b"] = "2"; // sec1 自带 b ini.sections["sec1"]["c"] = "${a} ${b}"; ini.sections["sec2"]["a"] = "3"; // sec2 自带 a ini.sections["sec2"]["c"] = "${a} ${b}"; ini.default_section(ini.sections.at("sec0")); ini.interpolate(); Assert::AreEqual(ini.sections.at("sec1").at("c"), std::string("0 2")); // a 来自默认节,b 保留自身值 2 Assert::AreEqual(ini.sections.at("sec2").at("c"), std::string("3 1")); // a 保留自身值 3,b 来自默认节测试结论:sec1的a取自默认节(值为0),而b保留自身定义(值为2);sec2的a保留自身值3,b来自默认节(值为1)——完美印证“补缺不覆盖”的语义。
五、插值算法:${variable} 与 ${section:variable}
插值是 IniPP 最强大的能力。README 给出的算法分三步:
- 局部符号归一化:在每个节内部,把
${variable}全部改写成${section:variable}; - 全局替换:把每个
${section:variable}替换为其值; - 迭代收敛:重复第 2 步,直到没有更多替换发生,或达到最大递归深度(默认 10)。
对应源码Ini::interpolate()(见 inipp.h#L161-L174):
void interpolate() { int global_iteration = 0; auto changed = false; // 第 1 步:把每个节内的 ${variable} 变成 ${section:variable} for (auto & sec : sections) replace_symbols(local_symbols(sec.first, sec.second), sec.second); // 第 2、3 步:反复做全局替换直到无变化或达到深度上限 do { changed = false; const auto syms = global_symbols(); for (auto & sec : sections) changed |= replace_symbols(syms, sec.second); } while (changed && (max_interpolation_depth > global_iteration++)); }插值的作用域与惰性替换语义
这套两阶段设计解决了一个经典问题:未定义变量的引用不会被错误替换。由于第 1 步先把局部引用改为带节前缀的全局形式,而全局符号表来自替换循环开始时global_symbols()的快照,因此在一次迭代中,若某个节尚未定义某变量,${section:variable}会保持原样直到下一次迭代(或永远保留)。
单元测试 TestInterpolate1 专门验证了这个惰性语义:
ini.sections["sec1"]["x"] = "${sec2:z}"; ini.sections["sec1"]["y"] = "2"; ini.sections["sec2"]["z"] = "${y}"; ini.interpolate(); // 期望:x 保持 "${y}",而不是被替换成 "2" Assert::AreEqual(ini.sections.at("sec1").at("x"), std::string("${y}"));即sec2:z = ${y}中的y只在其所属节sec2内解析(第 1 步会把它改成${sec2:y}),并不会去引用sec1里的y——插值永远是按节隔离的,除非显式写成带节前缀的形式。
循环引用与递归深度保护
对于相互引用的配置(如x = 0 ${y}与y = 1 ${x}),插值不会死循环,而是受max_interpolation_depth = 10限制(inipp.h#L112)。TestInfiniteRecursion1/2/3 构造了三种循环引用场景(同节互引、跨节互引、多节长链循环),验证解析器在 10 轮迭代后安全停止,不会栈溢出或挂死。
真实插值效果:test3 用例
最能体现插值能力的测试用例是 test3.ini,它模拟了一个 ffmpeg 批量转码脚本的生成场景——所有命令行片段都由基础变量拼装而成,例如:
[export] base = ${folder}\export-${builtin:timestamp} video = ${base}-raw-video.yuv [batch] ffmpeg = "${export:folder}\ffmpeg.exe" -y codecs0 = ${presets:${preset0}_filter} ${presets:${preset0}_audiocodec} ${presets:${preset0}_videocodec}注意这里还出现了嵌套插值${presets:${preset0}_filter}:先解析内层${preset0},再用其结果拼出新的引用键。对照 test3.output 中>>> INTERPOLATE <<<之后的输出,可以看到codecs0最终被展开为-c:a flac -c:v ffv1,encode0被展开为完整的 ffmpeg 命令行。这个用例直观说明:IniPP 的插值足以支撑“模板化配置 + 批量生成复杂命令”这类实际工程需求。
六、extract:把字符串安全转换为目标类型
INI 中所有值本质上都是字符串。IniPP 提供自由函数inipp::extract()将字符串转换为目标类型(见 inipp.h#L73-L90):
template <typename CharT, typename T> inline bool extract(const std::basic_string<CharT> & value, T & dst) { CharT c; std::basic_istringstream<CharT> is{ value }; T result; if ((is >> std::boolalpha >> result) && !(is >> c)) { // 必须整个字符串都被消费完 dst = result; return true; } return false; } // std::basic_string 的特化:直接拷贝,始终成功 template <typename CharT> inline bool extract(const std::basic_string<CharT> & value, std::basic_string<CharT> & dst) { dst = value; return true; }两个设计要点:
- 使用
std::boolalpha,因此布尔值应写作true/false而不是1/0; - 通过“读取成功后必须到达流末尾(
!(is >> c))”来确保整串完整消费:像"xxx"、"1000000"(对int16_t溢出)、"-20 xxx"这类不完整或溢出的字符串都会返回false,且目标变量保持不变。
TestExtract 系统验证了这些边界情形:extract("hello world", str)成功、extract("false", bool_)成功、extract("xxx", i16)失败、extract("1000000", i16)因溢出失败、extract("-20 xxx", i16)因尾随内容失败、extract("1000000", i32)成功。
七、在 Matter 仓库中的真实应用:持久化配置存储
IniPP 并非孤立存在,它承担着 Matter 多个平台持久化配置存储(Persistent Storage)的底层职责。通过全仓库检索(grep -rl "inipp" src examples)可以确认以下调用点:
| 调用位置 | 用途 |
|---|---|
| src/platform/Linux/CHIPLinuxStorageIni.cpp | Linux 平台 KVS 配置的 INI 文件读写 |
| src/platform/NuttX/CHIPLinuxStorageIni.cpp | NuttX 平台复用同一套 INI 存储实现 |
| src/platform/webos/CHIPWebOSStorageIni.cpp | webOS 平台 INI 存储 |
| src/controller/ExamplePersistentStorage.cpp | 控制器示例的持久化存储 |
| examples/chip-tool/BUILD.gn、examples/fabric-admin/BUILD.gn 等 | 各示例在 GN 构建中依赖inipptarget |
以 Linux 实现为例(CHIPLinuxStorageIni.cpp),它把整个配置保存进inipp::Ini<char>对象的sections,读写路径与 Inipp 的算法严格对应:
- 读取时先查找
sections.find("DEFAULT")作为默认节(CHIPLinuxStorageIni.cpp#L54-L58); - 取键值统一通过
inipp::extract(section[escapedKey], value)完成字符串到目标类型的转换(CHIPLinuxStorageIni.cpp#L128、#L156、#L184、#L213、#L264); - 写键值则统一写入
sections["DEFAULT"]节(CHIPLinuxStorageIni.cpp#L348、#L364)。
也就是说,Matter 在 Linux/NuttX 上把“DEFAULT 节”当作唯一的扁平键值存储区,并在读写两侧复用了 IniPP 的解析、生成与 extract 设施。这一真实集成证明 IniPP 已经过生产级项目验证,而不仅仅是教学示例。
八、构建与测试:如何在自己工程中接入
方式一:GN(与 Matter 一致)
Matter 通过source_set接入(third_party/inipp/BUILD.gn)。你自己的 GN 工程可仿照:
config("inipp_config") { include_dirs = [ "third_party/inipp/repo/inipp" ] } source_set("inipp") { sources = [ "third_party/inipp/repo/inipp/inipp/inipp.h" ] public_configs = [ ":inipp_config" ] }方式二:CMake
上游 CMake 脚本(third_party/inipp/repo/inipp/CMakeLists.txt)演示了最简接入:加入头文件目录、把inipp.h安装到 include 目录,并启用unittest子目录运行测试:
cmake_minimum_required(VERSION 3.20) project(inipp) include_directories("inipp/") install(FILES inipp/inipp.h DESTINATION include) enable_testing() add_subdirectory("unittest/")方式三:最朴素——直接拷贝头文件
由于它是单头文件库,最简单的使用方式就是复制 inipp/inipp.h 到你的 include 目录,然后#include "inipp.h"即可,无需任何构建系统改动。
运行官方测试
仓库自带完整的单元测试工程:
- 测试驱动与断言封装见 unittest/test.h;
- 全部测试用例见 unittest/unittest.cpp,覆盖:解析+生成往返(
TestParseGenerate1~4,其中1W为宽字符变体)、插值语义、三种循环引用、extract边界、默认节语义; - 每个用例配套
.ini输入与.output期望输出(如 test1.ini 与 test1.output),输出格式统一为>>> ERRORS <<</>>> GENERATE <<</>>> INTERPOLATE <<<三段,便于比对; - unittest/headertest.cpp 专门验证头文件可被独立编译(编译通过即视为通过),确保单头文件属性不破坏。
在非 MSVC 环境下,unittest.cpp自带简易Assert/Logger适配层(见 unittest.cpp#L5-L38),因此可以直接用任意 C++11 编译器编译运行。
九、边界行为与易踩的坑(综合源码与测试)
结合源码与测试,使用 IniPP 时建议留意以下行为:
- 注释仅支持
;开头,且必须在行首(前导空白会被先裁剪)。行内注释(如a = 1 ; comment)不被支持,; comment会被当作值的一部分; - 重复键不报异常,只记录错误行:同节重复赋值时,先出现的值生效,后出现的行被追加到
ini.errors(inipp.h#L148-L152)。errors中保存的是原始行,不区分错误类型; - 节名后的内容不会校验:如
[badsec] trailing这类行会被接受(源码只检查首字符与尾字符),节名也允许为空([]会把变量放入空名节,见 test4.ini 与 test4.output); - 变量名前导空白不会被裁剪:只有
=前的变量名做rtrim,var = 1的变量名会带前导空格,建议书写时保持一致格式; - 插值深度上限为 10:循环引用不会崩溃,但未解析完的
${...}会原样保留在值里(参见 test3.output 中未定义的preset4~preset9相关项); generate输出是规范化/排序后的:std::map迭代使节和键按字典序输出,且=两侧空白被移除,因此generate结果不等于原始文件字节;- 布尔值提取要写
true/false,因为extract使用std::boolalpha; extract失败时目标变量保持不变:适用于带默认值初始化的场景,如官方示例中int compression_level = -1;失败时仍为-1。
十、小结
IniPP 以“一个头文件”实现了 INI 解析、生成、默认节合并与变量插值的完整闭环,算法在 inipp.h 中清晰可读,行为由 unittest 全面锁定,并且已在 Matter 的 Linux/NuttX/webOS 持久化存储(CHIPLinuxStorageIni.cpp、CHIPWebOSStorageIni.cpp)与 chip-tool、fabric-admin 等示例中落地使用。对于需要轻量配置管理、模板化文本生成,或希望理解 Matter 配置存储底层的开发者,它都是一个低接入成本、行为可预期的高质量选择。
【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考