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