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.