找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4880

积分

0

好友

633

主题
发表于 3 小时前 | 查看: 3| 回复: 0

在嵌入式开发中,配置文件的读写几乎无处不在。而 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 这类对擦写次数敏感的非易失性存储来说,是至关重要的。它的具体逻辑是:

  1. 值未变化 → 直接返回,不做任何写操作(no-op 优化)。
  2. 值长度相同且支持原地改写 → 直接在原位置修改,不会创建临时文件(需要 glue 层提供 ini_openrewrite 宏)。
  3. 其他情况 → 创建一个临时文件(文件名末尾字符替换为 ~)。然后,它逐行拷贝原文件内容,并在合适的位置插入、修改或删除目标键值对。最后,将临时文件重命名为原文件,完成原子化替换。

删除操作巧妙地复用了写入函数 ini_puts

  • 删除单个键:ini_puts(Section, Key, NULL, Filename)
  • 删除整个节:ini_puts(Section, NULL, NULL, Filename)

四、编译配置

minIni 依赖预处理器宏在编译时进行功能裁剪,这有助于进一步缩小体积。

默认值 说明
INI_BUFFERSIZE 512 栈缓冲区大小,它决定最大行长度和路径长度。
INI_READONLY 未定义 定义此宏后,所有写入相关的函数都会被禁用,能有效减小代码体积。
INI_REAL 未定义 定义为 floatdouble 后,会启用浮点数读写支持。
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.cminIni.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!

ARM Linux 终端界面显示 minIni 测试通过的结果

八、集成踩坑总结

坑 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.cminIni.h 和你自己的 minGlue.h 复制到同一个目录中,而不是通过 -I 去引用 dev/ 下的源文件。

CompuPhase 网站上的 minIni 项目文档页面,介绍了其轻量级 INI 解析库

九、适用场景与局限

适合的场景

  • 嵌入式 Linux 或 RTOS 上的轻量级配置管理。
  • 对 Flash 存储(如 SD 卡、eMMC)擦写次数敏感的场景。
  • 需要适配非标准文件系统(比如 FatFs、Petit-FatFs)的项目。
  • 对内存占用有严苛要求的场合。

需要注意的局限

  • 每次读写都要打开和关闭文件,并不适合高频读写的应用。
  • 不支持多行值、嵌套结构等 INI 扩展语法。
  • 写入由临时文件完成,如果中途意外断电,可能会残留一个 ~ 后缀的临时文件。
  • Unicode 的支持仅限 Windows 平台(依赖 tchar.h),Linux 下为纯 ASCII 模式。

参考链接

对于在资源受限的嵌入式 C/C++ 项目中进行轻量化配置,minIni 这种库虽然“老派”,但其稳固、透明的设计哲学至今仍有很强的参考意义。如果你也在头文件中遇到了 TCHAR 这个“经典”问题,希望本篇的避坑指南正好帮到了你。




上一篇:C#入门实战:10个必练的算法与程序设计示例
下一篇:马路式爱情与王响式好人:为什么很多年轻人正在重演这两种失败活法
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-1 06:40 , Processed in 0.602669 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表