As 20 linhas da vergonha

Enquanto o arroz vai cozinhado, lembrei-me de um caso curioso que aconteceu comigo no início da carreira, envolvendo gerenciamento de memória em C++. Naquela época, eu era muito mais. Mais jovem, mais rápido, mais arrogante e mais newbie...

Um colega de trabalho mais experiente estava explicando para um outro colega menos experiente que para cada new deve haver um respectivo delete. Caso contrário o objeto persistirá em memória podendo ocorrer memory leaks.

Nesse momento eu o interrompi e disse que "não necessariamente, pois basta que o objeto saia de escopo para que o destrutor seja chamado automaticamente.".

A partir daí rolou uns 10 min de debate com argumentos e contra-argumentos, tendo toda a equipe parada assistindo. No final, metade da equipe concordava com ele e a outra metade comigo, e naquele momento ninguém tinha na cabeça uma forma de checar pra ver. Olhar na internet não era uma opção.

Como eu tinha todas as respostas, afinal já programava em PHP, Java e C++ fiquei de demonstrar posteriormente que eu estava certo.

Dias depois, num daqueles momentos filosóficos, lembrei que um endereço na memória é um número, e que é possível tanto converter um endereço para um número comum, quanto fazer o caminho inverso, embora a segunda opção normalmente seja inútil.

Bolei então uma forma de provar a minha teoria: Criaria um objeto num escopo restrito alocando com new. Guardaria o endereço desse objeto como um número comum num long fora do escopo restrito, esperaria o escopo restrito finalizar, o destrutor do obejto ser chamado e então, no escopo externo, usaria o número que representava o endereço do objeto para acessar aquela área de memória mostrando que receberia um SIGSEGV (segmentation fault) por tentar acessar uma área de memória inválida. Gerei então o código abaixo.

#include 
#include 

using namespace std;

int main(int argc, char** argv) {
    unsigned long num = 0;

    {
        string* str = new string("Hello World!!!");
        num = (unsigned long)str;

        cout << "str: " << str <<  endl;
        cout << "num: " << num <<  endl;
    }

    cout << *((string*)num) << endl;

    return 0;
}

Eis que para minha surpresa o programa NÃO explodiu na minha cara. Ele funcionava corretamente demonstrando que na verdade EU era quem estava errado.

Pesquisando mais constatei que sim, para cada new deve haver um e somente delete, e mais, para cada new[] deve haver um e somente um delete[]. O que eu achava que sabia sobre gerenciamento de memória e escopos, provavelmente era alguma confusão entre diferentes linguagens.

Quando eu iniciei a discussão dias antes, eu cometi todos os erros básicos que um newbie comete:

  1. Se meter onde não foi chamado.
  2. Jurar de pé junto que está certo sem ter provas.
  3. Subestimar o conhecimento de alguém mais experiente.
  4. Achar que tem todas as respostas só porque acabou de ver isso na aula.
  5. Achar que só porque conhece o básico de várias linguagens, então tem todas as respostas.

Como todo bom integrante da espécie dos Homo sapiens, falei merda. A minha reação a esta descoberta foi assumir para e equipe inteira que eu estava errado, pedir desculpas por ter falado besteira e mostrar essas vinte linhas de código que usei para demonstrar isso. Além disso, mostrei todas as fontes que pesquisei, e ainda trouxe à tona outros detalhes sobre gerenciamento de memória que nos estavam escapando, como referências circulares. Isso levou até a um re-desing de algumas partes do projeto.

As minhas 20 linhas de cógigo ficaram conhecidas como "As 20 linhas da vergonha" e natualmente eu fui sacaneado por isso. Cada vez que alguém tinha alguma dúvida falavam "faz aí as 20 linhas da vegonha pra testar". O significado era mais ou menos como "faz aí um teste simples pra não passar vergonha depois".

O mais importante é que a minha atitude pós-cagada foi bem aceita e trouxe benefícios para a equipe. Se eu tivesse me esquivado, mais cedo ou mais tarde alguém ia provar que eu estava errado na frente de todo mundo, ou pior, nos bastidores. Admitir o erro e aprender com isso mostrou que eu estava preparado para ser contestado. Adicionalmente isso mostrou para equipe que por mais que eu parecesse arrogante (eu pareço mais do que sou...), eu sabia que era um humano comum, que errava como qualquer um, não me achando superior a ninguém. Isso abriu caminho para que outros membros da equipe se sentissem à vontade para fazer o mesmo e nós nos tornamos uma equipe ainda mais coesa.

O monge e o macaco

Era uma vez um rapaz que gostava de programar. Ele descobriu a programação por acaso e se encantou com a idéia de transformar as idéias na sua cabeça em software para que outras pessoas pudessem usar. Então ele começou uma jornada em busca da linguagem que fosse capaz de expressar exatamente o que ele pensava.

Ele começou com Pascal e aprendeu que com ela poderia iterar e recursar, modularizar e estruturar. E viu que isso era bom, e estudou mais e mais, e naquela época ele foi feliz. Porém o excesso de restrições começou a travar o seu pensamento e nessa época ele foi infeliz.

Então ele decidiu que queria algo que melhor e procurou Java. E então ele aprendeu que poderia orientar a objeto sem se preocupar com a coleta de lixo. E viu que isso era bom e nessa época ele foi feliz. Mas chegou um dia que o excesso de camadas começou a travar o seu pensamento e nessa época ele foi infeliz.

Então ele decidiu que se quisesse ser um programador de vedade teria que aprender C e Assembly. Ele estudou e estudou, e descobriu que podia fazer tudo que sua mente imaginava, inclusive besteiras. E nessa época ele foi feliz. Ele tinha para si todo o controle da máquina e assumiu a responsabilidade sobre esse poder. Ele percebeu que poderia ter o que tinha em Pascal, com C e ele foi feliz. Ele percebeu que poderia ter o que tinha em Java, com C++ e ele foi feliz. E ele percebeu que poderia ter o que quisesse se estivesse disposto a pagar o preço em Assembly, e ele foi muito feliz.

Mas algo faltava que ainda o incomodava e ele não sabia explicar o porquê. Tentou escrever um programa que explicasse o que faltava, mas não conseguiu resolver. Então, nessa época, ele foi infeliz. Ele se desesperou, e procurou apender todas as linguagens do mundo, mas nenhuma era suficiente para explicar o que estava faltando. Tentou VB, C#, Python, Lisp e nada. Nem mesmo Brainfuck conseguia explicar o que faltava. E ele era infeliz, e até a sua infelicidade era incompleta. Ele pensou em desistir, mas até esse pensamento era incompleto.

Então um dia ele conheceu um monge e esse monge lhe apresentou uma nova linguagem não muito conhecida. Ele viu mas não ficou interessado, pois nenhuma linguagem até ali havia mostrado o que lhe faltava. Mas o monge insistiu e ele aceitou, e então ele começou a estudar Perl.

Então ele viu que a linguagem era cheia de símbolos e construções estranhas e aquilo não era bom. Era feio e esquisito, e ele continuava sem saber o que lhe faltava. Sua mente já estava cansada e seus pensamentos minguavam, mas algo o impelia a continuar e estudar.

Um dia, ele não sabe qual, aconteceu algo e ele não soube o quê. Veio um estalo e tudo se tornou claro. Seu mundo de sombras teve luz. E ele não sabia explicar o que era, mas isso era bom e ele foi feliz.

Então ele procurou fazer um retrospecto de seu caminho até ali para descobrir o que lhe acontecera. Ele olhou para si e descobriu que podia ver coisas que não via antes. Ele olhou para Pascal e resolveu problemas que não tinham solução naquela época. Ele olhou para Java e entendeu quais eram as camadas necessárias e quais eram as barreiras. Ele olhou para C e percebeu que a linguagem era tão poderosa que ele deixou-a amarrar suas mãos e prender sua mente, e ele entendeu como utilizar por completo esse poder a seu favor. Ele olhou para VB, C# e todo o .NET e aceitou que era melhor mesmo deixar pra lá... Então ele olhou para Lisp e viu como realmente eram escritas as linhas de código do universo, e que tinha Perl colando as coisas.

Então ele olhou para Perl, e notou que os símbolos estranhos eram só atalhos para abstrações e que a linguagem era muito mais que isso. Era grande, poderosa, flexível e bela, muito bela.

E foi aí que ele descobriu o que lhe faltava, e isso não era uma linguagem de programação. Perl lhe trouxe uma nova forma de olhar para o mundo e perceber coisas que antes ele não percebia. Perl o iluminou e libertou sua mente, e ele entendeu que até ali tinha sido apenas um macaco, repetindo comandos e algoritmos sem entender o seu real significado. Não importava qual era a linguagem, ele era somente um macaco, e antes desse estalo, mesmo com Perl ele não compreendia. Mesmo com Perl ele continuava a ser um macaco, e teria sido assim se ele não tivesse compreendido. Não importaria qual a nova linguagem, ele continuaria a ser um macaco.

Ele percebeu que sua mente estava escravizada por mecanismos que a obrigavam a não pensar, e quanto mais linguagens ele aprendia, mais e mais mecanismos apareciam para oprimir e degradar seu pensamento.

Então agora finalmente ele havia descoberto que o que lhe faltava não era uma tecnologia, mas sim era libertar a sua mente. Ele tornou-se um monge. E ele se libertou dos mecanismos de opressão, e todas as linguagens que ele conhecia passaram a se completar e fazer sentido. Ele não mais programava, mas falava Perl, então ele foi capaz de expressar completamente seus pensamentos em qualquer linguagem, e assim ele foi finalmente feliz.

Como escrever uma redação aleatória

Escrever bem é uma tarefa que exige talento, planejamento e tempo, no entanto diversas entrevistas de emprego exigem que o pobre candidato construa uma redação de X linhas em Y tempo. Isso às vezes gera resultados catastróficos.

A primeira etapa envolve a escolha de um tema. Um candidato paranóico sempre faz uma pesquisa sobre a empresa e se prepara para escolher um tema que cative o entrevistador.

Depois do tema, desenvolve-se uma tese escolhendo um determinado ponto de vista. Alguns minutos já são suficientes para pensar em introdução, argumentos e conclusão.

Uma introdução de quatro ou cinco linhas, seguida por pelo menos dois parágrafos de idêntico tamanho já gera texto suficiente para abordar o tema e defender a tese. Um bom espaçamento também ajuda a aumentar o número de linhas.

Redações de entrevistas não precisam primar pela qualidade da abordagem do assunto. O importante é demonstrar clareza e preencher no mínimo vinte linhas, pois na maioria das vezes o texto só servirá para o exame grafológico, nem será lido.

Com um pouco de boa vontade e paciência, as linhas vão surgir na ponta do lápis.

Membros privados em estruturas C

Esta semana, lá no trabalho tive mais uma prova de que paradigma de programação é algo completamente independente de linguagem, ou seja, não é pelo fato de você estar programando em C++, compilando com o g++ que o seu código vai ser orientado a objetos, tão pouco, se você programa em ANSI C o seu código obrigatoriamente vai ser estruturado ou você estará impedido de programar orientado a objetos.

Estou lendo certo, Orientação a Objetos em ANSI C?

Sim e não!

Coisas como herança, polimorfismo e sobrecarga são complicadas de fazer/emular em C, mas você pode programar utilizando um estilo que se comporte de forma semelhante à orientação a objetos. A libdfb é escrita em C mas "orientada a objetos", de forma que você cria, manipula de destrói elementos que se comportam de forma bem semelhante a objetos.

Aqui no trabalho, temos um sistema de abstração de hardware em C que se conecta com uma GUI em C++. A camada mais baixa, em C também foi escrita (e muito bem escrita, diga-se de passagem) com essas técnicas, simulando uma orientação a objetos. Uma dessas técnicas, me chamou atenção por usar uma daquelas notas de rodapé dos livros de C.

Ao compilar código em C, cada símbolo só tem visibilidade dentro da unidade de compilação na qual ele foi declarado, a menos que seja declarado novamente nas outras unidades como extern. Dessa forma, é possível "esconder" certos símbolos dentro de sua unidade de compilação, tornando-os inacessíveis ao mundo exterior. Temos com isso encapsulamento.

A unidade de compilação é o conjunto de arquivos que depois de pre-processados e compilados geram um único código objeto. Basicamente (mas não exatamente), podemos tomar como unidade de compilação cada arquivo de implementação de código fonte (*.c, *.cpp. etc). Maiores detalhes sobre as etapas de compilação em C e C++ podem ser encontrados no blog do Caloni.

Utilizando essas informações, podemos criar uma struct em ANSI C na qual os seus membros internos são "privados". A mágica está em aprisionar a definição dos membros dentro da unidade de compilação e criar métodos de acesso para esses membros. Para isso usamos as notas de rodapé que nos mostram a diferença entre declaração e definição de elementos em C:

Declaração ou manifesto: Apresenta ao compilador um identificador sem dizer muito sobre seu significado, ou seja, diz ao compilador que o identificador XXX existe, mas pouco se sabe sobre o que ele representa. Ex.:

extern int a;
void bla(void);
struct st_data;

Definição ou implementação: Diz ao compilador o que determinado identificador representa, como por exemplo quanto de memória deve ser alocada para ele e qual o endereço de memória onde podemos encontrá-lo, entre outras coisas. Ex.:

int a;
void bla(void) {/* Do anything.  */}
struct st_data {/* Some members. */}

Quando falamos de estruturas e tipos, sem a definição o compilador não tem como alocar memória para eles pois nada se sabe a respeito de quanto espaço uma variável daquele tipo precisa. Por outro lado, algumas vezes, sem a declaração, o linker não tem como saber que aquele símbolo existe.

Quando declaramos uma estrutura num cabeçalho, normalmente nós também definimos seus membros ali mesmo e toda vez que adicionamos esse cabeçalho a um fonte nosso, nós incluimos na unidade de compilação desse fonte tanto a declaração quanto a definição dessa estrutura, tornando os membros da estutura públicos à essa unidade de compilação. Isso nos permite acessar seus membros diretamente.

Se separarmos a declaração da definição, somente símbolo que representa a estrutura estará disponível, mas não os seus membros. Assim, qualquer tentativa de acesso direto a um membro, gerará um erro de compilação. Um efeito colateral interessante é que como o compilador nada sabe sobre o tamanho da estrutura, não será possível definir diretamente uma variável do tipo da estrutura, somente um ponteiro para ela, pois ponteiros têm todos o mesmo tamanho e o compilador precisa apenas do nome símbolo do tipo para criar o ponteiro.

Temos então o header mytype.h mais ou menos assim:

#ifndef MY_TYPE_H
#define MY_TYPE_H

/* O typedef é apenas pra não ficar repetindo a palavra struct. */
/* A declaração é somente o trecho:                             */
/* struct _mytype                                               */
typedef struct _mytype my_type;

void create_my_type( my_type** );
void destroy_my_type( my_type** );

void set_data( my_type* , int );
int get_data( my_type* );
void set_text( my_type* , const char* );
char* get_text( my_type* );

#endif

A implementação mytype.c:

#include "mytype.h"

#include 
#include 
#include 

struct _mytype {        /* Aqui fica a definição da estrtura. */
    int data;           /* Somente depois disso é que o com-  */
    int text[21];       /* pilador vai saber como alocá-la.   */
};                      /* Tente um sizeof(my_type) no main.  */

void create_my_type( my_type** my_ptr )             { /* some code... */ }
void destroy_my_type( my_type** my_ptr )            { /* some code... */ }
void set_data( my_type* my_ptr , int d )            { /* some code... */ }
int get_data( my_type* my_ptr )                     { /* some code... */ }
void set_text( my_type* my_ptr , const char* text ) { /* some code... */ }
char* get_text( my_type* my_ptr )                   { /* some code... */ }

O código fonte completo do exemplo pode ser encontrado aqui.

Como não é possível criar diretamente variáveis desse tipo, precisamos definir um "construtor" e um "destrutor".

Tentativas de acesso direto a membros geram erro de compilação:

user@host:~/private-struct-members$ gcc -o teste mytype.h mytype.c main.c
main.c: Na função ‘main’:
main.c:22: erro: dereferencing pointer to incomplete type
user@host:~/private-struct-members$

Dessa forma, com um pouco de criatividade e tendo os conceitos tanto da linguagem quanto dos paradigmas, é possível implementar códigos realmente interessantes. Neste exemplo bobo talvez não tenha ficado clara a utilidade de forçar uma emulação de encapsulamento ou o uso de construtor/destrutor em C, mas em sistemas onde as circunstâncias não permitem um C++, ou que a complexidade tenda a atingir níveis críticos, essas técnicas se mostram de grande valia. No nosso caso, essa técnica especificamente, permitiu que um programador experiente, que não participou do projeto todo, descobrisse que sua tentativa de acesso direto a um membro de uma estrutura, estava contextualmente inadequada. Sem isso, um bug cabuloso de lógica iria aparecer somente em tempo de execução, provavelmente fazendo o software explodir na cara do cliente.

Links úteis (ou não...): http://www.directfb.org http://www.caloni.com.br http://www.numaboa.com.br/informatica/c/ Livro Desenvolvimento do Kernel do Linux

Encontro Técnico da rio.pm

No dia 26 de abril de 2008 a partir das 10:00 hs, no sexto andar da UERJ, será realizado o próximo encontro técnico da rio.pm. Teremos palestras e oficinas abordando Perl e Smalltalk entre outros assuntos, e ainda contaremos com a presença do Randal Schwartz, figurinha carimbada dos nossos ES. Entrada franca. Você pode conferir a grade de palestras através desse link. As dúvidas podem ser respondidas na nossa Lista de Discussão. O endereço completo da UERJ é: Campus Francisco Negrão de Lima - Maracanã Endereço: Rua São Francisco Xavier, 524 - Maracanã - Cep: 20550-013 Telefone Geral: (21) 2587-7100