Configuração via arquivos INI, JSON ou YAML

1. Por que usar arquivos de configuração em C?

1.1. Separar código de dados: parâmetros mutáveis sem recompilação

Em aplicações C tradicionais, parâmetros como endereços de servidores, portas de conexão ou níveis de log ficam hardcoded no código-fonte. Qualquer alteração exige recompilação completa do projeto. Arquivos de configuração resolvem esse problema permitindo que valores sejam alterados externamente, sem tocar no código compilado.

1.2. Comparação com variáveis de ambiente e argumentos de linha de comando

Variáveis de ambiente (getenv) e argumentos de linha de comando (argc/argv) também permitem parametrização externa, mas possuem limitações: variáveis de ambiente poluem o escopo global e não suportam hierarquia; argumentos de linha de comando tornam-se inviáveis com muitos parâmetros. Arquivos de configuração oferecem estrutura, documentação embutida (comentários) e persistência entre execuções.

1.3. Escolha do formato: quando usar INI, JSON ou YAML

  • INI: formato mais simples e rápido de parsear. Ideal para configurações planas com poucas seções. Excelente em sistemas embarcados.
  • JSON: suporta estruturas aninhadas (arrays, objetos). Padrão para APIs web e ferramentas modernas. Parseamento moderadamente rápido.
  • YAML: máxima legibilidade humana com indentação significativa. Suporta âncoras e referências. Parseamento mais lento, mas ideal para configurações complexas.

2. Parsing de arquivos INI (formato clássico)

2.1. Estrutura típica

Arquivos INI organizam-se em seções [secao], com pares chave=valor e comentários iniciados por ; ou #.

; config.ini - Configuração de servidor
[server]
host=192.168.1.100
port=8080
timeout=30

[logging]
level=info
file=/var/log/app.log

2.2. Implementação manual vs. bibliotecas

Implementar um parser INI manualmente é viável para casos simples, mas bibliotecas como inih (header-only, ~100 linhas) e libconfuse oferecem robustez, suporte a escapes e tratamento de erros.

2.3. Exemplo prático com inih

#include <stdio.h>
#include <string.h>
#include "ini.h"

typedef struct {
    char host[256];
    int port;
    int timeout;
} server_config_t;

static int handler(void* user, const char* section, const char* name,
                   const char* value) {
    server_config_t* cfg = (server_config_t*)user;
    if (strcmp(section, "server") == 0) {
        if (strcmp(name, "host") == 0)
            snprintf(cfg->host, sizeof(cfg->host), "%s", value);
        else if (strcmp(name, "port") == 0)
            cfg->port = atoi(value);
        else if (strcmp(name, "timeout") == 0)
            cfg->timeout = atoi(value);
    }
    return 1;
}

int main() {
    server_config_t cfg = { .port = 0, .timeout = 0 };
    if (ini_parse("config.ini", handler, &cfg) < 0) {
        printf("Erro ao ler config.ini\n");
        return 1;
    }
    printf("Host: %s, Porta: %d, Timeout: %d\n", cfg.host, cfg.port, cfg.timeout);
    return 0;
}

3. Parsing de arquivos JSON (formato moderno e aninhado)

3.1. Modelo de dados JSON

JSON representa dados como objetos ({}), arrays ([]), strings, números, booleanos e null. Permite aninhamento profundo, ideal para configurações hierárquicas.

{
  "logger": {
    "level": "debug",
    "output": "/var/log/app.log",
    "format": "json",
    "rotation": {
      "max_size_mb": 100,
      "max_files": 5
    }
  }
}

3.2. Bibliotecas populares em C

  • cJSON: leve, header-only, sem dependências externas. Ideal para projetos pequenos.
  • Jansson: madura, com suporte a schema e encoding. Mais completa.
  • parson: extremamente leve (~500 linhas), focada em parseamento básico.

3.3. Exemplo prático com cJSON

#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"

typedef struct {
    char level[16];
    char output[256];
    char format[16];
} logger_config_t;

int load_json_config(const char* filename, logger_config_t* cfg) {
    FILE* f = fopen(filename, "r");
    if (!f) return -1;
    fseek(f, 0, SEEK_END);
    long len = ftell(f);
    fseek(f, 0, SEEK_SET);
    char* data = malloc(len + 1);
    fread(data, 1, len, f);
    data[len] = '\0';
    fclose(f);

    cJSON* json = cJSON_Parse(data);
    free(data);
    if (!json) return -2;

    cJSON* logger = cJSON_GetObjectItem(json, "logger");
    if (logger) {
        cJSON* item;
        if ((item = cJSON_GetObjectItem(logger, "level")))
            snprintf(cfg->level, sizeof(cfg->level), "%s", item->valuestring);
        if ((item = cJSON_GetObjectItem(logger, "output")))
            snprintf(cfg->output, sizeof(cfg->output), "%s", item->valuestring);
        if ((item = cJSON_GetObjectItem(logger, "format")))
            snprintf(cfg->format, sizeof(cfg->format), "%s", item->valuestring);
    }
    cJSON_Delete(json);
    return 0;
}

int main() {
    logger_config_t cfg = {0};
    if (load_json_config("config.json", &cfg) != 0) {
        printf("Erro ao carregar config.json\n");
        return 1;
    }
    printf("Logger: level=%s, output=%s, format=%s\n",
           cfg.level, cfg.output, cfg.format);
    return 0;
}

4. Parsing de arquivos YAML (formato hierárquico legível)

4.1. Características do YAML

YAML usa indentação (espaços) para definir hierarquia, suporta listas com -, dicionários com chave: valor, âncoras (&) e aliases (*). É o formato mais legível para configurações complexas.

plugins:
  - name: auth
    path: /usr/lib/plugins/auth.so
    params:
      timeout: 30
      retries: 3
  - name: cache
    path: /usr/lib/plugins/cache.so
    params:
      max_entries: 1000

4.2. Bibliotecas em C

  • libyaml: parser de baixo nível, evento-driven. Flexível, mas requer mais código.
  • cyaml: wrapper de alto nível sobre libyaml, com mapeamento direto para structs C.

4.3. Exemplo prático com libyaml

#include <stdio.h>
#include <yaml.h>

void parse_yaml_config(const char* filename) {
    FILE* f = fopen(filename, "r");
    if (!f) return;

    yaml_parser_t parser;
    yaml_event_t event;
    yaml_parser_initialize(&parser);
    yaml_parser_set_input_file(&parser, f);

    do {
        yaml_parser_parse(&parser, &event);
        if (event.type == YAML_SCALAR_EVENT) {
            printf("Valor: %s\n", event.data.scalar.value);
        }
        yaml_event_delete(&event);
    } while (event.type != YAML_STREAM_END_EVENT);

    yaml_parser_delete(&parser);
    fclose(f);
}

5. Abstração unificada: interface de configuração genérica

5.1. Definição de struct comum

typedef struct {
    char host[256];
    int port;
    int timeout;
    char log_level[16];
    char log_file[256];
} config_t;

5.2. Funções de carga unificadas

config_t load_config_ini(const char* file);
config_t load_config_json(const char* file);
config_t load_config_yaml(const char* file);

config_t load_config(const char* file) {
    const char* ext = strrchr(file, '.');
    if (!ext) return (config_t){0};
    if (strcmp(ext, ".ini") == 0) return load_config_ini(file);
    if (strcmp(ext, ".json") == 0) return load_config_json(file);
    if (strcmp(ext, ".yaml") == 0 || strcmp(ext, ".yml") == 0)
        return load_config_yaml(file);
    return (config_t){0};
}

5.3. Vantagens da abstração

Permite trocar o formato de configuração sem alterar a lógica de negócio. Pode-se implementar fallback: tentar JSON, depois YAML, depois INI.

6. Tratamento de erros e validação

6.1. Parsing mal-sucedido

Sempre verificar retornos de funções de parsing. Arquivo ausente, sintaxe inválida ou campos obrigatórios faltando devem ser tratados explicitamente.

6.2. Validação de tipos e limites

int validate_port(int port) {
    return (port >= 1 && port <= 65535) ? 1 : 0;
}

int validate_timeout(int timeout) {
    return (timeout > 0 && timeout <= 3600) ? 1 : 0;
}

6.3. Mensagens de erro amigáveis

fprintf(stderr, "Erro na linha %d, coluna %d: valor inválido para 'port'\n",
        error_line, error_col);

7. Considerações de desempenho e portabilidade

7.1. Overhead de parsing

  • INI: mais rápido (~1-5 µs para arquivos pequenos)
  • JSON: moderado (~10-50 µs)
  • YAML: mais lento (~50-200 µs) devido à complexidade sintática

7.2. Uso em sistemas embarcados

Preferir INI ou JSON com alocação estática. Evitar alocações dinâmicas frequentes. Bibliotecas como inih e parson são ideais por seu tamanho reduzido.

7.3. Boas práticas

  • Cachear configuração em memória após leitura
  • Implementar recarga via sinal SIGHUP:
void sig_handler(int signo) {
    if (signo == SIGHUP) reload_config();
}

Referências