Já programou (em linguagem C) usando X Macro ?

Um tempo atrás estava lendo o código de uma implementação de linguagem Forth para ARM (nota mental: fazer um post disso) e me deparei com o uso de uma técnica conhecida como X Macro. Confesso que não conhecia. Comentei com alguns amigos experientes e eles também nunca tinham usado. Resolvi então fazer este post, pois é algo sensacional para resolver problemas de organização no seu código. Sim, eu sei que macros em C é um assunto que divide programadores e que elas vão do paraíso para o inferno em segundos. Acalma-se, vai ser interessante.

A ideia é relativamente simples. Existem informações que devem andar juntas ou irão gerar problemas. Por exemplo, você tem uma enumeração de 0 a 9 e um vetor de 10 posições onde cada posição tem relação com a enumeração citada. Ou ainda, você pode ter uma lista de comandos e outra lista, com o texto de ajuda desses comandos, que precisam andar casadas. Certamente podem imaginar como criar isso, separadamente, assim como sabem os desastres decorrentes de não manter o sincronismo adequado dessas informações.

A X macro permite que você tabele os dados que deseja manter juntos, de forma a sempre fazer alterações apenas nessa tabela, com pouca chance de erro. Depois, partindo dessa tabela, você define as estruturas de dados derivadas, através da seleção apropriada de campos dessa tabela.

Vamos ver um trechinho de código, mostrando uma tabela com dois campos, “ID” e “comando” (as colunas), e com 4 registros (as linhas):

#define XMACRO_COMMAND_LIST     \
    X(CMD_APAGAR,   "apagar")   \
    X(CMD_ESCREVER, "escrever") \
    X(CMD_IMAGEM,   "imagem")   \
    X(CMD_INVALIDO,  "")

Note que a macro XMACRO_COMMAND_LIST depende de uma outra macro X, ainda não definida. Somente na hora de usar, a macro X é definida. Veja a seguir como isso é feito:

enum cmd_e
{
#define X(CMD_ENUM, CMD_NAME) CMD_ENUM,
	XMACRO_COMMAND_LIST
#undef X
};

A mágica acontece na linha 3, onde a macro X é definida. Os seus argumentos são os nomes dos campos da tabela criada (CMD_ENUM, CMD_NAME), mas é você que escolhe quais campos deseja usar. No exemplo anterior, apenas foram usados os rótulos para a criação da enumeração, proveniente do campo CMD_ENUM. Toda a tabela (XMACRO_COMMAND_LIST) é então expandida, logo após a definição da macro X. Depois, para que a macro possa ser usada novamente em outra situação, ela deve ser removida (#undef X). Esse código irá gerar o trecho abaixo:

enum cmd_e
{
    CMD_APAGAR,
    CMD_ESCREVER,
    CMD_IMAGEM,
    CMD_INVALIDO,
};

Imagino que vocês conseguem entender o que essa segunda definição e expansão da X macro irá fazer, não ? Notem que está sendo usado agora o campo CMD_NAME.

static const char *cmd_names[] =
{
#define X(CMD_ENUM, CMD_NAME) CMD_NAME,
	XMACRO_COMMAND_LIST
#undef X
};

Sim, temos a lista de nomes de comandos, com casamento garantido !

static const char *cmd_names[] =
{
    "apagar",
    "escrever",
    "imagem",
    "",
};

Um uso muito interessante é em máquinas de estados. Eu costumo criar minhas máquinas como um vetor de ponteiros para funções, onde cada função é um estado. Essa técnica permite que a enumeração dos estados não perca o sincronismo com o vetor de ponteiros. Veja um exemplo nessa página do stack overflow.

Como o redator desse blog prefere criar a copiar, segue um exemplo autoral, bem mais complexo. Recomendo fortemente que estude o código, como dever de casa. Deixei os comentários nas expansões, para ajudá-lo. O código está também no repositório do Base 10, no Bitbucket. Ele deve compilar com qualquer compilador compatível com C99. No GCC, bastou um

gcc -std=c99 xmacros.c -o xmacros
/*
Blog Base 10 (http://base10.wordpress.com)

Copyright 2017,  Marcelo Barros de Almeida <marcelobarrosalmeida@gmail.com>

Licença: BSD (https://opensource.org/licenses/BSD-3-Clause)
*/
#include <string.h>
#include <stdio.h>
#include <stdbool.h>

// Cria a lista de elementos que devem ser mantidos juntos. No caso, temos:
//
// 1) o indice usado na enumeracao
// 2) o simbolo representativo da enumeracao
// 3) o ponteiro para a funcao que executa o comando
// 4) o nome do comando
// 5) a string de ajuda do comando
#define XMACRO_COMMAND_LIST                                                        \
    X(0, CMD_APAGAR,   cmd_apagar_func,    "apagar",   "Apaga a flash")            \
    X(1, CMD_ESCREVER, cmd_escrever_func,  "escrever", "Escreve na flash")         \
    X(2, CMD_IMAGEM,   cmd_imagem_func,    "imagem",   "Gera uma imagem da flash") \
    X(3, CMD_INVALIDO, cmd_invalido_func,  "",         "")

// Enumeracao usada no programa, gerando:
//
// enum cmd_e
// {
//     CMD_APAGAR = 0,
//     CMD_ESCREVER = 1,
//     CMD_IMAGEM = 2,
//     CMD_INVALIDO = 3,
// };
enum cmd_e
{
#define X(CMD_IDX, CMD_ENUM, CMD_FUNC, CMD_NAME, CMD_HELP) CMD_ENUM = CMD_IDX,
	XMACRO_COMMAND_LIST
#undef X
};

// Lista com nomes de comandos, gerando:
//
// static const char *cmd_names[] =
// {
//     "apagar",
//     "escrever",
//     "imagem",
//     "",
// };
static const char *cmd_names[] =
{
#define X(CMD_IDX, CMD_ENUM, CMD_FUNC, CMD_NAME, CMD_HELP) CMD_NAME,
	XMACRO_COMMAND_LIST
#undef X
};

// Lista com as ajudas dos comandos, gerando:
//
// static const char *cmd_help_msgs[] =
// {
//     "Apaga a flash",
//     "Escreve na flash",
//     "Gera uma imagem da flash",
//     "",
// };
static const char *cmd_help_msgs[] =
{
#define X(CMD_IDX, CMD_ENUM, CMD_FUNC, CMD_NAME, CMD_HELP) CMD_HELP,
	XMACRO_COMMAND_LIST
#undef X
};

// Um tipo apenas para facilitar apontar para funcoes "void f(void)""
typedef void (*cmd_func_t)(void);

// Essa foi pura preguica mesmo de criar varias funcoes. Admito.
// Mas eh legal! Gera uma funcao para cada comando, como a seguir:
//
// void cmd_apagar_func(void) {
//     printf("%s: %s\n",cmd_names[CMD_APAGAR],cmd_help_msgs[CMD_APAGAR]); \
// }
//
// void cmd_escrever_func(void) {
//     printf("%s: %s\n",cmd_names[CMD_ESCREVER],cmd_help_msgs[CMD_ESCREVER]); \
// }
// ...
#define X(CMD_IDX, CMD_ENUM, CMD_FUNC, CMD_NAME, CMD_HELP) \
    void CMD_FUNC(void) { \
        printf("%s: %s\n",cmd_names[CMD_IDX],cmd_help_msgs[CMD_IDX]); \
    }
	XMACRO_COMMAND_LIST
#undef X

// Finalmente, a lista de ponteiros para funcoes eh gerada:
//
// cmd_func_t cmd_funcs[] =
// {
//     cmd_apagar_func,
//     cmd_escrever_func,
//     cmd_imagem_func,
//     cmd_invalido_func,
// };
cmd_func_t cmd_funcs[] =
{
#define X(CMD_IDX, CMD_ENUM, CMD_FUNC, CMD_NAME, CMD_HELP) CMD_FUNC,
	XMACRO_COMMAND_LIST
#undef X
};

int main(void)
{
    int n;
    char typed_cmd[32];

    printf("Digite o seu comando.\n");

    while(true)
    {
        if(scanf("%31s",typed_cmd) == 1)
        {
            for(n = 0 ; n < CMD_INVALIDO ; n++)
            {
                if(strcmp(cmd_names[n],typed_cmd) == 0)
                    cmd_funcs[n]();
            }
        }
        fflush(stdin);
    }

    return 0;
}

Autor: Marcelo Barros

Dispositivos embarcados, Linux, eletrônica, programação.

Deixe um comentário