Tratamento Dinâmico de Mensagens
Todos os dispositivos eletrônicos que precisam comunicar-se com outros necessitam incorporar em seu código rotinas para o tratamento de mensagens recebidas e enviadas. Neste artigo nós iremos abordar um exemplo de tratamento dinâmico para handlers de mensagens, que oferece algumas vantagens (e desvantagens) sob a tradicional forma utilizada, o bloco switch - case.
Todos os dispositivos eletrônicos que precisam comunicar-se com
outros necessitam incorporar em seu código rotinas para o tratamento de
mensagens recebidas e enviadas. Neste artigo nós iremos abordar um
exemplo de tratamento dinâmico para handlers de mensagens, que oferece
algumas vantagens (e desvantagens) sob a tradicional forma utilizada, o
bloco switch - case.
O switch-case usualmente utilizado para o tratamento de ID de
mensagens e chamada de handlers é simples e rápido. Por muitas vezes
ele continuará sendo a melhor opção para o uso no tratamento de
mensagens por essas duas características, mas em alguns casos este
bloco pode tornar-se deselegante ou até mesmo ter o seu uso
inviabilizado em sua maneira tradicional. A seguir nós veremos alguns
trechos de código exemplificando a abordagem típica e, em seguida, uma
proposta para o tratamento dinâmico de mensagens. Em todos os exemplos
foi utilizado o compilador C HI-TEC para microcontroladores PIC; porém
qualquer compilador C ANSI deverá comporta-se de acordo.
Este é um exemplo clássico de uso de um bloco switch-case para o tratamento de uma mensagem id informada:
1: void trataId( char id ) {
2: switch ( id ) {
3: case 1:
4: trataId1();
5: return;
6: case 2:
7: trataId2();
8: return;
9: case 3:
10: trataId3();
11: return;
12: case 4:
13: trataId4();
14: return;
15: case 5:
16: trataId5();
17: return;
18: case 6:
19: trataId6();
20: return;
21: case 7:
22: trataId7();
23: return;
24: }
25: }
Exemplo 1
Neste trecho nós analizamos o tratamento de 7 mensagens, com seus IDs posicionados no intervalo de 1 a 7. Para cada um destes IDs nós possuímos uma função de tratamento, que executa todas as operações referidas ao respectivo ID. A primeira característica citada como vantagem deste bloco, a simplicidade, é facilmente percebida ao visualizar o código. A segunda, velocidade, é menos perceptível à primeira impressão mas analizando-se a listagem em assembly ou o tempo de execução deste techo de código isto fica bastante claro.
Porém, nós podemos começar à vislumbrar as desvantagens imaginando o mesmo bloco acima contendo 20 ou 30 ID's de mensagens. Certamente este código começaria a tornar-se um tanto deselegante de se ver, prejudicando a legibilidade. Como isso não é um fator crítico na escrita do software (apesar de determinante, em minha opinião), podemos pensar também na segunda característica desfavorável: a flexibilidade. Em C, qualquer rótulo case precisam ser uma constante inteira. Sendo assim, as opções do bloco acima, ou um semelhante, estão presas ao que foi previamente definido na escrita. Não é possível alterar de maneira usual um destes valores; por exemplo, no código acima alterar a função handler do id 7 durante a execução do programa. Também não seria possível adicionar um novo id/handler dinamicamente, o que pode ser útil - e por vezes necessário - em determinadas aplicações.
Uma solução para tentar contornar estes dois problemas é sugerida abaixo:
1: /* Número máximo de ID's no vetor */
2: #define MAX_ID_NUMBER 10
3: /* Declara o vetor que armazena os Id's e handlers */
4: int idHandler[ MAX_ID_NUMBER ];
5: /* Função que registra o ID e o seu handler de tratamento */
6: void registraId( unsigned char id, void * handler ) {
7: idHandler[ id ] = (int) handler;
8: }
9: /* Função de tratamento dos IDs */
10: void trataId( unsigned char id ) {
11: /* Declarando o ponteiro para função */
12: void (*funcaoHandler)( void );
13: /* Iniciando, fazendo cast */
14: funcaoHandler = (void(*)(void)) idHandler[ id ];
15: /* Evitando um handler para um endereço nulo */
16: if ( funcaoHandler == 0 ) return;
17: /* Executando handler */
18: (*funcaoHandler)();
19: }
Exemplo 2
Apesar de certamente mais complexo que o exemplo anterior, o funcionamento deste código não é complicado de ser entendido. Nas linhas 2 e 4 nós definimos um vetor de inteiros, chamado idHandler, que armazenará os IDs das mensagens e qual o seu respectivo handler. Neste ponto nós precisamos prever o número máximo de IDs diferentes que a aplicação vai suportar. Outras abordagens também podem ser utilizadas para permitir a utilização de um vetor dinâmico.
Na linha 6 nós temos a declaração da função registraId que é responsável por atribuir, alterar ou excluir um id à um handler especificado. Diferentemente do bloco do exemplo anterior, estes valores são dinâmicos e serão inicializados posteriormente. Nós chamamos de handler a função responsável por tratar cada uma das mensagens. A linha 7 simplesmente adiciona ao vetor, na posição id, o handler informado (endereço da função de tratamento), efetuando um cast para o tipo inteiro.
A linha 10 dá início a função trataId que efetua a mesma função da trataId utilizando o bloco switch-case. Ela é a responsável por chamar a função handler de tratamento de mensagens, a partir de um Id informado. Na linha 12 nós declaramos um ponteiro para função e, em seguida, na linha 14 o inicializamos com o valor do endereço do handler armazenado em idHandler na posição id, sendo o (void(*)(void)) apenas a forma tradicional de efetuar cast para um ponteiro de função void com parâmetro também void. A linha 16 simplesmente analiza o valor do endereço, e caso este seja 0x0000 retorna da função de tratamento sem nenhuma execução. Isso evita que o programa seja apontado para o primeiro endereço, o que poderia acontecer caso um id não atribuído à um handler fosse chamado, uma vez que o compilador C inicializa as suas variáveis inteiras com o valor zero. A linha 18 é a chamada da função handler propriamente dita, transferindo o fluxo de execução para a função determinada.
Antes de chamar a função trataId informando um id para tratamento, nesta abordagem será necessária a atribuição dos respectivos id/handlers. Um exemplo de registro para um comportamento semelhante ao do bloco switch-case é o seguinte:
registraId( 1, trataId1 );Este registro pode ser feito em tempo de execução (normalmente na inicialização) e qualquer dos handlers pode ser alterado apenas chamando a função registraId novamente. Por exemplo, para, a qualquer momento da execução, alterar o tratamento das mensagens com ID 6 para a função trataId3 bastaria executar a chamada registraId(6, trataId3). Para desvincular um handler de um ID bastaria informar um handler 0, uma vez que a função de tratamento não permite este endereço.
registraId( 2, trataId2 );
registraId( 3, trataId3 );
registraId( 4, trataId4 );
registraId( 5, trataId5 );
registraId( 6, trataId6 );
registraId( 7, trataId7 );
Depois de registrado, o tratamento das mensagens é executado apenas através da função trataId ( id ), assim como em nosso exemplo 1.
Os leitores mais atentos já devem ter observados que os IDs informados necessariamente precisam está dentro do intervaldo entre 0 e MAX_ID_NUMBER. Isso normalmente é simples de obter durante o projeto, mas nos casos em que este requisito inviabilize a solução a utilização de algoritmos de hash também poderão ser utilizados para aplicar um busca rápida do par Id/Handler em um vetor sem, necessariamente, IDs dentro de um intervalo muito restrito.
Com esta abordagem nós ganhamos dinamismo e legibilidade (principalmente com um grande número de IDs), mas certamente perdemos em eficiência. Este método é mais complexo (por oferecer mais flexibilidade), o que ocasiona uma maior utilização de memória - RAM e ROM - que nem sempre será algo que você terá disponível em um sistema embarcado. A utilização de qualquer método deverá ser cuidadosamente analisada, pensando os prós e os contras e escolhendo a solução que melhor se adeque ao seu projeto.
Roberto Alcântara Filho
roberto@eletronica.org
Exemplo demonstrativo:
/*
* handlerDemo.c
* Exemplo de como apontar dinamicamente handler's para o
* tratamento de mensagens.
*
* Este peça de software é parte do artigo escrito e
* disponibilizado em www.Eletronica.org.
* Roberto Alcântara */ /* Funcoes de tratamento dos ID's.*/ unsigned char foo = 0; void trataId1( void ) { foo += 1; } void trataId2( void ) { foo += 2; } void trataId3( void ) { foo += 3; } void trataId4( void ) { foo += 4; } void trataId5( void ) { foo += 5; } void trataId6( void ) { foo += 6; } void trataId7( void ) { foo += 7; } /* Número máximo de ID's no vetor */ #define MAX_ID_NUMBER 10 /* Declara o vetor que armazena os Id's e handlers */ int idHandler[ MAX_ID_NUMBER ]; /* Função que registra o ID e o seu handler de tratamento */ void registraId( unsigned char id, void * handler ) { idHandler[ id ] = (int) handler; } /* Função de tratamento dos IDs */ void trataId( unsigned char id ) { void (*funcaoHandler)( void ); //Declarando o ponteiro para função funcaoHandler = (void(*)(void)) idHandler[ id ]; //Iniciando, fazendo cast if ( funcaoHandler == 0 ) return; //Evitando um handler para um endereço nulo (*funcaoHandler)(); //Executando handler } main() { char conta; registraId( 1, trataId1 );
registraId( 2, trataId2 );
registraId( 3, trataId3 );
registraId( 4, trataId4 );
registraId( 5, trataId5 );
registraId( 6, trataId6 );
registraId( 7, trataId7 ); for ( conta=1; conta<=7; conta++ ) { trataId( conta ); } foo = 0; }

