Ponteiros!

2010 July 4
by Pedro Kiefer

Outro dia no trabalho resolvemos brincar com ponteiros, não que o código que resultou da brincadeira seja algo bom, mas foi divertido. Basicamente tinhamos uma função que traduzia uma string interna passada para a string que utilizariamos em tela, software foi crescendo e mais traduções eram necessárias para a mesma string.

Então aos exemplos, do que tinhamos, da brincadeira que fizemos com os ponteiros e o que seria a melhor solução para o código. Antes algumas definições, a estrutura que contém as strings é a seguinte, basicamente um mapa 1 pra n:

enum {
	KERNEL = 1,
	WEB,
	CLI
};

struct test {
	int idx;
	char *kernel;
	char *web;
	char *cli;
};

struct test example[] = {
	{.idx = 0, .kernel = "eth0", .web = "Ethernet 0", .cli = "ethernet0" },
	{.idx = 1, .kernel = "eth1", .web = "Ethernet 1", .cli = "ethernet1" },
	{.idx = 2, .kernel = "eth2", .web = "Ethernet 2", .cli = "ethernet2" },
	{.idx = 3, .kernel = NULL, .web = NULL, .cli = NULL}
};

A versão original da função que procurava pela string correta era algo parecido com:

char *get_str_for_v1(const char *eth, enum str_type type)
{
	char *rtn = NULL;
	int i;

	switch (type) {
	case KERNEL:
		for (i = 0; i < sizeof (example); i++) {
			if (strcmp(eth, example[i].kernel) == 0) {
				rtn = example[i].kernel;
				break;
			}
		}
		break;
	case WEB:
		for (i = 0; i < sizeof (example); i++) {
			if (strcmp(eth, example[i].kernel) == 0) {
				rtn = example[i].web;
				break;
			}
		}
		break;
	case CLI:
		for (i = 0; i < sizeof (example); i++) {
			if (strcmp(eth, example[i].kernel) == 0) {
				rtn = example[i].cli;
				break;
			}
		}
		break;
	}

	return rtn;
}

O que é obviamente muito feio! Com excesso de código repetido, péssimo de ler e dar manutenção. Vendo esse código propus o desafio de escrever a mesma função com um for só. Meu colega de trabalho resolveu usar ponteiros pra resolver isso, calculando o offset de cada um dos membros da estrutura e recuperando o ponteiro da string a partir do offset. Claro, isso parece muito simples de se fazer… mas na hora de escrever o código, demorou bem mais do que imaginavamos, mas foi bastante divertido entender como funcionam os casts em C.

O resultado foi o seguinte código, as macros eu adicionei para facilitar a leitura do código, mais sobre elas daqui um pouco…

#define member_offset(type, member) \
			((unsigned long)(&((type *)0)->member))
#define get_member_by_offset(ptr, type, offset) \
			((type *)((void *)(ptr) + offset))

char *get_str_for_pointer_fun(const char *eth, enum str_type type)
{
	int i;
	unsigned long offset;

	switch(type) {
	case KERNEL:
		offset = member_offset(struct test, kernel);
		break;
	case WEB:
		offset = member_offset(struct test, web);
		break;
	case CLI:
		offset = member_offset(struct test, cli);
		break;
	}

	for (i = 0; i < sizeof (example); i++) {
		if (strcmp(eth, example[i].kernel) == 0) {
			return *get_member_by_offset(&example[i], char *, offset);
		}
	}

	return NULL;
}

Com certeza não é a melhor implementação possível, talvez seja a mais complexa de se entender, especialmente se não for utilizada uma macro para esconder a implementação. Mas é uma implementação divertida, ou pelo menos foi divertido tentar achar a combinação correta de ponteiros para gerá-la.

E finalmente o código que eu julgo o mais aceitável:

char *get_str_for_v2(const char *eth, enum str_type type)
{
	int i;

	for (i = 0; i < sizeof (example); i++) {
		if (strcmp(eth, example[i].kernel) == 0) {
			switch (type) {
			case KERNEL:
				return example[i].kernel;
			case WEB:
				return example[i].web;
			case CLI:
				return example[i].cli;
			}
		}
	}

	return NULL;
}

Macros

Bom, disse que ia explicar o funcionamento das duas macros utilizadas, então recapitulando elas:

#define member_offset(type, member) \
			((unsigned long)(&((type *)0)->member))

#define get_member_by_offset(ptr, type, offset) \
			((type *)((void *)(ptr) + offset))

A macro member_offset calcula o offset de um membro qualquer de um estrutura. Para fazer isso, ela dá um cast para um ponteiro do tipo da estrutura requerida ao valor 0. O que parece estranho a princípio, mas faz todo o sentido! Do ponteiro para zero até o membro requerido da estrutura existem os n bytes que representam o offset em memória do início da estrutura até o membro.

Munido do valor do offset, podemos então recuperar a estrutura que se encontra naquele offset utilizando a macro get_member_by_offset. Obviamente é necessário sabermos qual o tipo de dado que se encontra naquele offset! Então a partir do ponteiro (endereço) da estrura que queremos acessar podemos somar o offset do membro requerido. Mas só somar o offset não resolve, é necessário dar os casts apropriados.

Como queremos um que o endereço calculado (ponteiro original + offset) seja um ponteiro, é necessário indicar isto ao compilador, colocando um * no cast do cálculo. Além disso, é necessário indicar que tipo de ponteiro estamos recuperando naquele endereço, portanto o cast vira (type *). No exemplo, o cast é (char **) indicando corretamente que naquele endereço encontra-se uma string.

Esse mesmo método de recuperar um membro de uma estrutura é utilizado na implementação de listas no kernel do linux. A macro definida é um pouco diferente, uma vez que ela realiza estas duas operações de uma só vez, mas a explicação do seu funcionamento é a mesma! Segue a sua implementação:

#define list_entry(ptr, type, member) \
	((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

Basicamente o que este código faz é recuperar o ponteiro para a estrutura a partir de um membro qualquer dela. No caso das listas, o membro utilizado para recuperar a estrutura original é o struct list_head. Com isso, é possível utilizar diversos struct list_head numa mesma estrutura, adicionando ou não a estrutura alocada a uma das listas.

No comments yet

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS

Bad Behavior has blocked 63 access attempts in the last 7 days.