Matter(connectedhomeip)项目内置 INI 解析器 IniPP 使用指南:头文件式解析、生成与插值详解
2026/9/19 11:57:33 网站建设 项目流程

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支持charwchar_t,在 Windows 上可原生处理 Unicode;
  • 默认节支持:类似 PythonconfigparserDEFAULT节语义;
  • 插值支持:支持${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 的完整工作流:

  1. parse()从任意std::basic_istream<CharT>读取并解析;
  2. generate()把内部数据结构重新输出为规范化文本(=两侧空格被去除、节与键按字典序排序);
  3. default_section()DEFAULT节中的变量注入其它所有节;
  4. interpolate()递归替换${...}占位符;
  5. 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(); // ... };

值得注意的实现细节:

  • sectionsstd::map,因此节和键在生成时按字典序输出,这也是generate的输出顺序与手写输入顺序可能不一致的原因;
  • errors是一个std::list<String>parse不会因遇到坏行而抛出异常,而是把整行原文记录进errors,由调用方决定如何处理——这一点使 IniPP 在嵌入式等健壮性优先的场合更安全;
  • 语法相关的字符常量全部定义为静态常量:[]=;${:},以及插值最大递归深度max_interpolation_depth = 10(见 inipp.h#L103-L112)。

三、解析算法逐行拆解

README 用伪代码形式给出了解析算法,源码实现位于Ini::parse()(见 inipp.h#L124-L159)。两者结合,完整的规则如下:

  1. 初始时,当前section被设为空字符串""
  2. 逐行读取,并先做左右空白裁剪(detail::ltrim/detail::rtrim,见 inipp.h#L43-L55);
    • 若行为空,或;开头,则该行被忽略(注释行);
    • 若行以[开头,则section切换为[]之间的字符串;若行尾不是],该行被记为错误(例如[badsec);
    • 否则,若行中含有=号,则=之前为variable、之后为value,二者分别做rtrim/ltrim裁剪;如果该变量在本节内已被赋值,该行被记为错误(重复键报错);
    • 其余情况(既不是注释、不是节、也没有=的行),该行被记为错误;
  3. 解析完成后,错误行累积在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 不覆盖已有键 }

由于底层Sectionstd::map<String, String>insert的语义天然保证:目标节中已存在的同名变量不会被默认节覆盖,而缺失的变量会被补入。这与 PythonconfigparserDEFAULT节的行为一致。

单元测试 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 来自默认节

测试结论:sec1a取自默认节(值为0),而b保留自身定义(值为2);sec2a保留自身值3b来自默认节(值为1)——完美印证“补缺不覆盖”的语义。

五、插值算法:${variable} 与 ${section:variable}

插值是 IniPP 最强大的能力。README 给出的算法分三步:

  1. 局部符号归一化:在每个节内部,把${variable}全部改写成${section:variable}
  2. 全局替换:把每个${section:variable}替换为其值;
  3. 迭代收敛:重复第 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 ffv1encode0被展开为完整的 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.cppLinux 平台 KVS 配置的 INI 文件读写
src/platform/NuttX/CHIPLinuxStorageIni.cppNuttX 平台复用同一套 INI 存储实现
src/platform/webos/CHIPWebOSStorageIni.cppwebOS 平台 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 时建议留意以下行为:

  1. 注释仅支持;开头,且必须在行首(前导空白会被先裁剪)。行内注释(如a = 1 ; comment)不被支持,; comment会被当作值的一部分;
  2. 重复键不报异常,只记录错误行:同节重复赋值时,先出现的值生效,后出现的行被追加到ini.errors(inipp.h#L148-L152)。errors中保存的是原始行,不区分错误类型;
  3. 节名后的内容不会校验:如[badsec] trailing这类行会被接受(源码只检查首字符与尾字符),节名也允许为空([]会把变量放入空名节,见 test4.ini 与 test4.output);
  4. 变量名前导空白不会被裁剪:只有=前的变量名做rtrimvar = 1的变量名会带前导空格,建议书写时保持一致格式;
  5. 插值深度上限为 10:循环引用不会崩溃,但未解析完的${...}会原样保留在值里(参见 test3.output 中未定义的preset4~preset9相关项);
  6. generate输出是规范化/排序后的std::map迭代使节和键按字典序输出,且=两侧空白被移除,因此generate结果不等于原始文件字节;
  7. 布尔值提取要写true/false,因为extract使用std::boolalpha
  8. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询