Contenido

Buenas prácticas en Ansi C (2)

Tras comentar las buenas prácticas más básicas en Ansi C, veamos ahora las buenas prácticas cuando estamos haciendo una librería, ya sea estática o dinámica.

En concreto, me centraré en la librería estática y luego pasaré a dar algunas pautas para las dinámicas.

Ansi C

Encapsulación

El objetivo de lo que voy a exponer consiste en aislar en la medida de lo posible nuestra librería con el fin de que sea lo más reutilizable posible.

Veremos cómo proteger a nuestra librería del mal programador, así como al mal programador de nuestra librería :D

Hay que diferenciar aquí dos tipos de archivos de cabecera: los internos, utilizados por nuestros archivos .c, y los externos, que será la forma que tengan otros programas de utilizar nuestra librería.

Macros

Quedan completamente prohibidas las macros en los archivos de cabecera externos.

Hay distintas razones para esto: no forman parte como tales de nuestra librería, no son fácilmente mantenibles, y son más difíciles de depurar.

Enumerados

No es buena idea utilizar enumerados en los archivos de cabecera externos. Sinceramente, me gustaría decir lo contrario, pero dependiendo de las opciones de compilación podemos conseguir verdaderos poltergeist por culpa de los enumerados.

Por lo tanto, para las constantes es mejor utilizar #define.

En los internos dará igual, porque para cuando lleguen al usuario de nuestra librería ya están compilados.

Sólo lo indispensable

En los archivos de cabecera externos se pondrá exclusivamente lo indispensable:

  • Cuanta más funcionalidad exportemos, más tendremos que mantener.
  • Cuanto más complejo sea, más preguntas recibiremos.
  • Cuanto más sencillo sea, más feliz el usuario de la libreria.

Estructuras

Nunca deis la oportunidad al programador de poder acceder a una estructura de vuestra librería.

Para ello yo suelo utilizar la siguiente técnica: en las cabeceras externas no hay ninguna estructura, pero sí punteros a void; en las funciones que exporto, lo primero que hago es transformar los punteros a void a estructuras internas (que pueden estar en archivos de cabecera internos).

Veamos un ejemplo.

Archivo ejemplo.h:

1
2
3
4
5
6
// archivo de cabecera externo "ejemplo.h"

typedef ejemplo_t void;

ejemplo_t* ejemplo_new ();
void       ejemplo_destroy(ejemplo_t* t);

Archivo ejemplo.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include "ejemplo.h"

typedef struct {
  int a;
  char* b;
} ejemplo_tp;

ejemplo_t* ejemplo_new () {
  ejemplo_tp* result;

  result = calloc (1, sizeof(ejemplo_tp));
  // inicializar result
  return result;
}

void ejemplo_destroy(ejemplo_t* vejemplo) {
  ejemplo_tp* ejemplo = (ejemplo_tp*) vejemplo;

  // liberar variables internas de "ejemplo"
  free (ejemplo);
}

Aquí hay algunas convenciones de nombrado que yo suelo utilizar, aunque seguro que podéis encontrar otras mejores.

Lo primero, es decir que el _t de la estructura me hace referencia a “tipo”, con el fin de diferenciar lo que es un tipo de lo que es una variable. Se me hace muy raro ver cosas como:

1
2
3
ejemplo data; // raro. Además, ¿cómo llamas a la variable?
ejemplo_t ejemplo; // se diferencia claramente el tipo de la variable.
a = (ejemplo)(data); // ¿Estamos multiplicando variables o haciendo un cast?

Después vemos un _tp que yo utilizo para nombrar mis tipos privados, para diferenciarlos de los que estarán en las cabeceras externas.

Para terminar, en mis métodos suelo llamar a las variables de los tipos públicos con una uve (v) delante, con el fin de avisarme que ése es el void.

Getters y setters

Como no dejamos al usuario acceder a nuestra estructura a mano, no podrá realizar modificaciones sobre las variables. Por lo tanto, será necesario utilizar getters y setters para este fin.

Y una vez dicho eso… No debería ser lo habitual utilizar estos getters y setters, ya que la librería no debe ser un almacén de datos, sino una unidad de proceso. Es decir: lo normal no debe ser modificar valores desde fuera, sino desde dentro, como resultado de operaciones. Es evidente que, aun así, siempre necesitaremos algún getter y setter, aunque trataremos de evitarlos.

Es bastante común tener que enviar mucha información de configuración. Ésta puede encapsularse en una estructura pública y recibirla en el constructor.

Orientación a objetos/TADs

En C no tenemos Orientación a objetos, pero sí tenemos TADs (Tipos Abstractos de Datos). Toda librería debería ser un TAD.

Por esta razón, será normal encontrar en nuestras librerías un constructor y un destructor. Antes ya puse un ejemplo de esto.

Si dejamos al usuario acceder a nuestras variables, tarde o temprano tendremos un alto acoplamiento.

Creciendo

Si necesitamos ampliar nuestras estructuras públicas, siempre deberían hacerlo por debajo. De esta manera se seguirá manteniendo compatibilidad hacia atrás.

A menudo suele ser buena idea utilizar un número de versión o el tamaño de la estructura en uno de sus propios campos, con el fin de determinar que estamos utilizando ésa versión. El problema de estas aproximaciones es que dependemos del buen uso que le dé el programador.

Una solución mejor es transformar esa estructura pública en un nuevo TAD que ya manejaremos nosotros, evitando cualquier tipo de imprudencia o descuido.

De todas maneras… ¡¡ya dije que no usárais estructuras públicas!! :P

Librerías dinámicas

Es importante que nuestra librería dinámica mantenga en su archivo de cabecera público tipos de datos con forma de puntero a función para facilitar la vida a quien vaya a usarla, ya que no se puede acceder directamente a las funciones, pero siempre habrá que cargarlas.

En el ejemplo anterior:

1
2
3
4
5
6
// archivo de cabecera externo "ejemplodll.h"

typedef ejemplo_t void;

typedef ejemplo_t* (* PF_ejemplo_new) ();
typedef void       (* PF_ejemplo_destroy) ();

Fachadas

La creación de una librería dinámica es una invitación a la duplicación de código. Todos los programas que quieran utilizarla tendrán que cargar la librería, cargar sus funciones en variables/estructuras internas, realizar casts complejos, gestionar errores, etc.

Por lo tanto, es buena idea proporcionar una librería estática cuya función consista en acceder a la librería dinámica. Es el resultado de aplicar el patrón fachada o facade.

Esto facilitará mucho la vida al usuario. Veamos un ejemplo (continuando con el ejemplo de más arriba, que amplío un poco):

DLL

Archivo ejemplo.h:

1
2
3
4
5
6
7
8
9
// archivo de cabecera externo "ejemplo.h"

typedef ejemplo_t void;

typedef ejemplo_t* (* PF_ejemplo_new) ();
typedef void       (* PF_ejemplo_destroy) ();

typedef void       (* PF_ejemplo_operacion1) (ejemplo_t* t);
typedef void       (* PF_ejemplo_operacion2) (ejemplo_t* t, int a);

Archivo ejemplo.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "ejemplo.h"

typedef struct {
  int a;
  char* b;
} ejemplo_tp;

ejemplo_t* ejemplo_new () {
  ejemplo_tp* result;

  result = calloc (1, sizeof(ejemplo_tp));
  // inicializar result
  return result;
}

void ejemplo_destroy(ejemplo_t* vejemplo) {
  ejemplo_tp* ejemplo = (ejemplo_tp*) vejemplo;

  // liberar variables internas de "ejemplo"
  free (ejemplo);
}

void ejemplo_operacion1(ejemplo_t* vejemplo) {
  ejemplo_tp* ejemplo = (ejemplo_tp*) vejemplo;

  // operar
}

void ejemplo_operacion2(ejemplo_t* vejemplo, int a) {
  ejemplo_tp* ejemplo = (ejemplo_tp*) vejemplo;

  // operar
}

Lib

Archivo lejemplo.h:

1
2
3
4
5
6
7
8
// archivo de cabecera "lejemplo.h"
typedef    lejemplo_t void;

ejemplo_t* lejemplo_new ();
void       ljemplo_destroy(lejemplo_t* t);

void       lejemplo_operacion1 (lejemplo_t* t);
void       lejemplo_operacion2 (lejemplo_t* t, int a);

Archivo lejemplo.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// archivo de implementación, "lejemplo.c"
#import "ejemplo.h"
#import "lejemplo.h"

typedef struct {
  PF_ejemplo_new new;
  PF_ejemplo_destroy destroy;
  PF_ejemplo_operacion1 operacion1;
  PF_ejemplo_operacion2 operacion2;
} functions_tp;

typedef struct {
  void* dllhandle;
  ejemplo_t* internal;
  functions_tp functions;
} lejemplo_tp;

ejemplo_t* lejemplo_new () {
  ejemplo_tp* result = calloc (1, sizeof(lejemplo_tp));


  result->dllhandle = dlopen ("ejemplo.so", RT_LAZY);
  result->functions.new = (PF_ejemplo_new) dlsym (result->dllhandle, "ejemplo_new");
  result->functions.destroy = (PF_ejemplo_destroy) dlsym (result->dllhandle, "ejemplo_destroy");
  result->functions.operacion1 = (PF_ejemplo_operacion1) dlsym (result->dllhandle, "ejemplo_operacion1");
  result->functions.operacion2 = (PF_ejemplo_operacion2) dlsym (result->dllhandle, "ejemplo_operacion2");

  result->internal = result->funcitons.new ();

  // añadir control de errores allá donde haga falta

  return result;
}

void lejemplo_destroy(lejemplo_t* vejemplo) {
  lejemplo_tp* ejemplo = (lejemplo_tp*) vejemplo;

  ejemplo->functions.destroy(ejemplo->internal);
  dlclose (ejemplo->dllhandle);
  free (ejemplo);
  // Añadir comprobaciones de nulos y demás.
}

void lejemplo_operacion1 (lejemplo_t* t) {
  lejemplo_tp* ejemplo = (lejemplo_tp*) vejemplo;

  ejemplo->functions.operacion1 (ejemplo->internal);
}

void lejemplo_operacion2 (lejemplo_t* t, int a) {
  lejemplo_tp* ejemplo = (lejemplo_tp*) vejemplo;

  ejemplo->functions.operacion2 (ejemplo->internal, a);
}

Se podría hacer hasta una carga “lazy”, esperando a cargar las funciones en el momento de su utilización, pero eso puede implicar una gestión de errores más compleja (con lo indicado, la gestión de errores está centralizada al inicializar).

Utilizando esta librería, al usuario se le quitan muchos dolores de cabeza, ya que tendría que realizar eso mismo de todas maneras.

Olvidos, descuidos, etc.

Seguramente he olvidado comentar muchísimas cosas, pero creo que ya me he extendido lo suficiente.

Se aceptan sugerencias para completar todos estos consejos.