Software de desarrollo de simulaciones para las pruebas

Anuncio
8
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
2. DLL (Dynamic Link Library)
2.1.
¿Qué es una DLL? Funcionalidad
Una biblioteca de vínculos o enlaces dinámicos (Dynamic Link Library, en inglés) no es más que
una porción de código ejecutable que puede cargar y usar otro programa bajo demanda. Y no
sólo un programa, esta librería puede ser compartida por varios programas. Este nombre se
utiliza en exclusiva en sistemas operativos Microsoft Windows aunque este concepto de
librería dinámica está presente en todos los sistemas operativos. En el caso de los sistemas
operativos de Microsoft en general estas librerías están contenidas en archivos de extensión
.dll o incluso dentro archivos ejecutables con extensión .exe. En otros sistemas de tipo Unix
por ejemplo suelen tener la extensión .so (shared object).
2.2.
Ventajas e inconvenientes. Alternativas
A medida que se va escribiendo el código de un programa es fácil darse cuenta de que hay
partes del código que se repiten. Si sólo se van a usar para pequeños programas basta con
introducir ese código en funciones o procedimientos de otros tipos. Pero para grandes
programas con muchos archivos de código, lo ideal sería tener ese código en un lugar separado
desde el cual otros códigos puedan acceder a él. Además, si este código es siempre el mismo lo
idóneo es que quede compilado de forma definitiva y así el compilador no tendrá que
malgastar tiempo de proceso en volver a realizar un trabajo ya hecho y que ya sabemos que
está bien. Estos son los conceptos claves de una librería.
Una ventaja adicional a esto que se ha comentado de las librerías (reutilización de código y
mayor velocidad de compilación y mantenimiento) es que al ser usado por varios programas se
trata de un código muy probado, con lo que la eficiencia es mucho mayor.
Existen dos tipos de librerías:


Librerías estáticas: La porción de código de esta librería se introduce dentro del
código del programa, con lo que para usarla un programa tiene que compilarse
junto con ella y una vez hecho la librería ya no se usa a la hora de la ejecución.
Librerías dinámicas: El código de la librería no se copia al ejecutable una vez
compilado, éste permanece ya compilado en un archivo fijo al cual el ejecutable
accede en busca de su código. Por tanto a la hora de ejecutar el programa es
necesario tanto el ejecutable como la librería, en caso de que ésta no esté habría
un error.
Una librería de enlace dinámico es por tanto una evolución de las librerías estáticas. La
cuestión es, ¿en qué casos es mejor una u otra? ¿Qué otras ventajas e inconvenientes tiene el
uso de estas librerías frente a no usarlas además de todo lo anteriormente visto? Se analizará
punto por punto según varios aspectos:
9
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M





Tamaño de archivo y memoria: Si se usa una librería estática o si no se usan
librerías, al tener que copiarse el código de la librería al ejecutable el tamaño de
éste es mayor. Si ese código es encima compartido por varios programas, al
meterse todo ese código en una librería dinámica el ahorro en espacio en disco
puede ser grande. Al no tener que cargar varias porciones de código iguales sino
sólo una desde la que acceder los programas de forma compartida, ahorra
también en memoria. Sin embargo, siendo realistas, esto suponía una gran ventaja
en otros tiempos en los que el espacio en disco (y sobre todo el espacio en
memoria) era crítico. A día de hoy, en los que los precios y los tamaños de las
memorias de almacenamiento y ejecución han caído y subido respectivamente de
forma significativa esto parece ya una cuestión menor.
Velocidad de ejecución: El uso de librerías estáticas a priori debe proporcionar una
mayor velocidad de ejecución al programa en cuestión, ya que no tiene que “salir
afuera” a buscar otro código con el que continuar la ejecución. La realidad es que
la diferencia de rendimiento no es muy grande considerando la gran velocidad de
procesamiento que tienen los microprocesadores de hoy en día.
Gestión de código y flexibilidad ante cambios: El hecho de que una librería
dinámica tenga una porción de código externo al programa hace que la corrección
de pequeños errores de ese código sea más fácil. Bastaría con cambiar la librería y
todos los programas que la usan continuaría funcionando y con la mejora en el
código implementada.
Origen de las librerías dinámicas: Una librería dinámica puede estar hecha por
otros desarrolladores diferentes a los de los programas que lo usan. Es más,
pueden estar hechas con entornos de desarrollo diferentes las librerías y los
programas. Es lo que ocurre con las de los sistemas operativos, los programas
preparados para éstos utilizan aquellas librerías para acelerar la programación de
aplicaciones. Esto proporciona una ventaja enorme a los programadores, tanto
para realizar aplicaciones que empleen dichas librerías como justo al contrario:
programar librerías que amplíen la funcionalidad de algunos programas. Esto
último será fundamental para el caso posterior que nos ocupará de la
programación de simulaciones usando librerías.
Dependencia de las librerías: El hecho de que varias aplicaciones empleen la
misma librería hace la fiabilidad de ellas dependa de más factores. Eso implica que
si una librería tiene un problema (por ejemplo, porque se borre o porque se
sustituya por una versión anterior inintencionadamente), muchos programas
pueden dejar de funcionar adecuadamente o sencillamente ni siquiera funcionar.
Es un problema conocido de versiones del sistema operativo Microsoft Windows,
en lo que se conoce como “DLL Hell”: era relativamente frecuente que al
desinstalar un programa se desinstalasen sus DLLs que otros programas usaban.
Afortunadamente, en versiones más modernas de este sistema este problema está
más controlado.
Parece claro que hoy en día es fundamental el uso de las DLLs en sistemas Windows para
aprovechar todas sus ventajas. Para códigos relativamente grandes, también parece útil el que
creemos nuestras librerías DLL propias. Y en el ejemplo que se verá con simulaciones, como se
10
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
ha dicho, será la forma en que se pueda ampliar la funcionalidad de otros programas. Más
adelante se verá en profundidad.
2.3.
Lenguajes de programación, entornos y DLLs
Una DLL puede ser escrita en cualquier lenguaje de programación, como C, C++, Pascal, Visual
Basic, etc. De la misma forma, cualquier entorno que use estos lenguajes será capaz de
generar archivos DLL compilados. Algunos de ellos contienen asistentes que pueden facilitar la
tarea de crear el código de una nueva librería, cuestión que veremos más adelante con el
entorno Borland C++ Builder. En cualquier caso, se use el entorno y lenguaje que se use, las
ideas básicas son comunes a todos ellos, así como la estructura, la compartición de datos en
memoria, la carga de funciones, la forma en que se programan…
Por otro lado, cada sistema operativo tendrá sus particularidades en este tema, aunque en
general también la idea es la misma, sólo que quizás algunas funciones estándar puedan ser
diferentes y otros pequeños puntos. También por otro lado, existen librerías de programación
(tales como las MFC de Microsoft) que permiten hacer el mismo trabajo sobre un lenguaje y
también habría que conocer las particularidades de esas librerías.
Es por ello que comenzaremos hablando de esta forma de programación de la forma más
genérica posible para a continuación hacer especial incapié en cómo se haría en el lenguaje
que se usará en los ejemplos posteriores: C y C++, a los entornos que se usarán y también al
sistema operativo al que irá destinado el trabajo: toda la serie de Windows basada en NT (con
el consiguiente API de Win32).
2.4. Aspectos generales de programación de DLLs y de carga de
recursos de una DLL
2.4.1. Puntos clave en la programación de una DLL
2.4.1.1.
Objetos exportables
A la hora de escribir el código fuente del que luego se compilará para formar la DLL, los objetos
(funciones y clases) que deban ser accesibles desde otros ejecutables, se denominan
exportables, también callbacks si son funciones, en atención a una denominación muy usual en
la literatura inglesa ("callback functions"). Esta circunstancia debe ser conocida por el
compilador, por lo que es necesario especificar qué recursos se declaran "exportables";
además debe indicarse al "linker" que genere una librería dinámica en vez de un ejecutable
normal, aunque como siempre todo esto depende del entorno que usemos.
Las funciones, clases o tipos de datos de las que constará la DLL se programan en un principio
de la misma manera que como si se hicieran dentro del código del programa que invocará
dicho objeto, aunque hay algunas particularidades. En el caso de sistemas Windows, hay que
usar los especificadores _export y dllexport, como veremos más adelante.
11
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
2.4.1.2.
Archivo de definición y librería o tabla de importación
Además de las fuentes de la librería, en determinados casos, la creación de una DLL exige la
existencia de dos ficheros auxiliares: una librería de importación y un fichero de definición .def
("definition file"). La primera es una librería estática clásica (.lib o .a) que sirve como índice o
diccionario de la dinámica. El segundo es un fichero ASCII. En caso de ser necesarios, la
creación de estos ficheros auxiliares se realiza generalmente en el mismo momento en que se
crea la librería. Sin embargo, en determinadas circunstancias, especialmente cuando se
dispone de una DLL construida de la que no se tienen las fuentes, la creación exige de
herramientas auxiliares.
La necesidad de tales ficheros depende del compilador y de las circunstancias. La
documentación de Microsoft señala que generalmente, la librería de importación es necesaria
para usar la librería con enlazado estático, pero no para enlazado dinámico (explícito). En
cambio, la documentación de MinGW señala: "la librería de importación es necesaria si (y solo
si) la DLL debe ser utilizada por un compilador distinto de la colección de herramientas
MinGW, ya que estas son perfectamente capaces de enlazar con sus DLLs sin necesidad de
ningún recurso auxiliar".
Sea cual sea la forma utilizada, los recursos declarados exportables son incluidos por el
enlazador en una tabla especial contenida en la DLL, que se llama tabla de exportación
("export table") o tabla de entrada ("entry table"). La tabla de exportación tiene dos tipos de
datos importantes (en realidad son dos tablas): los nombres con que aparecen los recursos y
un número de orden. Cuando una aplicación (.exe o librería dinámica) invoca una función
situada en una librería, el módulo que realiza la invocación puede referirse al recurso por
nombre o por número de orden. Como se puede intuir, la segunda forma es ligeramente más
rápida, ya que no se necesitan comparaciones de cadenas para localizar la entrada, pero en el
caso de Windows Microsoft recomienda que las librerías dinámicas se exporten por nombre;
de lo contrario no se garantiza que nuestras librerías sean utilizables por todas las plataformas
y versiones de Windows.
Cuando un recurso es exportado por número, la parte de nombres de la tabla no necesita ser
residente en la memoria del ejecutable que la utilizará. En cambio, si es exportada por
nombres, dicha tabla sí necesita ser residente, y será cargada en memoria cada vez que el
módulo sea cargado.
Es importante tener en cuenta, sobre todo si se va a construir DLLs que serán utilizadas por
terceros, que los nombres de los recursos exportados no pueden interferir con ningún otro
nombre utilizado por el programa o por otra DLL del sistema, de forma que debemos
asegurarnos que estos nombres serán únicos.
2.4.1.3.
Función DLLMain
En la programación de aplicaciones, se puede especificar una función que es el punto de
entrada de la ejecución del programa, normalmente llamada función main y en el caso de la
programación bajo el API de Win32 función WinMain. De la misma manera, las DLLs pueden
tener una función que se considera su punto de entrada y sirve para inicializar variables,
objetos u otros elementos (hilos, procesos, etc) en el momento en el que son cargadas por
parte de otro programa. Es algo totalmente opcional, no es estrictamente necesario tener esta
12
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
función dentro del código. En el momento en que se cargue la librería, esta función será
invocada inmediatamente, sin que el programador tenga que hacer ninguna acción adicional
tras la carga de la librería.
Dependiendo del entorno de programación y el lenguaje elegido, puede tener diferentes
nombres, como veremos más adelante en el caso de Borland C++ Builder. Incluso en algunos
compiladores se puede especificar el nombre de dicha función por la línea de comandos. En
general, el prototipo de esta función es, para el caso de la programación en C/C++ y el API de
Win32:
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
2.4.1.4.
Modificador _export o __export
Como se ha mencionado anteriormente, tanto si se usa el copilador C++ Borland como otros,
los recursos "exportables" pueden ser declarados con los especificadores _export o __export
(son equivalentes). Hay que recordar que C++ dispone de una palabra clave específica: export,
cuyo significado se asemeja al que se utiliza aquí: indicar al compilador que la declaración será
accesible desde otras unidades de compilación. Sin embargo, tener en cuenta que _export y
export no tienen nada que ver entre sí. La primera es una particularidad de ciertos
compiladores; la segunda es una palabra clave del C++ Estándar. De ahí que inicialmente
contenga un “_” o uno doble para especificarlo.
valor-devuelto __export nombre-funcion (argumentos);
valor-devuelto _export nombre-funcion (argumentos);
class _export nombre-de-clase;
tipo-de-dato _export nombre-de-variable;
Los recursos exportables también pueden ser declarados mediante el especificador
__declspec(dllexport):
__declspec(dllexport) valor-devuelto funcion (argumentos);
class __declspec(dllexport) nombre-de-clase;
__declspec(dllexport) tipo-de-dato nombre-de-variable;
Ejemplos:
extern "C" _export double SumaValores(double, double);
class _export claseDeEjemplo;
double _export temperaturaFuel;
extern "C" __declspec(dllexport) double SumaValores(double, double);
class __declspec(dllexport) miNuevaClase { /* ... */ };
13
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
__declspec(dllexport) int numPagina;
2.4.2. Puntos clave en la carga de recursos de una DLL
2.4.2.1.
Vinculación implícita y explícita
Existen dos formas de vincular una librería DLL a un ejecutable: vinculación implícita y
vinculación explícita.
Para una vinculación implícita a un archivo DLL, los archivos ejecutables, procesos u otras
librerías que pretendan utilizar recursos de una DLL deberán obtener lo siguiente del
proveedor del archivo DLL:

Un archivo de encabezado (archivo .h) que contenga las declaraciones de las funciones
exportadas, clases u otro tipo de objeto exportado. Todas las clases, todas las
funciones y todos los datos deberían tener __declspec(dllimport). El uso de este
especificador es el mismo al que vimos antes con dllexport, un ejemplo podría ser:
__declspec( dllimport ) int i;
__declspec( dllimport ) void func();

Una biblioteca de importación (archivos de librería estática .lib) a la que vincularse. El
vinculador crea la biblioteca de importación cuando se genera el archivo DLL.

El archivo de librería dinámica con la extensión .dll.
Los archivos ejecutables que utilizan el archivo DLL deben incluir el archivo de encabezado que
contiene las funciones o clases exportadas en cada archivo de código fuente que contenga
llamadas a esas funciones exportadas. Desde el punto de vista de la programación, las
llamadas a las funciones exportadas son como cualquier otra llamada a función en una
programación sin librerías.
Para generar el archivo ejecutable de llamada deberá vincularlo a la biblioteca de importación.
El sistema operativo deberá ser capaz de encontrar el archivo DLL cuando cargue el archivo
ejecutable de llamada.
Usando este método de vinculación, los recursos exportables de la librería están siempre
disponibles para su uso y la inicialización de esos elementos se produce antes del main del
ejecutable principal. El programador no tendrá que hacer nada especial para usar esos
recursos ni para inicializarlos.
En cuanto a la vinculación explícita, las aplicaciones deben realizar una llamada a función para
cargar explícitamente el archivo DLL en tiempo de ejecución. Para una vinculación explícita a
un archivo DLL, una aplicación debe:

Llamar a LoadLibrary para cargar el archivo DLL y obtener un identificador de módulo.

Llamar a GetProcAddress para obtener un puntero a función para cada función
exportada a la que la aplicación desee llamar. Como las aplicaciones llaman a las
14
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
funciones del archivo DLL mediante un puntero, el compilador no genera referencias
externas, por lo que no hay necesidad de vincularse a una biblioteca de importación.

Llamar a FreeLibrary cuando se haya acabado de utilizar el archivo DLL.
A diferencia del método de vinculación anterior, los recursos exportables de la DLL estarán
disponibles sólo en el momento en que se necesiten. La librería es inicializada cuando el
programador decide cargarla y liberada también en el momento en que desee hacerlo (lo cual
puede implicar un menor consumo de memoria y en general de recursos de sistema, además
de un mayor control sobre la librería, aunque quizás una cierta mayor complejidad en cuanto a
la programación).
Como se verá más adelante, en el desarrollo de las simulaciones de las pruebas de combustible
se usa la vinculación explícita, debido principalmente a que en ellas se tiene que cargar otras
librerías de terceros de las que se dispone únicamente de archivos .dll. Además, el primer
procedimiento tiene algunas desventajas que ya se han visto también en el estudio de librerías
estáticas, como el consumo de recursos. Se verá por tanto a continuación más en profundidad
este método de vinculación.
2.4.2.2.
La función LoadLibrary
Los procesos llaman a LoadLibrary para vincularse explícitamente a un archivo DLL. Si este
procedimiento se ha realizado correctamente, la función asigna el archivo DLL especificado al
espacio de direcciones del proceso que lo llama y devuelve un identificador al archivo DLL que
se puede usar con otras funciones utilizadas en este tipo de vinculación y que se verán más
adelante, como GetProcAddress y FreeLibrary.
LoadLibrary intenta encontrar el archivo DLL indicado por su ruta y nombre de archivo el cual
se pasa como parámetro mediante la misma secuencia de búsqueda que se utiliza para la
vinculación implícita. Si el sistema no encuentra el archivo DLL o la función de punto de
entrada (recuérdese aquí el DLLMain) devuelve el valor FALSE, LoadLibrary devolverá el valor
NULL. Si la llamada a LoadLibrary especifica un módulo de DLL que se asignó a ese espacio de
direcciones, la función sólo devolverá un identificador del archivo DLL e incrementará la
cuenta de referencia del módulo.
Si el archivo DLL tiene una función de punto de entrada, el sistema operativo llamará a la
función en el contexto del subproceso que llamó a LoadLibrary. No se llamará a la función de
punto de entrada si el archivo DLL ya está asociado al proceso a causa de una llamada anterior
a LoadLibrary sin una llamada correspondiente a la función FreeLibrary (en este caso sería una
carga anidada de una misma librería, caso que no se usará).
Por tanto, para el caso particular de la programación en C/C++ y el API de Win32, el prototipo
de esta función sería:
HINSTANCE LoadLibrary(LPCTSTR lpcNombreArchivoDll);
15
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
2.4.2.3.
La función GetProcAddress
Una vez la DLL ha sido cargada, a partir de ese momento se puede proceder a la carga de sus
recursos. Para obtener la dirección de una función exportada por esa librería, se usa la función
GetProcAddress, que para Win32 y C/C++ tiene el siguiente prototipo:
FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName);
Esta función toma como primer parámetro el identificador de módulo del archivo DLL cargado
(es decir, lo que devuelve la función LoadLibrary) y toma como segundo el nombre de la
función a la que se desea llamar o el ordinal de exportación de la función. Devuelve un puntero
a función. Una vez obtenido, ya se puede usar esa función en el programa.
Como se mencionó en puntos anteriores, en lugar de especificar un nombre se podría usar un
ordinal de exportación. Sólo podrá obtener este ordinal de exportación si el archivo DLL al que
se está vinculando se ha creado con un archivo de definición de módulos (.def) y si los
ordinales figuran en la lista de funciones del archivo .def correspondiente al archivo DLL.
Utilizar un ordinal de exportación en lugar de un nombre de función para llamar a
GetProcAddress resulta un poco más rápido si el archivo DLL tiene muchas funciones
exportadas, puesto que los ordinales exportados sirven como índices en la tabla de
exportación del archivo DLL. Con un ordinal de exportación, GetProcAddress puede encontrar
la función directamente, sin tener que comparar el nombre especificado con los nombres de
función de la tabla de exportación del archivo DLL. No se empleará este método ya que no se
dispone de los .def de las librerías que se cargarán, como veremos más adelante.
Como está llamando a la función DLL mediante un puntero y no hay comprobación de tipos en
tiempo de compilación, debe asegurarse de que los parámetros pasados a la función son
correctos, para no sobrepasar la memoria asignada en la pila y causar así una infracción de
acceso. Hay que consultar los prototipos de las funciones exportadas por la DLL, por lo que es
clara la necesidad de una adecuada documentación sobre qué es lo que una librería puede
proporcionar a sus usuarios y cómo emplearla.
2.4.2.4.
La función FreeLibrary
Los procesos que se vinculan explícitamente a un archivo DLL llaman a la función FreeLibrary
cuando el módulo de DLL deja de ser necesario. Esta función reduce el número de referencias
del módulo y, si dicho número es cero, elimina la asignación del espacio de direcciones del
proceso. El prototipo en Win32 y C/C++ es:
FreeLibrary (HINSTANCE)
Donde su único parámetro es el identificador de la DLL obtenido con el valor devuelto por
LoadLibrary. No hay que olvidar nunca liberar la carga de una DLL en el momento en que ya no
se va a necesitar más de sus recursos exportados.
2.4.3. Ejemplo de creación y carga de DLLs
A continuación, se mostrará un ejemplo de programación en C/C++ de creación y carga de una
nueva DLL.
16
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
En este sencillo ejemplo de creación de DLL, simplemente se exporta una función que muestra
un mensaje en pantalla:
// miLibreria.c
#include <stdio.h>
__declspec(dllexport) void Funcion1 () {
printf(“Función de mi DLL\n”);
}
// miLibreria.h
__declspec(dllexport) void Funcion1 () ;
Tras compilar el ejemplo anterior se obtiene el archivo miLibreria.dll. A continuación se
muestra un ejemplo de carga de la función de la librería anterior:
#include <windows.h>
typedef void (*FUNCION1)(void);
FUNCION1 Funcion1;
HINSTANCE dllHandle;
int main()
{
dllHandle = LoadLibrary("miLibreria.dll");
if(dllHandle != NULL) // Si no ha habido problemas al cargar la librería
{
Funcion1 = (FUNCION1)GetProcAddress(dllHandle, "FuncionInit");
if(Funcion1 != NULL)
{
Funcion1();
// Llamada a la función cargada
}
else
{
// Error al cargar la función
}
FreeLibrary(dllHandle);
}
}
Como se puede ver, la programación de DLLs no entraña una gran dificultad. Eso sí, se ha de
tener cuidado de llevar un buen control del flujo de la ejecución, teniendo en cuenta que en
17
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
algún momento podría haber algún problema en la carga de la librería o de una función,
inspeccionando adecuadamente el valor que se va adquiriendo en los punteros.
2.4.4. Convenciones de llamada y DLLs
Hay un aspecto a la hora de programar funciones para una DLL que hay que tener muy en
cuenta: la convención de llamadas.
Las convenciones de llamada de una función se diferencian unas de otras por los siguientes
puntos:

La forma que cada una utiliza para la limpieza de la pila (stack).

El orden de paso de parámetros (derecha a izquierda o a la inversa).

El uso o no de mayúsculas y minúsculas, y ciertos prefijos en los identificadores
globales.
Normalmente, en la programación de aplicaciones con un mismo entorno de desarrollo,
lenguaje de programación y sistema operativo, sea cual sea la convención usada no supone un
problema ya que en todo momento se va a usar una misma convención. Sin embargo, en la
programación de DLLs puede ocurrir que la librería esté hecha con un entorno y la aplicación
que la vaya a usar tenga otro, por lo que las convenciones de llamada podrían ser diferentes y
la ejecución podría ser errónea al producirse un incorrecto paso de parámetros y uso de datos,
por lo que esto es algo con lo que hay que tener especial cuidado.
La especificación de la forma que se utilizará en el programa, puede hacerse a nivel global o
solo a nivel particular de algunas funciones específicas. Para indicarlo a nivel global se utiliza
alguno de los comandos específicos del compilador. La forma de indicarlo a nivel particular es
mediante el uso de ciertas palabras reservadas para que sea utilizada una forma específica en
lugar de la que tenga asignada el compilador por defecto. Estas palabras deben indicarse en la
declaración o prototipo, y delante del especificador de invocación de la función. Son las
siguientes: __cdecl (invocación del lenguaje C), __pascal (invocación en Pascal), __fastcall
(invocación registro), __msfastcall (invocación rápida) y __stdcall (invocación estándar). En la
programación Windows, al incluir la cabecera windows.h, estas convenciones de llamada se
utilizan a través de sus propios typedefs. En concreto se utilizan las siguientes equivalencias
Especificador Windows Equivalente Tipo de invocación
CDECL
__cdecl
Invocación C
WINAPI
__stdcall
Estándar
CALLBACK
__stdcall
Estándar
Tabla 1: Equivalencia de especificadores Windows y tipos de convención de llamada
De la misma forma, una característica que distingue a unos compiladores (y lenguajes) de
otros, es el tratamiento dado a los identificadores de los objetos; lo que se conoce como
18
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
sistema de codificación de nombres ("name encoding scheme"). De este sistema depende que
durante las fases intermedias de la compilación, los identificadores sean guardados tal como
los escribe el programador o sufran mutaciones más o menos importantes.
Un ejemplo de esto ocurre en el compilador Borland C++. En BC++, cuando está activada la
opción -u (opción por defecto), el compilador guarda todos los identificadores globales en su
grafía original (mayúsculas, minúsculas o mixta), añadiendo automáticamente un guión bajo
“_” delante de cualquier identificador global, ya sea de función o de variable. Para modificar
este comportamiento se puede utilizar la opción -u- como parámetro en la línea de comando
del compilador.
La siguiente tabla resume el efecto de un modificador aplicado a una función. Por cada
modificador, se muestra el orden en que son colocados en la pila los parámetros de la función.
Después se indica si es la función que realiza la invocación ("Caller"), o la función llamada
("Called"), la responsable de sacar los parámetros fuera de la pila. Finalmente, se muestra el
efecto en el nombre de una función global.
quita Cambio
de
nombre
Modificador Colocación de Quién
parámetros los parámetros (sólo en lenguaje C)
__cdecl
Derecha a izq. func. invocante se añade '_' como prefijo
__fastcall
Izq. a derecha func. invocada se añade '@' como prefijo
__pascal
Izq. a derecha func. invocada se convierte a Mayúsculas
__stdcall
Derecha a izq. func. invocada Sin cambio
Tabla 2: Efecto de los diferentes modificadores de convención de llamada
2.5.
Entornos de desarrollo y DLLs. Herramientas.
Ahora se estudiarán algunos entornos de desarrollo disponibles para la programación de DLLs
para las simulaciones y se analizarán qué particularidades, ventajas y aspectos a tener en
cuenta para realizar estas tareas.
2.5.1. Borland C++ Builder 5
2.5.1.1.
Descripción del entorno
Borland C++ Builder es un entorno integrado de desarrollo (eminentemente visual) con el que
se pueden diseñar, compilar y depurar aplicaciones C++ con una mínima escritura manual de
código. Es el usado principalmente dentro del grupo Test Means en el departamento de
Ingeniería de Sistemas de Avión para el desarrollo del sistema CATS. Es un entorno bastante
intuitivo y ya conocido por lo que es ideal para el desarrollo de simulaciones sin preocuparse
de aprender el manejo de nuevos entornos ni adquirir otros que acarreen un coste mayor al
desarrollo.
19
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
Como se puede ver en la figura 3, el entorno es muy intuitivo: consta de una ventana principal
(la superior) con todas las opciones al alcance del usuario, como la gestión de los proyectos y
archivos, compilación, ejecución, depuración y el conjunto de componentes visuales para
insertar en los formularios. En la ventana izquierda se pueden editar las propiedades de cada
uno de los componentes insertados y la ventana central es la de edición, depuración y
ejecución de código.
Figura 1: Vista principal del entorno de desarrollo Borland C++ Builder 5
Este entorno emplea un conjunto de clases llamadas VCL (Visual Component Library), que son
colecciones de objetos escritos en el lenguaje Pascal (por herencia de otro entorno de
desarrollo de Borland, el Delphi). Se trata de una serie de recursos pre-construidos de los que
puede echar mano el programador para integrarlos en sus aplicaciones. De esta manera, en
lugar de emplear los métodos clásicos de programación con el API Win32 se emplean éstos
para aumentar la productividad. Eso sí, a costa de tener que aprender a usar este conjunto de
librerías. Como se parte de que se ha usado frecuentemente este entorno, en una gran parte
esto ya no supone un problema.
Este entorno proporciona además asistentes para la creación de diferentes proyectos típicos
frecuentes. Uno de ellos es el de la creación de una DLL, que pasaremos a estudiar a
continuación.
2.5.1.2.
El asistente DLL (DLL Wizard). Creación de una DLL.
Para entrar en este asistente, sólo hay que dirigirse al menú File y a continuación pulsar sobre
la opción New…
20
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
Figura 2: Creación de nuevos ítems y el asistente DLL de Borland C++ Builder
En la ventana que se muestra (figura 4) nos da a elegir entre una serie de opciones para crear
nuevos proyectos, librerías, archivos, etc. Pulsando sobre DLL Wizard se entra en dicho
asistente (figura 5) donde se nos da a elegir por un lado el lenguaje con el que se programará
(C o C++), si se desea usar las librerías VCL o no y una última opción para que el código
resultante sea acorde a las características del entorno Visual C++ de Microsoft (esta última
opción no se usará).
Eligiendo lenguaje C++ y usando las VCL, se creará un archivo que sirve como “esqueleto” a la
hora de empezar a programar una DLL. Este esqueleto tiene la siguiente forma:
#include <vcl.h>
#include <windows.h>
#pragma hdrstop
//--------------------------------------------------------------------------// Important note about DLL memory management when your DLL uses the
// static version of the RunTime Library:
//
// If your DLL exports any functions that pass String objects (or structs/
// classes containing nested Strings) as parameter or function results,
// you will need to add the library MEMMGR.LIB to both the DLL project and
// any other projects that use the DLL. You will also need to use MEMMGR.LIB
// if any other projects which use the DLL will be performing new or delete
// operations on any non-TObject-derived classes which are exported from the
// DLL. Adding MEMMGR.LIB to your project will change the DLL and its calling
// EXE's to use the BORLNDMM.DLL as their memory manager. In these cases,
// the file BORLNDMM.DLL should be deployed along with your DLL.
//
// To avoid using BORLNDMM.DLL, pass string information using "char *" or
// ShortString parameters.
//
// If your DLL uses the dynamic version of the RTL, you do not need to
// explicitly add MEMMGR.LIB as this will be done implicitly for you
//--------------------------------------------------------------------------#pragma argsused
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
return 1;
}
21
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
Vemos en primer lugar que además de cargar la librería windows.h se carga la correspondiente
para poder usar la librería de clases VCL, llamada vcl.h. Por otro lado, la directa #pragma
hdrstop sirve para las cabeceras precompiladas, que es una técnica de este entorno que
permite compilar ficheros de cabecera (e incluso código fuente) sólo una vez, y reutilizar este
código ya compilado. Esto elimina la necesidad de que el compilador tenga que recompilar los
ficheros de cabecera de cada uno de los ficheros fuente del proyecto. Este hdrstop sirve para
indicarle al compilador que guarde el estado de compilación de este archivo.
A continuación hay una advertencia sobre si se exporta objetos tipo String de VCL, en el que
hay que incluir una librería de Borland para que funcione adecuadamente.
Por último, vemos algo que ya se indicaba en capítulos anteriores. En lugar de usar como
función de punto de entrada de la DLL el DLLMain se usa un nombre diferente en este
entorno: DLLEntryPoint.
Todo lo indicado anteriormente para la programación de DLLs se cumple para este entorno, las
funciones y sus parámetros son los mismos. Eso sí, hay que tener en cuenta algunos detalles:


La convención de llamada por defecto en este entorno es la del C. Por lo tanto, si
queremos pasar a una convención estándar, hay que usar el modificador __stdcall (o
bien, como se ve en el esqueleto, usar el especificador de Windows WINAPI
equivalente a __stdcall).
Hay que tener en cuenta el problema del planchado de nombres o “name mangling”,
que se describirá a continuación.
2.5.1.3.
El problema del “name mangling”
El entorno Borland C++ maneja tanto el lenguaje C como el C++. Cuando el compilador C++
traduce un módulo en el que existen funciones, los identificadores originales son
reemplazados por una versión distorsionada que incluye de una forma codificada los tipos de
argumentos utilizados por la función. Esta distorsión o deformación de nombres, conocida
también como decoración o planchado de nombres ("name mangling"), es la que hace posible
la sobrecarga de funciones. Puesto que aunque dos funciones compartan el mismo nombre, si
son distintos los tipos de argumentos, las versiones internas ("planchadas") de tales nombres
son distintas.
Además, la decoración de nombres es un mecanismo C++ de seguridad (de comprobación de
tipos), que ayuda al enlazador a comprobar si las invocaciones a funciones situadas en otros
módulos son correctas (respecto a la idoneidad de los argumentos utilizados).
Hay que tener en cuenta que este planchado de nombres de las funciones no está especificada
por el estándar C++, de forma que es normal que dos compiladores de fabricantes distintos
planchen las funciones de forma diferente (por ejemplo, Visual C++ de Microsoft y Borland C++
Builder no lo hacen igual). El resultado es que, salvo que se tomen precauciones especiales, no
está garantizado que las librerías construidas con un compilador C++ funcionen con otro. Con
esto se perdería una de las ventajas fundamentales que tienen las librerías DLL.
22
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
Para evitar el planchado de nombres, hay que utilizar la declaración extern “C” en la
declaración (prototipo) de la función. Se puede indicar para una sola función o bien para un
grupo o bloque de enlazado:
extern "C" void funcion(int); // para una función
extern "C" {
// para un bloque de enlazado
void funcion1(int);
void funcion2(int);
void funcion3(int);
};
extern "C" {
// para todas las de un fichero de cabecera
#include "milibreria.h"
};
Naturalmente lo anterior tiene un coste: la ausencia de posibilidad del mecanismo de
sobrecarga de funciones en el módulo en el que se evita la decoración de nombres.
2.5.1.4.
Depuración de DLLs
Una de las dificultades que conlleva la programación viene de la depuración de código. En el
caso de las DLLs, se puede optar por emplear por dos mecanismos diferentes:


Como las DLLs necesita de un código que la invoque, se podría pensar en hacer dos
cosas a la vez: el código de la librería y en el código de un programa que además de
cargarla permita depurarla integrando herramientas para el paso de parámetros,
control de carga, lectura de variables, etc. con lo cual no sería necesario ahondar en las
peculiaridades que tiene cada entorno en el tema de la depuración. Esta opción quizás
parece más inmediata pero si el proyecto es un poco largo puede resultar al final muy
engorroso.
Directamente depurar la DLL usando las herramientas propias del entorno sobre
depuración, bien a través del código directo de la DLL o bien el programa que invoca
una DLL. Si lo que se está programando es una aplicación que invoca una DLL de la que
no poseemos su código la depuración en este caso no será posible (o no al menos de
forma sencilla, apareciendo el código en lenguaje ensamblador).
Borland C++ Builder tiene poderosas herramientas de depuración que además ya se conocían
por experiencia del uso del entorno y que se usaron para la simulación que se verá más
adelante, aunque no se usó para depurar DLLs. Para poder depurar DLLs hay que configurar
varios parámetros dentro del entorno y otros aspectos a tener en cuenta, pero se comprobó
que para el tipo de programas que se iban a crear no era la solución más factible. Más
adelante, cuando se hable sobre las simulaciones de fuel, se comentarán todas las razones,
pero diremos ahora como adelanto que todas las DLLs de las simulaciones tienen una
característica común: todas constan de 3 funciones de igual nombre (con lo que hacer un
programa de carga de DLLs de simulaciones sería común a todas ellas) y los datos obtenidos
con ellas pueden obtenerse y visualizarse rápidamente usando el entorno SEAS.
23
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
2.5.2. MinGW
MinGW, contracción de Minimalist GNU for Windows, es una implementación de GNU
Compiler Collection (GCC) y GNU Binutils para el desarrollo de aplicaciones nativas del sistema
Windows. Se trata de una rama diferente seguida en el desarrollo de otro entorno, el Cygwin,
del que luego se hablará. Se puede decir que es una versión reducida de éste, ya que hay
conjuntos de librerías no disponibles para darle una mayor simplificación.
MinGW por tanto sólo es un compilador y un conjunto de librerías. No se trata de un entorno
de desarrollo como Builder. La gestión de los archivos de un proyecto se tiene que hacer de
forma manual, al igual que la edición de su código, no hay librerías de clases propias para su
uso…
La principal ventaja que tiene es muy probablemente que todo es más “estándar”. La
programación es más clásica, en el sentido en que se usan funciones y recursos muy conocidos
de los estándares del C, C++ y del API de Win32. Al compilar DLLs, no se produce planchado de
nombres, con lo que produce una librería en teoría con mejor compatibilidad para que
cualquier programa la pudiera cargar.
Este es el motivo por el que se usó al comienzo de la programación de simulaciones. Se
desconocían todas las medidas a tener en cuenta en Builder y, hasta que se consiguió
comprenderlas, el MinGW permitía obtener mientras lo que se buscaba. También consiguió
solucionar algunos problemas de incompatibilidades de algunas aplicaciones que había que
usar en simulaciones presentaban con el entorno de Borland.
Para generar una DLL se compila de una forma estándar de GCC pero usando un especificador,
el –shared, y estableciendo como extensión en el archivo .dll en lugar de .exe:
gcc -c midll.c
gcc -shared -o midll.dll midll.o
Como punto negativo, todo aquello que aporta Builder que como se ha comentado no aporta
MinGW, especialmente el uso de las librerías VCL y el diseño de la GUI, que en algunos
aspectos acelera enormemente la programación.
2.5.3. Cygwin
Como se ha dicho anteriormente, MinGW comenzó su desarrollo basándose en Cygwin.
Cygwin es algo más que un compilador, es una colección de herramientas desarrolladas para
proporcionar un comportamiento similar a los sistemas Unix en Windows. Para ello dispone de
un Shell, el bash, que simula el de los entornos Unix.
Éste es el tercer entorno empleado para la programación de simulaciones. El motivo de pensar
en el uso de Cygwin fue que MinGW tenía algunas limitaciones por ser una versión reducida de
éste. Por ejemplo, algunas librerías, como las de programación basada en hilos y programación
de comunicación por puerto de serie, no estaban disponibles.
Un inconveniente de este entorno frente a MinGW es que es necesario un archivo llamado
Cygwin1.dll para cualquier aplicación programada, incluídas las DLLs. Hay que tener cuidado
con la localización de este archivo en el sistema.
24
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
Quitando estos cambios y añadidos, por lo demás es igual a MinGW.
2.5.4. Microsoft Visual C++ 6
Este es otro de los entornos de desarrollo de los que se dispone para poder usarse para el
desarrollo de DLLs. De la misma forma que el Borland C++ tiene sus propias librerías de clases
(VCL), Visual C++ tiene las suyas, llamadas MFC (Microsoft Foundation Classes).
Figura 3: El interfaz gráfico de usuario del entorno de desarrollo Microsoft Visual C++ 6
Estas librerías de clases no se han empleado para ningún desarrollo, por lo que su uso
implicaba necesariamente tener una documentación y un estudio previo, por tanto parecía
más indicado el empleo el entorno de Borland en lugar de éste. Sin embargo, junto con este
entorno se poseía el código de una librería escrita para este entorno para leer el contenido de
archivos Excel al igual que el entorno de Borland, que para algunas aplicaciones como más
adelante se verá resultó de gran utilidad debido a algunas posibles incompatibilidades de
Borland.
El entorno es muy similar al de Borland C++ Builder, incluso en la distribución de los diferentes
elementos en pantalla en el entorno gráfico. Esto y el hecho de que se usará código estándar C
con sus funciones y no su librería de clases (a diferencia de Borland) hará que su uso sea muy
intuitivo y que no sea necesario ahondar demasiado en sus características.
2.5.5. La herramienta Dependency Walker
Visto anteriormente el problema conocido como “name mangling” y las diferentes
convenciones de llamadas en DLLs, existen herramientas muy útiles para ver el contenido
interno de DLLs de las que no se dispone de su código y/o de una documentación adecuada.
También para cerciorarse de los resultados tras la compilación de una DLL en especial sobre
este tema del planchado de nombres.
Una de esas herramientas es la llamada Dependency Walker. Es gratuita y escanea no sólo
DLLs sino también todo tipo de módulo de Windows de 32 o 64 bits (exe, dll, ocx, sys, etc.)
25
Software de desarrollo de simulaciones para las pruebas funcionales del avión A400M
Figura 4: La herramienta Dependency Walker
Como se puede ver en la figura 6, una vez cargado un archivo .dll se puede consultar
información variada sobre él: el tipo de exportación (C, C++, …), el ordinal de la función u otro
recurso exportado, el nombre (con el que se puede ver inmediatamente si está planchado o
no), el punto de entrada, las dependencias con los distintos módulos (en el caso de librerías
compiladas con Cygwin aparecería la dependencia necesaria con el archivo cygwin1.dll del que
se hablaba antes), etc.
Haciendo doble clic sobre una de esas dependencias se consulta información en línea sobre
ella en la web de desarrolladores de Microsoft, aunque no va a ser necesario para el tema de
programación de simulaciones. También indicará si hay conflicto dentro de las funciones de la
DLL con algunas de esos módulos.
Descargar