Compatibilidad binaria en las bibliotecas

Para bibliotecas estables y ya liberadas, es muy importante intentar preservar la compatibilidad binaria en las revisiones mayores del código. Los desarrolladores de bibliotecas deben intentar no alienar a los usuarios realizando de forma frecuente cambios que sean binariamente incompatibles en bibliotecas que son de producción. Por supuesto, en aquellas bibliotecas que se encuentran en desarrollo o están etiquetadas como no finalizadas puedes realizar tantos cambios como se necesiten.

Una biblioteca típicamente exporta un número de interfaces. Estas incluyen nombres de rutinas, las firmas o prototipos de estas rutinas, variables globales, estructuras, campos de estructura (tanto los tipos como su significado), el significado de los valores enumerados y la semántica de los archivos que la biblioteca crea. Mantener compatibilidad binaria significa que estas interfaces no cambiarán. Puedes añadir nuevas interfaces sin romper la compatibilidad binaria, pero no puedes cambiar las ya existentes, ya que los programas antiguos no se ejecutarán correctamente.

Esta sección incluye un número de ideas acerca de como lograr que el código de una biblioteca sea compatible con sus versiones anteriores.

Mantener la compatibilidad binaria significa que los programas y archivos objetos que fueron compilados con una versión previa del código continuarán funcionando sin recompilar aún cuando la biblioteca sea reemplazada. Es posible encontrar una descripción detallada en el nodo info (libtool)Interfaces de la documentación de GNU libtool.

Información privada en las estructuras

En el sistema GNOME es una tarea muy común crear un nuevo GtkObject para crear una estructura cuyo primer miembro corresponde a la clase de la cual hereda este objeto y posteriormente un número de variables de instancia que se añaden después del primer miembro.

Este esquema de diseño normalmente lleva a que el código sea difícil de actualizar y cambiar: si se quiere extender un objeto donde se necesita mantener su estado interno, los programadores necesitan recurrir a varios hacks para mantener el mismo tamaño de las estructuras. Esto hace difícil agregar nuevos campos a las estructuras públicas sin romper la compatibilidad binaria, ya que los campos de la estructura pueden cambiar y el tamaño de las estructuras pueden variar.

Por consiguiente, se sugiere una estrategia que puedan adoptar los desarrolladores en el momento de crear nuevos objetos. Esta estrategia asegurará la compatibilidad binaria y al mismo tiempo mejorará la legibilidad y mantenibilidad del código.

La idea es que, para un objeto dado, el programador debiera crear tres archivos: el primero que contiene el contrato entre la biblioteca y el usuario (esto es el archivo de encabezado que se instala en el sistema); el segundo contiene la implementación del objeto; y el tercer archivo contiene la definición de la estructura para los campos privados o internos que no necesitan estar en la estructura pública.

La API pública de la estructura podría incluir un puntero a un elemento privado, el cual podría apuntar a los datos privado de la instancia. Las rutinas de implementación podría dereferenciar este punto para acceder a los datos privados; por supuesto, el puntero apunta a la estructura que se localiza privadamente.

Por ejemplo, imagina que se crea el objeto GnomeFrob. La implementación del widget será dividido en tres archivos:

Tabla 1. Archivos que muestran el widget de ejemplo GnomeFrob

ArchivoContenido
gnome-frob.hAPI pública de GnomeFrob
gnome-frob.cImplementación del objeto Gnomefrob
gnome-frob-private.hDatos privados de la implementación de GnomeFrob. Debieras querer usar esto si planea compartir los datos privados a través de varios archivos C. Si limitas el uso de la información privada a sólo un archivo, no necesita esto, ya que al definir la estructura en el archivo de implementación se logrará el mismo efecto.


Ejemplo 1. Ejemplo de la API pública de gnome-frob.h

typedef struct _GnomeFrob GnomeFrob;
typedef struct _GnomeFrobPrivate GnomeFrobPrivate;

struct _GnomeFrob {
       GtkObject parent_object;

       int  public_value;
       GnomeFrobPrivate *priv;
} GnomeFrob;

GnomeFrob *gnome_frob_new (void);
void       gnome_frob_frobate (GnomeFrob *frob);


Ejemplo 2. Ejemplo de gnome-frob-private.h

typedef struct {
        gboolean frobbed;
} GnomeFrobPrivate;


Ejemplo 3. Ejemplo de la implementación de gnome-frob.c.

void gnome_frob_frobate (GnomeFrob *frob)
{
        g_return_if_fail (frob != NULL);
	g_return_if_fail (GNOME_IS_FROB (frob));

	frob->priv->frobbed = TRUE;
}


Fíjese en el uso de prototipos de estructura: esto permite que el compilador realice más verificaciones en tiempo de compilación respecto a los tipos que estan siendo empleados.

Este esquema es útil en algunas situaciones, particularmente en el caso en el cual vale la pena almacenar un puntero extra para una instancia del objeto. Si esto no fuera posible, el programador necesitaría recurrir a otros artilugios para evitar este problema.

El propósito de tener un archivo de encabezado privado extra para las estructuras privadas es permitir que las clases derivadas usen esta información. Por supuesto, una buena práctica de programación indicaría que debiera proveerse de interfaces o métodos de acceso precisamente para que el programador no tenga que lidiar con estructuras privadas. Se debiera intentar alcanzar un balance entre buena práctica y pragmatismo cuando se crean estructuras privadas y métodos de acceso públicos.

Podrían aparecer algunos problemas, por ejemplo, cuando se ha enviado un versión del código y que está siendo ampliamente usado y se desea extenderlo. Hay dos soluciones para esto.

La primera opción es encontrar un campo puntero en la estructura pública que pueda hacerse privada y reemplazar el puntero a la parte privada de la estructura. Esto preserva el tamaño de la estructura ya que, para los propósitos de GNOME, se puede asumir que los punteros son todos del mismo tamaño. Se puede hacer que este campo apunte a la parte privada de la estructura que será ubicada por la función que originalmente creaba la estructura pública. Es importante que este campo no sea llamado private, se trata de una palabra reservada de C++ y creará problemas cuando el archivo de encabezado sea usado desde programas fuentes C++ — mejor llámalo priv. Por supuesto, este tipo de cambios sólo funcionará si el antiguo campo puntero solamente era usado para propósitos internos; si los usuarios de la biblioteca habían tenido acceso a dicho campo para cualquier propósito, será necesario buscar otro campo o buscar una solución diferente.

Si la estructura original fue derivada de GtkObject y no hay campos punteros que puedan ser reemplazados, puedes usar la facilidad de datos de objetos de GTK+. Esto permite asociar punteros a datos arbitrarios a objetos. Usa la función gtk_object_set_data() para adjuntar valores o estructuras a un objeto y cuando requieras usarlos la función gtk_object_get_data() permitirá recuperar dichos valores. Esto puede ser empleado para adjuntar una estructura privada a un objeto GTK+. Esto no es tan eficiente como el enfoque anterior, pero podría no importar para el dominio particular de la aplicación. Si los datos serán accedidos frecuentemente, se pueden usar quarks en vez de cadenas para agregar y obtenerlos a través de las funciones gtk_object_set_data_by_id() y gtk_object_get_data_by_id(), respectivamente.