Code generation com m4 ou scripts customizados

1. Introdução à Geração de Código em C

Escrever código C manualmente para estruturas repetitivas é tedioso e propenso a erros. Imagine manter manualmente uma tabela de lookup com 256 entradas para uma função CRC, ou um parser que precisa lidar com centenas de comandos diferentes. A geração de código resolve esse problema ao automatizar a criação de código fonte a partir de especificações de alto nível.

Cenários comuns onde a geração de código se destaca incluem:
- Tabelas de lookup para funções matemáticas ou checksums
- Parsers de protocolos ou formatos de dados
- Código de serialização/desserialização a partir de schemas
- Interfaces para sistemas de plugins
- Stubs para chamadas de sistema ou bibliotecas externas

As ferramentas mais utilizadas para essa tarefa são o m4 (um processador de macros genérico) e scripts customizados em shell, Python ou awk. Cada abordagem tem seus pontos fortes, e a escolha depende da complexidade da lógica necessária.

2. Fundamentos do m4 para Geração de Código C

O m4 é um processador de macros poderoso e maduro, presente em praticamente todos os sistemas Unix. Sua sintaxe é minimalista, mas extremamente flexível.

Sintaxe básica

Macros são definidas com define e expandidas pelo nome:

define(`NOME', `valor')

Para gerar código C, criamos macros que expandem para trechos de código:

define(`HEADER_GUARD', `#ifndef $1_H
#define $1_H
')

define(`FOOTER_GUARD', `#endif /* $1_H */
')

Exemplo prático: gerar função de inicialização de array

Considere um arquivo cores.m4 que define uma lista de cores e gera uma função de inicialização:

define(`CORES', `vermelho, verde, azul, amarelo, branco')
define(`GERA_CORES', `void init_cores(void) {
    static const char *cores[] = {`'dnl
    ifelse(`$#', `0', `', `patsubst(`$1', `,', `,
        "')')
    };
    for (int i = 0; i < sizeof(cores)/sizeof(cores[0]); i++) {
        printf("Cor %d: %s\n", i, cores[i]);
    }
}')

GERA_CORES(CORES)

Ao processar com m4 cores.m4 > cores.c, obtemos:

void init_cores(void) {
    static const char *cores[] = {
        "vermelho",
        "verde",
        "azul",
        "amarelo",
        "branco"
    };
    for (int i = 0; i < sizeof(cores)/sizeof(cores[0]); i++) {
        printf("Cor %d: %s\n", i, cores[i]);
    }
}

3. Técnicas Avançadas com m4

Macros recursivas e iteração

Para processar listas de tamanho variável, usamos recursão:

define(`ITERA', `ifelse(`$1', `', `', `processa(`$1')
ITERA(shift($@))')')
define(`processa', `    printf("Item: %s\n", "$1");')

ITERA(maça, banana, cereja, damasco)

Geração condicional com ifdef e ifelse

Útil para gerar código específico por plataforma:

ifdef(`LINUX', `#include <unistd.h>', `#include <windows.h>')
define(`PLATAFORMA_SLEEP', `ifelse(`$1', `linux', `sleep(1)', `Sleep(1000)')')

Uso de divert e undivert

Organiza a saída em seções, permitindo reordenar o código gerado:

divert(1)  /* Seção de includes */
divert(2)  /* Seção de implementação */
divert(0)  /* Volta ao fluxo normal */
undivert(1)
undivert(2)

Exemplo: gerar switch-case a partir de configuração

Arquivo comandos.txt:

INICIAR 1
PARAR 2
STATUS 3
RESET 4

Macro para gerar o switch:

define(`GERA_SWITCH', `
switch(cmd) {
    ifelse($#, 0, , $1)
}')
define(`ENTRY', `    case $2: return CMD_$1;
ENTRY')

GERA_SWITCH(`
ENTRY(INICIAR, 1)
ENTRY(PARAR, 2)
ENTRY(STATUS, 3)
ENTRY(RESET, 4)
')

4. Scripts Customizados como Alternativa

Quando a lógica de geração se torna complexa — envolvendo acesso a banco de dados, validação de dados ou formatação avançada — scripts customizados são mais adequados.

Exemplo com shell script: CSV para structs C

Arquivo campos.csv:

nome,string,50
idade,int,0
salario,float,0
ativo,bool,0

Script gen_struct.sh:

#!/bin/bash
echo "#ifndef REGISTRO_H"
echo "#define REGISTRO_H"
echo ""
echo "typedef struct {"
while IFS=, read campo tipo tamanho; do
    case $tipo in
        string) echo "    char $campo[$tamanho];" ;;
        int)    echo "    int $campo;" ;;
        float)  echo "    float $campo;" ;;
        bool)   echo "    int $campo;" ;;
    esac
done < campos.csv
echo "} Registro;"
echo ""
echo "#endif"

Exemplo com Python e Jinja2

Template struct_template.c.j2:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
{% for campo in campos %}
    {{ campo.tipo }} {{ campo.nome }};
{% endfor %}
} {{ struct_nome }};

void {{ struct_nome }}_print(const {{ struct_nome }} *obj) {
    printf("{{ struct_nome }}:\n");
{% for campo in campos %}
    printf("  {{ campo.nome }}: %{{ campo.formato }}\n", obj->{{ campo.nome }});
{% endfor %}
}

Script gerar.py:

from jinja2 import Template
import json

with open('schema.json') as f:
    schema = json.load(f)

with open('struct_template.c.j2') as f:
    template = Template(f.read())

output = template.render(**schema)
with open(f'{schema["struct_nome"].lower()}.c', 'w') as f:
    f.write(output)

5. Integração com o Build System

A geração de código deve ser integrada ao Makefile para ocorrer automaticamente durante a compilação.

Regras implícitas no Makefile

# Gerar código a partir de arquivos .m4
%.c: %.m4
    m4 $< > $@

# Gerar código a partir de scripts Python
%.c: %.py
    python3 $< > $@

# Gerar cabeçalhos
%.h: %.h.m4
    m4 $< > $@

Makefile completo

CC = gcc
CFLAGS = -Wall -Wextra -std=c11

SRC = main.c comandos.c
GEN_SRC = comandos.c
GEN_DEPS = comandos.m4 comandos.txt

all: programa

programa: $(SRC) $(GEN_SRC)
    $(CC) $(CFLAGS) -o $@ $^

# Geração automática
comandos.c: comandos.m4 comandos.txt
    m4 comandos.m4 > comandos.c

# Gerenciamento de dependências
comandos.c: comandos.txt

clean:
    rm -f programa comandos.c

.PHONY: all clean

6. Casos de Uso Concretos em Projetos C

Geração de tabela de lookup para CRC8

Arquivo crc8.m4:

define(`CRC8_TABLE', `
static const unsigned char crc8_table[256] = {
    ifelse($#, 0, , `$1')
};')

define(`GERA_TABLE', `CRC8_TABLE(`
    forloop(`i', 0, 255, `    0x`'eval(CRC8_POLY, 16), ')
')')

GERA_TABLE

Geração de parser de comandos

define(`COMANDO', `ifelse(`$1', `', `', `
    if (strcmp(cmd, "$1") == 0) return CMD_$2;
')')

int parse_comando(const char *cmd) {
COMANDO(INICIAR, 1)
COMANDO(PARAR, 2)
COMANDO(STATUS, 3)
    return CMD_DESCONHECIDO;
}

7. Boas Práticas e Armadilhas Comuns

  1. Mantenha o código gerado legível: Use indentação consistente e adicione comentários no template.

  2. Escolha a ferramenta certa: m4 é excelente para transformações simples de texto. Para lógica complexa, prefira Python.

  3. Versionamento: Inclua o código gerado no repositório apenas se a ferramenta de geração não estiver amplamente disponível. Caso contrário, versionar apenas os templates reduz ruído.

  4. Testes: Sempre verifique que o código gerado compila e passa nos testes. Adicione uma regra test no Makefile.

  5. Evite efeitos colaterais: Macros m4 podem ter efeitos colaterais inesperados. Teste cada macro isoladamente.

8. Conclusão e Próximos Passos

A geração de código com m4 ou scripts customizados transforma tarefas repetitivas em processos automatizados e confiáveis. m4 é ideal para transformações simples e portáveis, enquanto Python oferece maior flexibilidade para lógica complexa. A integração com sistemas de build como Make garante que o código gerado esteja sempre atualizado.

Para aprofundar, explore a documentação do m4, estude o sistema GNU Autotools (que usa m4 extensivamente) e considere frameworks de template como Jinja2 para projetos maiores. A geração de código é uma habilidade essencial para qualquer desenvolvedor C que lida com sistemas complexos.

Referências

  • GNU M4 Manual — Documentação oficial completa do m4, incluindo todas as macros embutidas e exemplos avançados
  • Jinja2 Documentation — Documentação oficial do Jinja2, template engine Python amplamente usado para geração de código
  • GNU Autoconf Manual — Documentação do Autoconf, que demonstra uso avançado de m4 para geração de scripts de configuração
  • Code Generation with m4 — Artigo técnico de Chris Wellons sobre técnicas práticas de geração de código C com m4
  • Makefile Tutorial by Example — Tutorial completo sobre Makefiles, incluindo regras implícitas e gerenciamento de dependências para código gerado
  • Using Python for Code Generation in C Projects — Artigo da Memfault sobre boas práticas de geração de código C com Python
  • m4 Macro Processor Examples — Seção de exemplos do manual do m4, com casos reais de uso para geração de código