在嵌入式开发中,配置文件的读写几乎无处不在。而 INI 格式因其简单明了,至今仍被大量项目采用。但在资源受限的嵌入式平台上,想找一个真正轻量、可移植、且没有动态内存分配的 INI 解析库并不容易。
今天要聊的 minIni 就是为这个场景设计的——整个实现不到 1000 行 C 代码,完全不依赖 malloc,并且能适配任意文件系统。
一、minIni 是什么
minIni 是由 CompuPhase 开发的一个开源 INI 文件解析库。它的几个核心特点值得关注:
- 体积极小:
minIni.c 本身仅约 967 行带注释的 C 代码。
- 零动态内存:所有缓冲区都在栈上分配,内存占用完全是可预测的。
- 不依赖标准 C 库的文件 I/O:通过一个“glue 文件”抽象层,可以适配任何文件系统。
- C 和 C++ 双支持:提供直接的 C 函数 API,同时也附带一个 C++ 封装类。
- Apache 2.0 许可:对商业应用非常友好。
一个典型的 INI 文件内容如下:
[Network]
hostname=MyBoard
address=192.168.1.100
port=8080
[Sensor]
name=temperature
interval=5000
unit=Celsius
minIni 不仅支持标准的节 (Section) 格式,也能处理没有 [Section] 的“无分节”配置文件,并且冒号分隔符(Key: Value)和等号(Key=Value)是等价的。
二、架构设计
minIni 的架构可以分三层来理解:
┌─────────────────────────────────────────┐
│ 应用层 │
│ C: 直接调用 ini_gets/ini_puts 等 │
│ C++: 使用 minIni 类 │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ minIni.c / minIni.h │
│ INI 格式解析引擎(读取、写入、枚举、删除)│
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ minGlue.h(Glue 层) │
│ 将文件 I/O 操作映射到底层文件系统 │
│ 提供 7 种现成 glue 文件: │
│ stdio / Linux+flock / FatFs / EFSL / │
│ Microchip MDD / CCS / IBEX FFS │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ 底层文件系统 │
│ stdio / FatFs / EFSL / 自定义 ... │
└─────────────────────────────────────────┘
Glue 文件构成了 minIni 可移植性的基石。它其实就是一组宏或内联函数,把 minIni 所需的文件操作映射到目标平台的具体实现上。切换平台时,唯一需要做的,就是更换一个 glue 文件,而解析引擎的代码则完全不用动。
这里有一个最小的 stdio glue 文件示例:
#include <stdio.h>
#define INI_FILETYPE FILE*
#define ini_openread(filename,file) ((*(file) = fopen((filename),"r")) != NULL)
#define ini_openwrite(filename,file) ((*(file) = fopen((filename),"w")) != NULL)
#define ini_close(file) (fclose(*(file)) == 0)
#define ini_read(buffer,size,file) (fgets((buffer),(size),*(file)) != NULL)
#define ini_write(buffer,file) (fputs((buffer),*(file)) >= 0)
#define ini_rename(source,dest) (rename((source),(dest)) == 0)
#define ini_remove(filename) (remove(filename) == 0)
#define INI_FILEPOS long int
#define ini_tell(file,pos) (*(pos) = ftell(*(file)))
#define ini_seek(file,pos) (fseek(*(file), *(pos), SEEK_SET) == 0)
有一个关键点需要记住:glue 文件必须严格命名为 minGlue.h。这是因为 minIni.h 中硬编码了 #include "minGlue.h"。
三、写入策略
minIni 在写 INI 文件时,采取了一种 读-拷贝-写(read-copy-write) 的模式。这对于 SD 卡或嵌入式 Flash 这类对擦写次数敏感的非易失性存储来说,是至关重要的。它的具体逻辑是:
- 值未变化 → 直接返回,不做任何写操作(no-op 优化)。
- 值长度相同且支持原地改写 → 直接在原位置修改,不会创建临时文件(需要 glue 层提供
ini_openrewrite 宏)。
- 其他情况 → 创建一个临时文件(文件名末尾字符替换为
~)。然后,它逐行拷贝原文件内容,并在合适的位置插入、修改或删除目标键值对。最后,将临时文件重命名为原文件,完成原子化替换。
删除操作巧妙地复用了写入函数 ini_puts:
- 删除单个键:
ini_puts(Section, Key, NULL, Filename)
- 删除整个节:
ini_puts(Section, NULL, NULL, Filename)
四、编译配置
minIni 依赖预处理器宏在编译时进行功能裁剪,这有助于进一步缩小体积。
| 宏 |
默认值 |
说明 |
INI_BUFFERSIZE |
512 |
栈缓冲区大小,它决定最大行长度和路径长度。 |
INI_READONLY |
未定义 |
定义此宏后,所有写入相关的函数都会被禁用,能有效减小代码体积。 |
INI_REAL |
未定义 |
定义为 float 或 double 后,会启用浮点数读写支持。 |
INI_NOBROWSE |
未定义 |
定义此宏后,会禁用 ini_browse() 这个回调功能。 |
INI_ANSIONLY |
未定义 |
强制使用 ANSI/ASCII 模式,禁用 Unicode/TCHAR 映射。 |
PORTABLE_STRNICMP |
未定义 |
提供一个可移植的 strnicmp 实现。 |
举个例子,在 CMake 中为一个只读的嵌入式场景做配置可能长这样:
target_compile_definitions(minini PUBLIC
INI_READONLY # 只读模式
INI_BUFFERSIZE=256 # 减小缓冲区,节省栈空间
INI_NOBROWSE # 禁用浏览功能
)
五、API 详解
5.1 C API
读取函数:
// 读取字符串,返回拷贝的字符数
int ini_gets(const char *Section, const char *Key, const char *DefValue,
char *Buffer, int BufferSize, const char *Filename);
// 读取整数,支持 0x 前缀的十六进制
long ini_getl(const char *Section, const char *Key, long DefValue,
const char *Filename);
// 读取浮点数(需定义 INI_REAL)
INI_REAL ini_getf(const char *Section, const char *Key, INI_REAL DefValue,
const char *Filename);
// 读取布尔值:Y/T/1 → true,N/F/0 → false,其他 → DefValue
int ini_getbool(const char *Section, const char *Key, int DefValue,
const char *Filename);
注意:ini_getbool 的运作方式,是去读取值的第一个字符进行判断。所以无论是 "yes"、"YES"、"1" 还是 "true",它都会返回 1。相应地,"no"、"0"、"false" 则返回 0。但如果你用它去判断一个端口号 "8080",它的首字符是 '8',不在任何已知的“真”或“假”规则内,此时就会返回你传入的默认值。
写入函数:
int ini_puts(const char *Section, const char *Key, const char *Value,
const char *Filename);
int ini_putl(const char *Section, const char *Key, long Value,
const char *Filename);
int ini_putf(const char *Section, const char *Key, INI_REAL Value,
const char *Filename); // 需定义 INI_REAL
int ini_putbool(const char *Section, const char *Key, int Value,
const char *Filename);
写入时,如果值里包含了注释字符(; 或 #)或尾部空格,minIni 会自动给整个值加上双引号。读取时又会自动把这层引号去掉。而双引号本身则通过 \" 进行转义。
枚举与浏览:
// 按索引遍历节名和键名
int ini_getsection(int idx, char *Buffer, int BufferSize, const char *Filename);
int ini_getkey(const char *Section, int idx, char *Buffer, int BufferSize,
const char *Filename);
// 检查节/键是否存在
int ini_hassection(const char *Section, const char *Filename);
int ini_haskey(const char *Section, const char *Key, const char *Filename);
// 回调方式遍历所有设置
typedef int (*INI_CALLBACK)(const char *Section, const char *Key,
const char *Value, void *UserData);
int ini_browse(INI_CALLBACK Callback, void *UserData, const char *Filename);
回调函数应当返回 1 来继续遍历,或者返回 0 来停止。
无分节 INI 文件: 对于没有 [Section] 的配置文件,只需将 Section 参数传 NULL 即可:
ini_gets(NULL, "key", "default", buf, sizeof(buf), "plain.ini");
ini_puts(NULL, "key", "value", "plain.ini");
5.2 C++ API
minIni 在 minIni.h 中以内联方式定义了一个 minIni 类,用起来会比 C API 更简洁一些:
#include "minIni.h"
minIni ini("config.ini");
// 读取
std::string name = ini.gets("Network", "hostname", "default");
long port = ini.getl("Network", "port", 80);
float offset = ini.getf("Sensor", "offset", 0.0f);
bool enabled = ini.getbool("Network", "enabled", false);
// 写入(重载的 put 方法,自动匹配类型)
ini.put("Network", "hostname", "NewBoard");
ini.put("Network", "port", 9090L);
ini.put("Sensor", "gain", 1.5f);
// 删除
ini.del("Network", "hostname"); // 删除键
ini.del("Network"); // 删除整节
// 枚举
for (int s = 0; ; s++) {
std::string section = ini.getsection(s);
if (section.empty()) break;
for (int k = 0; ; k++) {
std::string key = ini.getkey(section, k);
if (key.empty()) break;
// 处理 key...
}
}
C++ 处理无分节 INI 时,Section 参数传空字符串即可:
minIni plain("plain.ini");
plain.gets("", "key", "default");
plain.put("", "key", "value");
六、实战:集成到 ARM Linux 工程
下面我们以一个 ARM Linux 交叉编译的实例,来完整演示如何把 minIni 集成到真实的嵌入式项目中。
6.1 项目结构
your-project/
├── third_party/
│ └── minIni/
│ ├── minIni.c ← 从 minIni 源码包的 dev/ 目录复制
│ ├── minIni.h ← 同上
│ └── minGlue.h ← 使用 ARM Linux 版本(见下文)
├── src/
│ └── main.c
├── CMakeLists.txt
└── toolchain-arm-linux-gnueabihf.cmake
6.2 第一步:复制源文件
mkdir -p third_party/minIni
cp /path/to/minIni-1.5/dev/minIni.c third_party/minIni/
cp /path/to/minIni-1.5/dev/minIni.h third_party/minIni/
关键细节:minIni.c、minIni.h 和你自定义的 minGlue.h 必须放在同一目录下。这是因为 GCC 对 #include "file.h" 这种形式的包含指令,会优先在源文件所在的目录进行查找。如果你从原 dev/ 目录引用 minIni.c,编译器就会优先找到原始版的那个 minGlue.h,而不是你项目里的定制版本,这很容易导致编译行为与预期不符。
6.3 第二步:放置 Glue 文件
对于 ARM Linux 环境,官方推荐使用 minGlue-Linux.h。它基于 BSD 的 flock() 提供了文件锁,可以保证多进程并发访问的安全。把它复制并重命名:
cp /path/to/minIni-1.5/dev/minGlue-Linux.h third_party/minIni/minGlue.h
踩坑提醒:minIni.h 的第 54 行,也就是 ini_putbool() 的声明部分,用到了 TCHAR 类型,而不是标准的 mTCHAR。在非 Windows 平台上,TCHAR 是未定义的,这会直接导致编译报错。解决方法是,在你的 minGlue.h 文件开头手动补上这个定义:
#ifndef TCHAR
#define TCHAR char
#endif
这可以说是 minIni 自身的一个小 bug,并且在官方发布版中一直没修,集成时千万留意。
下面是调整完的、完整的 ARM Linux 版 minGlue.h 文件内容:
#include <stdio.h>
#include <unistd.h>
#include <sys/file.h>
/* 修复 minIni.h 中 ini_putbool() 使用了未定义的 TCHAR */
#ifndef TCHAR
#define TCHAR char
#endif
#define INI_FILETYPE FILE*
static inline int ini_openread(const char *filename, INI_FILETYPE *file) {
if ((*file = fopen((filename), "r")) == NULL)
return 0;
return flock(fileno(*file), LOCK_SH) == 0;
}
static inline int ini_openwrite(const char *filename, INI_FILETYPE *file) {
if ((*file = fopen((filename), "r+")) == NULL
&& (*file = fopen((filename), "w")) == NULL)
return 0;
if (flock(fileno(*file), LOCK_EX) < 0)
return 0;
return ftruncate(fileno(*file), 0) == 0;
}
#define INI_OPENREWRITE
static inline int ini_openrewrite(const char *filename, INI_FILETYPE *file) {
if ((*file = fopen((filename), "r+")) == NULL)
return 0;
return flock(fileno(*file), LOCK_EX) == 0;
}
#define ini_close(file) (fclose(*(file)) == 0)
#define ini_read(buffer,size,file) (fgets((buffer),(size),*(file)) != NULL)
#define ini_write(buffer,file) (fputs((buffer),*(file)) >= 0)
#define ini_rename(source,dest) (rename((source), (dest)) == 0)
#define INI_FILEPOS long int
#define ini_tell(file,pos) (*(pos) = ftell(*(file)))
#define ini_seek(file,pos) (fseek(*(file), *(pos), SEEK_SET) == 0)
/* 启用浮点支持 */
#define INI_REAL float
#define ini_ftoa(string,value) sprintf((string),"%f",(value))
#define ini_atof(string) (INI_REAL)strtod((string),NULL)
6.4 第三步:CMake 构建
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(your-project C CXX)
# minIni 静态库
add_library(minini STATIC third_party/minIni/minIni.c)
target_include_directories(minini PUBLIC third_party/minIni)
# 应用程序
add_executable(your-app src/main.c)
target_link_libraries(your-app PRIVATE minini)
ARM 交叉编译工具链文件 toolchain-arm-linux-gnueabihf.cmake:
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_C_FLAGS_INIT "-march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard")
set(CMAKE_CXX_FLAGS_INIT "-march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard")
编译命令:
# 本地编译(x86 验证逻辑)
cmake -B build && cmake --build build
# ARM 交叉编译
cmake -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=toolchain-arm-linux-gnueabihf.cmake
cmake --build build-arm
# 验证生成的二进制是 ARM 架构
file build-arm/your-app
# 输出: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), ...
Makefile 方式(替代方案):
CROSS ?=
CC := $(CROSS)gcc
CFLAGS := -Wall -Wextra -Ithird_party/minIni
all: your-app
your-app: src/main.c
$(CC) $(CFLAGS) -o $@ $< third_party/minIni/minIni.c
# ARM 交叉编译:make CROSS=arm-linux-gnueabihf-
七、完整测试用例
7.1 C 语言测试
以下测试覆盖了字符串、整数、浮点数、布尔值的读写,以及覆写、枚举、存在性检查、回调浏览、删除操作,还有无分节 INI 文件的处理。
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "minIni.h"
#define sizearray(a) (sizeof(a) / sizeof((a)[0]))
static const char inifile[] = "config.ini";
static const char plainfile[] = "config_plain.ini";
/* 浏览回调 */
static int browse_cb(const char *section, const char *key,
const char *value, void *userdata)
{
printf(" [%s] %s = %s\n", section, key, value);
return 1; /* 返回1继续遍历,返回0停止 */
}
/* 创建测试数据 */
static void create_test_files(void)
{
ini_puts("Network", "hostname", "MyBoard", inifile);
ini_puts("Network", "address", "192.168.1.100", inifile);
ini_putl("Network", "port", 8080, inifile);
ini_puts("Network", "enabled", "yes", inifile);
ini_putl("Network", "timeout", 30, inifile);
ini_puts("Sensor", "name", "temperature", inifile);
ini_putl("Sensor", "interval", 5000, inifile);
ini_puts("Sensor", "unit", "Celsius", inifile);
ini_putbool("Sensor", "active", 1, inifile);
/* 无分节文件 */
ini_puts(NULL, "app_name", "minIniDemo", plainfile);
ini_putl(NULL, "version", 2, plainfile);
}
/* 测试1:字符串读取 */
static void test_string_read(void)
{
char buf[100];
int n;
n = ini_gets("Network", "hostname", "N/A", buf, sizearray(buf), inifile);
assert(n == 7 && strcmp(buf, "MyBoard") == 0);
/* 不存在的键 → 返回默认值 */
n = ini_gets("Network", "gateway", "0.0.0.0", buf, sizearray(buf), inifile);
assert(strcmp(buf, "0.0.0.0") == 0);
/* 无分节 INI:Section 传 NULL */
n = ini_gets(NULL, "app_name", "N/A", buf, sizearray(buf), plainfile);
assert(n == 10 && strcmp(buf, "minIniDemo") == 0);
printf("[PASS] 1. String reading\n");
}
/* 测试2:整数读取 */
static void test_int_read(void)
{
assert(ini_getl("Network", "port", -1, inifile) == 8080);
assert(ini_getl("Sensor", "interval", -1, inifile) == 5000);
assert(ini_getl("Network", "retry", -1, inifile) == -1); /* 不存在→默认 */
assert(ini_getl(NULL, "version", -1, plainfile) == 2);
printf("[PASS] 2. Integer reading\n");
}
/* 测试3:浮点读写 */
static void test_float_read(void)
{
ini_putf("Sensor", "calibration", 3.14f, inifile);
float f = ini_getf("Sensor", "calibration", 0.0f, inifile);
assert(f > 3.13f && f < 3.15f);
printf("[PASS] 3. Float reading\n");
}
/* 测试4:布尔读取 */
static void test_bool_read(void)
{
assert(ini_getbool("Network", "enabled", 0, inifile) == 1); /* "yes" → 1 */
assert(ini_getbool("Sensor", "active", 0, inifile) == 1); /* 1 → 1 */
/* "8080" 首字符 '8' 不被识别,返回默认值 */
assert(ini_getbool("Network", "port", 0, inifile) == 0);
printf("[PASS] 4. Bool reading\n");
}
/* 测试5:写入与覆写 */
static void test_write(void)
{
char buf[100];
/* 写入新键 */
ini_puts("Network", "gateway", "192.168.1.1", inifile);
ini_gets("Network", "gateway", "N/A", buf, sizearray(buf), inifile);
assert(strcmp(buf, "192.168.1.1") == 0);
/* 覆写已有键 */
ini_puts("Network", "hostname", "NewBoard", inifile);
ini_gets("Network", "hostname", "N/A", buf, sizearray(buf), inifile);
assert(strcmp(buf, "NewBoard") == 0);
/* 写入新节 */
ini_puts("Logging", "level", "DEBUG", inifile);
ini_gets("Logging", "level", "N/A", buf, sizearray(buf), inifile);
assert(strcmp(buf, "DEBUG") == 0);
/* 包含特殊字符的值会被自动加引号 */
ini_puts("Network", "ssid", "My;Network#1", inifile);
ini_gets("Network", "ssid", "N/A", buf, sizearray(buf), inifile);
assert(strcmp(buf, "My;Network#1") == 0);
printf("[PASS] 5. Writing and overwriting\n");
}
/* 测试6:节/键枚举 */
static void test_enumeration(void)
{
char section[50], key[50];
int s, k, section_count = 0;
for (s = 0; ini_getsection(s, section, sizearray(section), inifile) > 0; s++) {
printf(" [%s]\n", section);
section_count++;
for (k = 0; ini_getkey(section, k, key, sizearray(key), inifile) > 0; k++) {
printf(" %s\n", key);
}
}
assert(section_count >= 2);
printf("[PASS] 6. Enumeration\n");
}
/* 测试7:存在性检查 */
static void test_presence(void)
{
assert(ini_hassection("Network", inifile) == 1);
assert(ini_hassection("NoExist", inifile) == 0);
assert(ini_haskey("Network", "hostname", inifile) == 1);
assert(ini_haskey("Network", "noexist", inifile) == 0);
printf("[PASS] 7. Presence check\n");
}
/* 测试8:浏览回调 */
static void test_browse(void)
{
printf(" --- Browse all settings ---\n");
ini_browse(browse_cb, NULL, inifile);
printf("[PASS] 8. Browsing\n");
}
/* 测试9:删除 */
static void test_delete(void)
{
/* 删除单个键 */
assert(ini_puts("Logging", "level", NULL, inifile) == 1);
assert(ini_haskey("Logging", "level", inifile) == 0);
/* 删除整个节 */
assert(ini_puts("Logging", NULL, NULL, inifile) == 1);
assert(ini_hassection("Logging", inifile) == 0);
/* 删除无分节键 */
assert(ini_puts(NULL, "version", NULL, plainfile) == 1);
assert(ini_haskey(NULL, "version", plainfile) == 0);
printf("[PASS] 9. Deletion\n");
}
int main(void)
{
create_test_files();
test_string_read();
test_int_read();
test_float_read();
test_bool_read();
test_write();
test_enumeration();
test_presence();
test_browse();
test_delete();
printf("\nAll tests passed!\n");
return 0;
}
7.2 C++ 测试
#include <cassert>
#include <cmath>
#include <iostream>
#include <string>
#include "minIni.h"
int main(void)
{
minIni ini("config.ini");
minIni plain("config_plain.ini");
/* 写入测试数据 */
ini.put("Device", "name", "ARM-Board-A7");
ini.put("Device", "revision", 3);
ini.put("Device", "enabled", true);
ini.put("Network", "ip", "10.0.0.50");
ini.put("Network", "port", 8883L);
ini.put("Network", "tls", true);
ini.put("Sensor", "type", "BME280");
ini.putf("Sensor", "offset", -0.5f);
plain.put("", "app_name", "minIniCppDemo");
plain.put("", "version", 2L);
/* 字符串读取 */
assert(ini.gets("Device", "name", "") == "ARM-Board-A7");
assert(ini.gets("Device", "missing", "default") == "default");
assert(plain.gets("", "app_name", "") == "minIniCppDemo");
std::cout << "[PASS] 1. String reading\n";
/* 整数读取 */
assert(ini.getl("Device", "revision", -1) == 3);
assert(ini.getl("Network", "port", -1) == 8883);
assert(plain.getl("", "version", -1) == 2);
std::cout << "[PASS] 2. Integer reading\n";
/* 浮点读写 */
float offset = ini.getf("Sensor", "offset", 0.0f);
assert(std::fabs(offset - (-0.5f)) < 0.01f);
ini.put("Sensor", "gain", 1.25f);
assert(std::fabs(ini.getf("Sensor", "gain", 0.0f) - 1.25f) < 0.01f);
std::cout << "[PASS] 3. Float reading\n";
/* 布尔读取 */
assert(ini.getbool("Device", "enabled", false) == true);
assert(ini.getbool("Network", "tls", false) == true);
std::cout << "[PASS] 4. Bool reading\n";
/* 写入与覆写 */
ini.put("Device", "location", "Lab-3");
assert(ini.gets("Device", "location", "") == "Lab-3");
ini.put("Device", "name", "ARM-Board-A53");
assert(ini.gets("Device", "name", "") == "ARM-Board-A53");
/* 包含特殊字符的值 */
ini.put("Device", "note", "rev;3#beta");
assert(ini.gets("Device", "note", "") == "rev;3#beta");
std::cout << "[PASS] 5. Writing and overwriting\n";
/* 删除 */
assert(ini.del("Device", "location"));
assert(!ini.haskey("Device", "location"));
assert(ini.del("Device")); /* 删除整个节 */
assert(!ini.hassection("Device"));
assert(plain.del("", "version")); /* 删除无分节键 */
std::cout << "[PASS] 6. Deletion\n";
std::cout << "\nAll tests passed!\n";
return 0;
}
7.3 运行测试
# 本地编译运行
cmake -B build && cmake --build build
./build/demo_c
./build/demo_cpp
# ARM 交叉编译后部署到目标板
cmake -B build-arm -DCMAKE_TOOLCHAIN_FILE=toolchain-arm-linux-gnueabihf.cmake
cmake --build build-arm
scp build-arm/demo_c build-arm/demo_cpp user@arm-board:/tmp/
实际测试输出大致如下:
=== minIni ARM Linux Demo (C API) ===
[PASS] 1. String reading
[PASS] 2. Integer reading
[PASS] 3. Float reading
[PASS] 4. Bool reading
[PASS] 5. Writing and overwriting
[PASS] 6. Section/key enumeration (3 sections, 13 keys)
[PASS] 7. Presence check
--- Browse all settings ---
[Network] hostname = NewBoard
[Network] address = 192.168.1.100
[Network] port = 8080
[Network] enabled = yes
...
[PASS] 8. Browsing
[PASS] 9. Deletion
[PASS] 10. Concurrent access glue (flock compile check)
All tests passed!

八、集成踩坑总结
坑 1:TCHAR 未定义编译报错
现象:在 Linux 平台编译时,遇到 unknown type name 'TCHAR' 的错误。
原因:minIni.h 第 54 行 ini_putbool() 的声明中,参数类型写成了 TCHAR,而非 mTCHAR。在非 Windows 且未启用 Unicode 的环境里,TCHAR 天生就没有定义,而 mTCHAR 则被正确地定义为 char。这算是 minIni 一个公开的 bug 了。
修复:在 minGlue.h 中增加 #define TCHAR char 即可。
坑 2:自定义 minGlue.h 不生效
现象:明明已经把定制好的 minGlue.h 放在项目目录并加入了 -I 头文件搜索路径,编译时却依然调用了原始的文件。
原因:GCC 在处理 #include "file.h" 时,搜索顺序是:① 当前源文件所在目录;② 再由 -I 指定的目录。由于 minIni.c 和原始的 minGlue.h 同在一个 dev/ 目录下,编译器总是优先找到“原版”。
修复:唯一的办法就是把 minIni.c、minIni.h 和你自己的 minGlue.h 复制到同一个目录中,而不是通过 -I 去引用 dev/ 下的源文件。

九、适用场景与局限
适合的场景
- 嵌入式 Linux 或 RTOS 上的轻量级配置管理。
- 对 Flash 存储(如 SD 卡、eMMC)擦写次数敏感的场景。
- 需要适配非标准文件系统(比如 FatFs、Petit-FatFs)的项目。
- 对内存占用有严苛要求的场合。
需要注意的局限
- 每次读写都要打开和关闭文件,并不适合高频读写的应用。
- 不支持多行值、嵌套结构等 INI 扩展语法。
- 写入由临时文件完成,如果中途意外断电,可能会残留一个
~ 后缀的临时文件。
- Unicode 的支持仅限 Windows 平台(依赖
tchar.h),Linux 下为纯 ASCII 模式。
参考链接
对于在资源受限的嵌入式 C/C++ 项目中进行轻量化配置,minIni 这种库虽然“老派”,但其稳固、透明的设计哲学至今仍有很强的参考意义。如果你也在头文件中遇到了 TCHAR 这个“经典”问题,希望本篇的避坑指南正好帮到了你。