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
-
Mantenha o código gerado legível: Use indentação consistente e adicione comentários no template.
-
Escolha a ferramenta certa: m4 é excelente para transformações simples de texto. Para lógica complexa, prefira Python.
-
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.
-
Testes: Sempre verifique que o código gerado compila e passa nos testes. Adicione uma regra
testno Makefile. -
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