untio

Registrado: 17 Sep 2008 Mensajes: 380 Ubicación: MICA S.A.
| Publicado: 23/04/2011 7:49 am | | | Título: Como crear una dll |
| Hola, Voy a crear una dll con wxDevC++, la versión viva de DevC++. El código de la dll, en mi caso, consiste en un solo fichero fuente. Ahí va:
| Código: | #include <windows.h> #include <stdio.h>
BOOL APIENTRY DllMain (HINSTANCE hInst /* Library instance handle. */ , DWORD reason /* Reason this function is being called. */ , LPVOID reserved /* Not used. */ ) { switch (reason) { case DLL_PROCESS_ATTACH: break;
case DLL_PROCESS_DETACH: break;
case DLL_THREAD_ATTACH: break;
case DLL_THREAD_DETACH: break; }
/* Returns TRUE on success, FALSE on failure */ return TRUE; } ///////////////////////////////////////////////////////// int _stdcall escribeint(int num) { printf("%d", num); return (num * 2); } ///////////////////////////////////////////////////////// int _cdecl escribeotroint(int num) { printf("%d", num); return(num * 3); }
|
Vamos a explicar algo acerca de DllMain:
Esta función es llamada por windows cada vez que un proceso o thread carga o descarga la dll. Esto puede ocurrir cuando inicias un programa que se ha linkado con el fichero .a (.lib para visual c++) ó cuando cargas la dll dinámicamente. Luego explicaré cómo.
En 9999 de 10000 casos sólo habéis de retornar TRUE. Si retornais FALSE, el sistema no cargará la dll y vuestro programa no podrá usarla. Es una función bastante puñetera:
1. No se pueden usar desde ella todas las funciones del api de windows (porque puede haber una referencia circular).
2. Sólo es recomendable usarla para lo que los de Microsoft llaman TLS (thread local storage). Me explico: Si usáis variables globales en el código de la dll cada proceso que haga uso de ella tendrá su propia copia en su propio espacio, por ahí no hay peligro.
El peligro está en que uno de vuestros programas tenga más de un thread (o hilo de ejecución. Por ejemplo, cuando se hace un programa que ejecuta una tarea intensa, pero se ha de mantener una ventana, por ejemplo, con un contador de lo que tarda la tarea y un botón para cancelarla, se ejecuta la tarea intensa a la vez que se responde a los mensajes de la ventana. Para ello se crea un nuevo thread.
Bueno, si el programa tiene más de un thread y más de 1 accede a la variable global de la dll, estarán accediendo a la misma copia. Si esto puede ser peligroso se utiliza TLS, pero en la mayoría de los casos, no es necesario.
3. Por ello, lo ideal, es que en la dll las variables sean locales a las funciones.
4. Si habéis de hacer cierta inicialización para la dll, lo ideal es hacerlo en una función normal. No lo hagáis en DllMain. Lo único que tenéis que hacer en ella es : return TRUE;
Si os fijáis en las otras 2 funciones, he escrito cada una de ellas con una convención de llamada diferente. No os aconsejo que las declaréis así:
#define EXPORT __declspec(dllexport) EXPORT int _stdcall escribeint(int num)
Aunque si vais a usar el mismo compilador para invocarla podéis declararla para el ejecutable que enlaza con ella como:
#define EXPORT __declspec(dllimport) EXPORT int _stdcall escribeint(int num)
La convención de llamada indica cómo se pasan los parámetros a una función y como se restaura el estado de las cosas cuando la función devuelve el control.
_stdcall significa Convención de Llamada eStándar. De estándar no tiene nada, es la que se usa en las llamadas a las librerías de Windows. Es un invento de Microsoft.
_cdecl es la convención de llamada que usan los compiladores de C.
Si la función que llama y la función llamada no están de acuerdo con la convención de llamada lo más normal es que vuestro programa falle.
Como opciones para el linker escribo:
-Wl,--output-def=midef.def
-Wl, se usa para pasar opciones al enlazador (linker) separadas por comas. La opción que le paso es que me cree un fichero def que se va a llamar midef.def (original que es uno) al terminar el enlazado.
Compiláis y os encontrais vuestra librería para añadir al linker cuando queráis usar vuestra dll y vuestra dll, pero es que todavía no hemos terminado.
Vamos a centrarnos en el fichero midef.def. Lo abrimos y vemos:
EXPORTS _Z10escribeinti@4 @1 _Z14escribeotrointi @2 _Z7DllMainP11HINSTANCE__mPv@12 @3
Son los nombres de las funciones que se pueden llamar desde cualquier proceso. Con ese nombre las conoce el linker. Y, si no lo remediamos, son los nombres que habrá que suministrarle al linker cuando añadamos el fichero .a.
Pero imaginad que habéis de cargar la dll dinámicamente y usar una función. Para ello habéis de suministrar su nombre.
Por eso en el fichero def, podemos declarar una especie de alias para nuestras funciones. Quedaría así:
EXPORTS _Z10escribeinti@4 @1 _Z14escribeotrointi @2 escribeint = _Z10escribeinti@4 @3 escribeotroint = _Z14escribeotrointi @4
Ahora cada una de las 2 funciones tiene 2 nombres y podéis añadir otros cuantos.
Como veis he quitado la referencia a DllMain. Esa función siempre es pública. No es necesario exportarla.
Cambiamos las opciones del linker a:
-Wl,midef.def
Volvemos a compilar.
Y ya está compilada nuestra dll.
Vamos ahora a llamar a nuestras funciones desde un programa externo.
Primero cargaremos la dll estáticamente:
| Código: | #include <cstdlib> #include <iostream>
using namespace std;
extern int _stdcall escribeint(int num); extern int _cdecl escribeotroint(int num);
int main(int argc, char *argv[]) { cout << escribeint(4) << endl; cout << escribeotroint(8); system("PAUSE"); return EXIT_SUCCESS; }
|
Fijaos en que declaro las funciones como extern y que pongo la convención de llamada en cada una de ellas.
Naturalmente, hemos de añadir al linker como librería el fichero .a que ha sido creado con la dll.
Ni que decir tiene que si exportáis muchas cosas lo óptimo es poner las declaraciones en un fichero .h
Si ejecutáis el programa veréis que funciona perfectamente.
Si queréis usar la dll con un compilador distinto, lo normal es que genere un error porque posiblemente el linker busque otro nombre para la función. Es posible que otro linker llame:
escribeint@4
A lo que el linker de devc++ llama:
_Z10escribeinti@4 Solución: Añadir al fichero .def:
escribeint@4 = _Z10escribeinti@4 @5 (5 o el número que toque)
Ahora vamos a ver cómo se ejecuta el código de la dll dinámicamente.
Ahí va el código:
| Código: | #include <windows.h> #include <cstdlib> #include <iostream>
using namespace std;
typedef int _stdcall (*miint1)(int); typedef int _cdecl (*miint2)(int num);
int main(int argc, char *argv[]) { HMODULE hmanejador; miint1 funcion1; miint2 funcion2; hmanejador = LoadLibrary("Proyecto9.dll"); if(hmanejador == NULL) { printf("No he podido cargar la dll.\n"); return 1; } funcion1 = (miint1) (GetProcAddress(hmanejador, "escribeint")); if(funcion1 == NULL) { FreeLibrary(hmanejador); printf("No he podido encontrar la función.\n"); return 1; } funcion2 = (miint2) GetProcAddress(hmanejador, "escribeotroint"); if(funcion2 == NULL) { FreeLibrary(hmanejador); printf("No he podido encontrar la función 2.\n"); return 1; } cout << funcion1(4) << endl; cout << funcion2(8); FreeLibrary(hmanejador); system("PAUSE"); return EXIT_SUCCESS; }
|
He añadido el fichero de cabecera: windows.h Porque es lo que se necesita para usar las funciones que utilizo para hacer el trabajo.
typedef int _stdcall (*miint1)(int);
Declaro un nuevo tipo de dato llamado miint1 que será un puntero a una función que acepta un entero, devuelve un entero y sigue la convención de llamada estándar (estándar sólo para Microsoft).
typedef int _cdecl (*miint2)(int num);
Declaro un nuevo tipo de dato. Lo mismo que antes pero sigue la convención de llamada de C.
HMODULE hmanejador;
Imaginad la de dlls y exes que hay en ejecución en vuestro sistema. Para referirse a cada uno de ellos se ha de tener algún identificador. Para eso se usan los manejadores.
miint1 funcion1;
Había definido el tipo de dato miint1. Ahora creo una instancia de este tipo de dato.
hmanejador = LoadLibrary("Proyecto9.dll");
En mi caso, la dll se llama "Proyecto9.dll". Has de poner el nombre de tu dll.
En este momento será llamada la función "DllMain" de tu dll. La carga continuará sólo si DllMain devuelve TRUE. Si no, la función fallará.
funcion1 = (miint1) (GetProcAddress(hmanejador, "escribeint"));
Recuerdas que en el fichero midef.def Le habíamos dado un nombre decente a la función, aquí está el por qué. Si no lo hubieramos hecho deberíamos escribir _Z10escribeinti@4 en lugar de escribeint.
Ahora solo nos queda hacer uso de nuestra función:
cout << funcion1(4) << endl;
Sí, se usa funcion1 como si fuese la función más normal del mundo.
Cuando nos hemos cansado de usar nuestras funciones, liberamos la librería para que el sistema vaya más desahogado:
FreeLibrary(hmanejador);
Le pasamos el manejador (el identificador, para que el sistema sepa qué librería queremos liberar).
Tranquilo, si otro proceso está usando la dll ésta seguirá en memoria para él.
Imaginad que tenéis una función o un módulo que usáis muchísimo en vuestros programas. Podéis optar por ponerlo en una dll y os ahorraréis el Copiar/Pegar.
Espero no haber cometido muchos errores. Y que me perdonéis por ellos.
REEDITADO: ME HE DADO CUENTA DE QUE HE MEZCLADO C Y C++. NO SE DEBE DE HACER, PERO ES QUE HE ESTADO CENTRADO EN LA DLL. PERDONAD. NOAFECTA A LA DLL. QUE ME PERDONEN LOS GURUS. |
|