3.2. Políticas seguidas para obtener un código portable

En esta sección se explica que decisiones se han tomado para maximizar la portabilidad del código fuente de pDI-Tools.

Se trata desde el dialecto de C usado hasta las funciones de librería usadas. Esto delimita el tipo construcciones de C usadas, el uso del preprocesador, los tipos de datos disponibles y otros detalles.

Se explica el impacto que han tenido estas decisiones y consideraciones en el código, y como se ha hecho para se mantenga simple y mantenible.

Por último se explica la estrategia usada para incorporar información dependiente de la arquitectura de forma que sea transparente y usable por la parte portable.

3.2.1. Variante de C usada

Para programar pDI-Tools determine usar un C lo más estándar posible. Para ello se ha escogido el dialecto ISO C90 de C. El ISO C90 es un dialecto de C entendido por todos los compiladores actuales y rara vez genera incompatibilidades.

Para verificar que pDI-Tools cumple este requerimiento se realizaban cada cierto tiempo compilaciones con los "flags" -ansi y -pedantic. Estos "flags" hacen que compilador GNU C Compiler muestre avisos y errores en caso de usarse construcciones específicas del compilador y/o llamadas a funciones exclusivas de GNU C Library.

Lo malo es que el ISO C90 en cierto modo es un poco incompleto, y en ocasiones queda a manos del implementador del compilador tomar algunas decisiones. Un ejemplo sería indicar que una determinada función sea el segmento de código de inicialización o finalización de un DSO (secciones .init y .fini). No obstante se procura recurrir a estas características lo mínimo posible.

Todo esto significa a renunciar a muchas extensiones, que aunque muy comunes, nadie nos garantiza que estén presentes en todos los compiladores. Por lo tanto nunca se usan comentarios C++, ni uniones anónimas, ni espacios de nombres (de C++) ni declaración de variables en medio del cuerpo de una función.

El resultado es un código bastante claro, clásico y sencillo en el aspecto de que no explota explícitamente características avanzadas del compilador. Esto no significa que algunas veces se escriba el código con un poco de picardía para poder reconducir un poco la generación del binario y así explotar ciertas optimizaciones comunes entre muchos compiladores modernos.

3.2.2. El preprocesador

El preprocesador (cpp(1)), a pesar de ir ligado a un compilador de C, es una aplicación que implementa un lenguaje de macros muy potente. De hecho es muy normal encontrar proyectos en otros lenguajes que hacen uso del preprocesador de C.

El problema es que el preprocesador a veces varía mucho de un compilador a otro. No todos ofrecen la misma potencia, flexibilidades o incluso algunos interpretan de forma diferente algunas estructuras. Por ejemplo GNU C Compiler permite declarar macros con número de parámetros variable, cosa nada habitual en otros compiladores.

El preprocesador, es esencial para establecer buenos entornos de depuración o para crear programas multiplataforma. Es ideal para tomar opciones de configuración en tiempo de compilación, para crear alias a estructuras, tipos y funciones y para activar/desactivar código.

El preprocesador decide que se compila finalmente, y por lo tanto, el código que intervendrá en la generación del ejecutable. Esto nos permite activar y desactivar código sin penalizaciones en el tiempo de ejecución de la versión final. Además nos permite escoger el código que se usará en una plataforma u otra, reduciendo el tamaño del binario final.

No obstante sólo se usan unas pocas características del preprocesador para evitar ligarnos accidentalmente a un compilador determinado:

3.2.3. Tipos de datos

Al escribir una aplicación multiplataforma se ha de ser cuidadoso con los tipos de datos, especialmente cuando se apunta tanto a plataformas de 32 como de 64 bits.

El tamaño de los tipos de datos en un modelo de programación y otro a veces varían, y por lo tanto también varía el rango de los números que puede manejar.

Además hay tipos que son exclusivos de un compilador o de un determinado estándar. Por ejemplo ISO C99 aporta el tipo long long int que fuerza a usar un entero de 64 bits.

Usaremos siempre tipos sencillos y fácilmente predecibles en todas las arquitecturas. Por ello nos ceñiremos sólo a tipos, operaciones y asignaciones entre tipos declaradas por el estándar ISO C90.

Sólo se quebrantará esta regla en el caso de operaciones con punteros a funciones, que por características del programa, son inevitables. Por suerte esto es algo soportado por todos los compiladores del mercado.

Respecto a la construcción de estructuras de datos y uniones se será respetuoso al máximo con el estándar. Esto significa que no existirán estructuras o uniones anónimas, ni se usarán extensiones del lenguaje para poder modelarlas mejor.

Los tipos de datos basados en estructuras se crearán usando el operador typedef. De esta forma nos referiremos a las estructuras normalmente por su nombre de tipo y no por el nombre de la estructura. El nombre de la estructura será el nombre del tipo pero con el prefijo _tag_. Por ejemplo:


typedef struct _tag_BECFG_GRAPH_NODE {
  BECFG_OBJ_INFO        *gn_object;

  int                   gn_weight;
  int                   gn_hasPar;

  int                   gn_nto;
  struct _tag_BECFG_GRAPH_NODE **gn_to;
} BECFG_GRAPH_NODE
      

Por último se usarán, siempre que se pueda, los tipos de datos indicados por el ISO C90 para las funciones de las librerías de C. Así por ejemplo siempre se preferirá usar el tipo ssize_t a int. También se usará siempre que se pueda tipos ELF para evitar hacer conversiones innecesarias.

3.2.4. Datos no-dependientes y dependientes de la arquitectura

Los datos dependientes de la arquitectura siempre van encapsulados en los datos independientes de la arquitectura. Pondremos por ejemplo la estructura de información de una interposición. Esta estructura, a parte de indicar el tipo de interposición, el símbolo sobre el que actúa, el "backend" y función que reciben el control, debe guardar también como deshacerse más tarde así misma. Esta información depende mucho de la arquitectura y sistema operativo.

Generalmente toda la información que maneja pDI-Tools suele ir agrupada en estructuras. Así tenemos estructuras de información sobre las interposiciones, estructuras de información sobre los objetos en memoria, etc.

La información dependiente de la arquitectura podría ir mezclada en la misma estructura con la información independiente de la arquitectura. Luego, mediante construcciones #if ... #endif se podría activar y desactivar de una plataforma a otra. El problema con esta solución es que lleva a un código difícil de mantener, y lo peor de todo, no marca una separación clara entre la información dependiente de la independiente de la arquitectura.

Para resolver estos problemas se decidió que la información dependiente de la arquitectura debe encapsularse en una estructura que reciba el nombre "arch". Volviendo al ejemplo anterior, tendríamos:


typedef struct _tag_PDI_INTERCEPT {
  /* Información no-dependiente sobre la interposición */
  int                      i_type;     /* tipo de interposición                  */
  char                     *i_symbol;  /* símbolo al que afectará                */
  struct _tag_PDI_ELFOBJ   *i_backend; /* backend que ejecuta esta interposición */
  void                     *i_wrapper; /* función encargada de interponerse      */

  /* datos específicos según arquitectura */
  PDI_ARCH_INTERCEPT       *i_arch;

  /* Siguiente intercepción en la lista */
  struct _tag_PDI_INTERCEPT *i_prev;
  struct _tag_PDI_INTERCEPT *i_next;
} PDI_INTERCEPT;
      

Donde la estructura i_arch es proporcionada por el "Elf Backend" específico de la arquitectura. Nótese que en realidad es un puntero a la información dependiente de la arquitectura y no la información en sí. Los motivos son los siguientes: