Bizarro… mas muito útil!

O artigo do Miguel sobre perólas do mundo Java me fez lembrar de um trecho de código com o qual me deparei há um tempo atrás que me parecia também muito bizarro… até eu sacar um pouco depois a imensa utilidade dele!

O código, em linguagem C, era algo como o seguinte:

do {
  __alguma_coisa;
} while (false);

Em princípio fazer uma construção de laço para executar algo apenas uma vez parece algo bem bizarro, não?! Diria que é quase um primo-irmão do if (liberado==true). Foi exatamente o que eu achei. Mas colocando esta construção dentro do contexto em que eu a achei eu consegui ver (depois de alguma pesquisa, é claro) que ela é extremamente útil em alguns casos.

A primeira vez que eu me deparei com esta construção eu estava analisando o código-fonte do GNUChess, um jogo de xadrez de código livre. Como eu sei que o pessoal que escreve código livre algumas vezes utiliza umas construções bem diferentes do que normalmente estamos acostumados, resolvi investigar o porquê deles estarem utilizando esta construção. Afinal de contas, se este código é livre e várias pessoas já devem tê-lo revisado, não havia motivo para um laço aparentemente tão inútil estar lá sem alguma utilidade verdadeira, não é mesmo?!

E foi só pesquisar um pouco sobre o assunto para enteder melhor o que estava acontecendo. As construções como a acima só apareciam em definições de macros. Nunca em código “normal”. Para quem talvez não tenha muita familiaridade com macros em C, elas são basicamente códigos que você define e que serão literalmente expandidos pelo compilador onde seu identificador aparecer no código. Ou seja, um código assim:

#define macro_exemplo printf("Macro de exemplo!\n");
int main(void) {
  macro_exemplo
  return 0;
}

seria entendido pelo compilador, após o processamento das macros, da seguinte forma:

int main(void) {
  printf("Macro de exemplo!\n");
  return 0;
}

Ou seja, o preprocessador de código do compilador substitui a referência à macro_exemplo pela sua definição literal. No caso do nosso código bizarro, teríamos que definir a macro da seguinte forma:

#define macro_exemplo2 do { __alguma_coisa; } while (false);

ou ainda:

#define macro_exemplo3 do { \
                         __alguma_coisa; \
                       } while (false);

Note que uma macro deve ser definida sem quebras de linhas ou com quebras de linhas sinalizadas pelo caracter de escape barra invertida (’\').

Mas, ainda assim, não haveria motivo para o laço na minha macro, certo? Bom, se você pensar na macro como um código por si só, talvez isto esteja certo, mas não podemos esquecer que este código será copiado literalmente para algum outro lugar sempre que o identificador da macro for referenciado pelo programa. E muitas vezes não queremos fazer macros simples como as que eu mostrei no meu exemplo. Por exemplo, podemos querer fazer algo mais complexo como o seguinte:

#define IMPRIME_ESPACOS(n) for (int j = 0; j < n; j++) printf(" ");
#define ROTULO(rot) printf("L%d:", rot); do { int n = numeroDigitos(rot); IMPRIME_ESPACOS(6 - (n + 2)) } while (false);

A macro IMPRIME_ESPACOS(n) imprime uma quantidade de espaços em branco representadas pelo “argumento” n. Estou usando argumento entre aspas porque o argumento de uma macro em C se comporta de maneira diferente de um argumento de função, já que não há checagem de tipo e sim uma simples substituição do valor passado como argumento da macro nas suas referências dentro da própria macro. Já a macro ROTULO(rot) recebe como “argumento” um rótulo e imprime Lrot seguido de um numero de espaços de tal forma que todas as linhas que chamarem a macro ROTULO(rot) estarão identadas na mesma posição (desde que o rótulo tenha menos de 6 caracteres, que era o limite utilizado no programa a partir do qual tirei o código de exemplo).

Alguém certamente já bateu o olho no código acima e deve ter dito: poxa, eu poderia chamar a função
numeroDigitos(rot) dentro de IMPRIME_ESPACOS na definição da macro ROTULO. Não podemos nos esquecer no entanto que macros não são chamadas de função e que onde se lê IMPRIME_ESPACOS em ROTULO dever-se-ia ler na verdade a definição de IMPRIME_ESPACOS. Neste caso, se passássemos uma chamada de numeroDigitos como “argumento” de IMPRIME_ESPACOS, a função numeroDigitos seria chamada dentro do loop definido em IMPRIME_ESPACOS sempre que a condição de parada do loop fosse ser calculada. Ou seja, teríamos um número excessivo e desnecessário de chamadas a numeroDigitos. Para solucionar este eventual problema, decidi criar uma variavel para armazenar o valor da chamada de numeroDigitos e passar esta variavel como argumento para IMPRIME_ESPACOS. O problema é que dependendo de onde este código for ser substituído pode não ser permitido ter declarações de variáveis.

Neste último caso (e em muitos outros), o código mostrado no início deste post, por mais bizarro que possa parecer a primeira vista, pode ser a solução, pois o uso do do { ... } while (false); torna possível a criação de um contexto local dentro das chaves do falso loop na macro. Dentro deste contexto é possível criar-se quantas variáveis se queira ou, por exemplo, encadear comandos dentro de uma macro que não ficarão perdidos se esta macro for utilizada dentro de um if-else.

Ah, e se alguém ficou curioso, procure na Internet: vocês verão muitos outros usos do do-while (false). Sinceramente, muitos deles, na minha opinião são dispensáveis e só servem para complicar código que poderia ser escrito de outra forma. E, pra falar a verdade, a única construção que eu já achei em códigos livres foi esta que eu cito aqui, dentro de macros. É preciso ter dicernimento na hora de usar este tipo de construção. Aliás, para falar a verdade, mesmo este caso que eu descrevi aqui, que eu acho interessante e útil eu só usária em últimos casos em códigos profissionais.

Tabus e baixarias

Nas últimas semanas tenho trabalhado em um projeto de integração entre um sistema em C e um sistema em Java: o sistema original, legado, é completamente em C, e a equipe na qual eu trabalho está reescrevendo algumas partes em Java, e eu tenho que fazer o sistema antigo conversar com o novo. O sistema em C utiliza um sistema de comunicação interna que consiste basicamente em serializar bytes e mandar via UDP. Isso torna o processo bem rápido e eficiente, mas torna a comunicação entre plataformas bem complicada, por causa de problemas como alinhamento de bytes, padding, little endian, big endian, ponto flutuante, e por aí vai.

Por isso tive que rever vários conceitos, além de relembrar várias coisas em C. Não trabalhava com C desde 2002, quando participei do projeto AURORA, organizado pelo CenPRA. O objetivo do Aurora era de construir um dirigível capaz de voar sozinho, usando dados de sensores, GPS e de câmeras de vídeo. Minha missão dentro do projeto foi de desenvolver a infraestrutura para captação de imagens através de barramento IEEE 1394 (firewire), processamento e extração de certos parâmetros em tempo real e envio de alguns frames para uma estação de controle em terra. Foi C na veia, e ainda tive que estudar IEEE1394, drivers para linux, formatos de vídeo, processamento de imagens, envio de pacotes via UDP, e muitos outros conceitos básicos. Me diverti muito nesta época.

Voltando ao projeto atual, uma das tarefas necessárias era de receber um fluxo de bytes e extrair informações seguindo um descritor da mensagem. Como meus campos de informação dentro do fluxo de bytes tinham posições e tamanhos pouco convencionais (12 bits, 3 bits começando no bit 4 do byte 2, e por aí vai), não consegui encontrar nenhuma lib que resolvesse meu problema de forma adequada (em particular, nenhuma lib de extração de dados trabalha corretamente com campos de bits). Por isso resolvi extrair os bits na mão, escrevendo algumas operações binárias: o resultado final ficou bastante bom e conciso. Mas várias pessoas me perguntaram porque eu fiz a loucura de implementar isso na mão. Segundo eles, deveria ter pesquisado mais, porque provavelmente algum engenheiro da Sun havia feito algo melhor e mais robusto.

Concordo que seja possível que eu não tenha procurado o suficiente. Mas escrever algumas poucas linhas de operações binárias não me parece ser tarefa do outro mundo. Talvez eu seja arrogante o suficiente para me achar melhor do que os engenheiros da Sun (ou pelo menos tão competente quanto). Mas talvez também os programadores estejam criando tabus em relação a alguns tópicos de computação, chamados por muitos de baixarias.

A evolução na computação se faz por meio de criação de camadas de abstração, que nos permitem trabalhar sem nos preocuparmos com certos problemas. Hoje em dia, quem programa em Java, Python, Ruby e outras linguagens similares não se preocupa mais em gerenciar memória, em perder ponteiros. ORMs permitem que a base de dados seja vista como um conjunto de objetos. RMI e RPC enviam objetos e estruturas de dados de uma máquina para a outra. E acreditem: eu adoro estas abstrações! Adoro trabalhar com dicionários, listas e tuplas em Python, adoro não ter que gerenciar ponteiros em Java. Mas duas ressalvas precisam ser feitas.

A primeira já foi feita pelo Joel, no artigo The Law of Leaky Abstractions: estas abstrações são ótimas, mas não são perfeitas e muitas vezes falham. E quando falham, é preciso conhecer os fundamentos daquilo que está por baixo. Quando sistemas de ORM retornam resultados esdrúxulos, é preciso entender que o sistema subjacente não trabalha com objetos, mas com tabelas. Quando struts não da conta de tratar uma requisição, é preciso entender que no fundo no fundo, um form é uma string de requisição HTTP. Pode parecer bobo, mas é incrível perceber a quantidade de programadores que simplesmente não entendem o que se passa por debaixo do pano. Tudo é mágico.

A segunda ressalva é o ponto que eu quero ressaltar neste texto: estas camadas de abstração criam tabus. Hoje as pessoas tem receio de escovar bits, ou processar um UDP na mão. Muito em breve, SQL será algo de outro mundo, uma vez que Hibernate, RoR e Django permitem que objetos sejam criados de forma direta. HTTP e CGI já são conceitos do passado: hoje temos forms, event handlers e componentes. Para tudo, se busca um framework, uma lib. Para tudo, se acha que o que é feito pelos engenheiros da Sun é melhor. Eu tive esta nítida impressão conversando com um colega de trabalho que já tem muiiiiitos anos de estrada. Ele tem uma boa experiência em programação de microcontroladores, e ele é da época em que C era novidade no mercado. E segundo ele, mexer com bits era algo trivial e corriqueiro naquela época.

Que fique bem claro: eu adoro não ter que mexer nestas coisas. Mas sei que problemas deste tipo podem aparecer na nossa frente de vez em quando, e quando aparecerem nós temos que poder resolve-los. E para isso, não podemos ter medo de passar por cima das abstrações. Por isso, eu defendo que todo programador deveria mexer em algum projetinho em C de tempos em tempos (um ou dois meses a cada 5 anos por exemplo). Isso já seria suficiente para lembrar que o mundo computacional não é tão alto nível quando gostaríamos. E o prazer de voltar para nossas caixas pretas é incomensurável.

Lendas urbanas: custo de alocação em Java

Segundo este artigo da IBM, estudos mostram que o tempo de alocação de um objeto em Java, utilizando a palavra chave new, é menor que o custo de um malloc em C. Os dados são interessantes: um new requer 10 instruções de máquina, enquanto que um malloc requer entre entre 60 e 100 instruções. Outro dado interessante do artigo é que o uso do Garbage Collector tornaria o processo mais eficiente do que o gerenciamento manual de memória, pelo fato do primeiro tratar de blocos maiores.

Meus comentários a respeito:

  1. A performance de Java melhorou muito ao longo do tempo. Mas o footprint da JVM, para qualquer programinha, ainda é muito grande. Alocação de objetos em Java pode ser mais rápido do que em C/C++, mas com certeza programas em C/C++ fazem um uso mais racional de memória.
  2. Esta área de otimização de memória, footprint, execução é a área de aprimoramento que a Sun deveria se concentrar exclusivamente, em vez de ficar inventando moda com closures, Java FX e outras coisas script-like. Java não vai conseguir concorrer com linguagens de script. Java tem que concorrer com C++.
  3. A minha experiência com alocação de grandes quantidades de dados em Java, aplicada ao processamento de sequências genéticas, não foi das melhores.

Quebra-cabeça em C

Segue uma lista de perguntas-pegadinhas sobre C.

http://www.gowrikumar.com/c/index.html

Útil para amantes de problemas de computação e programação em geral, e essencial para candidatos a grandes empresas de software multinacionais que prezam a qualidade de seus programadores.