Las DLL tuvieron su origen cuando se quiso compartir código común entre diferentes aplicaciones para así ahorrar algo de espacio en disco (posiblemente espacio en disquetes). Esto requirió que Windows implementara ciertos modelos de memoria bastante complicados para la época (de la historia de Unix/Linux poco sé, si la idea de compartir bibliotecas en tiempo de ejecución fue anterior o posterior –supongo que anterior, aunque vaya usté a saber-), pero era hacerlo así o limitar gravemente a Windows.
Ese modelo de memoria permitió otras cosas, como que las DLL solo pudieran tener un juego de datos para todos los programas que las usaran (con la posibilidad también de contener datos únicos para cada aplicación), ahorrando todavía más memoria. Una desventaja es que un programa maleducado podía tirar al suelo no sólo a otros que usaran una misma DLL, sino al propio Windows, y eso teniendo en cuenta que la DLL estuviera bien construida y no fuera la propia DLL la que tumbara al sistema.
Ahora las cosas son bastante diferentes, cada DLL se ejecuta en el mismo espacio de direcciones que la aplicación que la llama, de manera que ya no comparten memoria ni datos: cada aplicación tiene una instancia independiente que no afecta a otras en ejecución, por lo que el motivo principal se ha perdido y ahora cumplen otros roles no menos importantes.
Tipos de DLL
Hasta la entrada del .NET Framework, existían cuatro tipos de DLL que, dependiendo de su rol, podían contener una u otra cosa.
· DLL normal de código. Es un conjunto de funciones y funcionalidad más o menos destinada a ser utilizada por otros programas. El propio Windows está construido en base a esta característica: Las DLL kernel32.dll, gdi32.dll y user32.dll forman el núcleo del sistema operativo, al menos del subsistema de Win32/Win64.
· DLL de recursos. Como su nombre indica, sólo contienen recursos y nada de código (o el menos posible). Un recurso puede ser un bitmap, una cadena de texto o la definición de un cuadro de diálogo. Este tipo de DLL es muy útil para construir aplicaciones internacionales en las que las cadenas en cada idioma están almacenadas en una DLL diferente, de modo que cuando se carga el programa podemos indicar qué DLL de recursos cargar, teniendo así nuestra aplicación, de forma casi automática, disponible en todos los idiomas que queramos. El propio Windows lo hace así con las versiones MUI, que incluyen todos los textos y otros elementos en DLL normales pero renombradas con la extensión MUI. Usando este mismo paradigma, podemos tener soporte de temas y resulta mucho más fácil cambiar o actualizar los recursos de una aplicación.
· Contenedores COM, DCOM o ActiveX. Este tipo de DLL apareció relativamente tardíamente, aunque su utilidad es más que destacable, y más que un formato de DLL se trata de un subsistema de Windows que se apoya fuertemente en la DLL para funcionar: en general la mayoría de objetos COM se encuentran almacenados en este tipo de ficheros, si bien muchas veces se les cambia la extensión por OCX, aunque no es imprescindible.
· DLL de extensión. Aunque a primera vista podrían estar incluidas en el primer tipo, la construcción de estas necesita de ciertos cuidados especiales o nos encontraremos con serios problemas en su uso y su creación. Una DLL de extensión extiende la funcionalidad de alguna aplicación o marco de trabajo o del propio Windows.
Tras la entrada en escena del .NET Framework, los tipos de DLL se han extendido, o más bien los tipos anteriores han aumentado sus roles así como sus nombres y la división clara entre una clase y otra se ha perdido en cierta manera. En general una DLL .NET ya no se llama así, sino que se llama ensamblado y puede cumplir uno o varios roles anteriores. Los ensamblados normales ahora contienen una buena cantidad de metadatos que los convierten en un paso más allá de los contenedores COM, no suelen incluir código ensamblador sino que en su interior sólo hay instrucciones MSIL y Windows es incapaz de entenderlas sin el soporte del motor en tiempo de ejecución (runtime) de .NET.
Evidentemente, la división anterior es solo conceptual, ya que nadie nos impide tener una DLL que contenga código, recursos y extienda un marco de trabajo existente o se inserte como un servidor COM dentro de Windows. No obstante, y en general, los programadores suelen respetar la división.
Usos modernos de una DLL
Ya hemos visto algunos en el punto anterior, aunque podemos indicarlos para mayor claridad.
· Interfaz con un Driver. Muchas veces cuando tenemos que acceder a un dispositivo más o menos exótico el fabricante nos suministra una DLL que nosotros usaremos para comunicar nuestra aplicación con el dispositivo.
· Soporte de alguna funcionalidad suministrada por un tercero. Si bien también nos podría dar una biblioteca estática para enlazar con nuestra aplicación, el disponer de una DLL significa que ésta también podrá ser usada en otros lenguajes.
· Soporte de temas. Ponemos todos los recursos de una aplicación en una DLL y el hecho de cambiar de tema supone usar otra DLL con los mismos nombres y tipos de recursos pero diferentes en aspecto.
· Soporte de idiomas. Ponemos todas las cadenas de nuestra aplicación en una DLL y la cargamos en consonancia al idioma seleccionado. Aquí tenemos la desventaja de tener que cargar a mano con LoadString() todas las cadenas antes de usarlas. Por ejemplo, MFC ayuda en cierta manera a disponer de este sistema de internacionalización, y otras herramientas como C++Builder o el propio .NET las implementan de forma automática, generando y cargando la DLL adecuada en cada caso sin nuestra intervención.
· Ofrecer servicios actualizables. Esta es una gran ventaja, que utiliza el propio Windows y Visual C++ para insertar actualizaciones. Si ponemos cierta funcionalidad en una DLL, cuando queramos actualizarla sólo tendremos que cambiar dicha DLL por otra nueva que conserve el mismo interfaz público (o al menos la parte usada). Es lo que hace Microsoft con el runtime de Visual C++ cuando se produce alguna actualización de seguridad o corrección de bugs: en Windows Update nos aparece la correspondiente actualización, que sustituirá (más bien añadirá) la nueva versión al repositorio SXS.
· Modificar el comportamiento del sistema añadiendo ganchos (hooks) a Windows. Un ejemplo a esto es insertar una DLL para que actúe de filtro de teclado, dando funcionalidad a las teclas multimedia de nuestro teclado, y una variante es insertar una DLL en otro proceso para tomar el control (o permitir más funcionalidad), que es lo que hace, por ejemplo, Skype (Y no sé para qué).
· Compartir código entre aplicaciones. Aunque a fecha de hoy el ahorro de disco puede ser insignificante, disponer de DLLs con funcionalidad común puede suponer solucionar un bug o realizar algún tipo de mejora de forma simultánea en todas las aplicaciones instaladas que usen dicha DLL. Y por supuesto también está su contrapartida: estropear algo en todas.
De todos modos, esta lista sólo es una aproximación. Seguro que el lector conoce otros usos o se los puede imaginar.
Requisitos para ser una DLL "estándar"
Con esto nos referimos a los requisitos para que una DLL pueda ser utilizada por prácticamente cualquier otro lenguaje de programación, como puede ser el Visual Basic clásico, los lenguajes .NET, Cobol, Dephi, Fortran, etc., e incluso aplicaciones con soporte para ellas como Mathlab y otras.
· El interfaz de programación ha de consistir en funciones globales con el protocolo de llamada __stdcall de paso de parámetros. Cuando uno hace una llamada a función, el punto de retorno y los parámetros que vamos a usar se guardan en la pila. Dependiendo del protocolo de llamada, la limpieza de la misma corresponderá a quien llama o al llamado, así como el orden en el que los parámetros se guardan en ella también depende del protocolo. Como su nombre indica, __stdcall especifica un formato que todos los lenguajes deberían entender si quieren comunicarse con el mundo exterior. Hasta donde yo sé no existe ningún lenguaje moderno que pretenda soporte de extensiones y que no entienda el protocolo __stdcall.
· Si hay variables globales, éstas deben ser de los tipos más estándar posibles, como enteros con un tamaño prefijado. Nada de punteros ni estructuras complejas ni otros elementos dependientes de un compilador en concreto, aunque lo mejor es que no haya ningún tipo de elemento global aparte de las funciones.
· Los parámetros pasados y devueltos han der ser tipos estándar y perfectamente definidos, y debemos huir como de la peste de punteros a void y burradas similares (aunque a veces son inevitables).
· Nada de clases, ni objetos, ni funciones con extensiones no estándar (por ejemplo, paso variable de parámetros o parámetros por defecto).
· Disponer de un punto de entrada con la firma y la funcionalidad siguiente (Sacado de la MSDN, luego explicaremos esto, cualquiera de las dos firmas nos vale):
BOOL WINAPI DllMain( HINSTANCE hinstDLL, // handle to DLL module DWORD fdwReason, // reason for calling function LPVOID lpReserved ); // reserved
int __stdcall DllMain( HINSTANCE hinstDLL, // handle to DLL module DWORD fdwReason, // reason for calling function LPVOID lpReserved ); // reserved
No obstante, muchas de estas reglas son relajables. La mayoría de lenguajes pueden lidiar con punteros y con estructuras de datos, otros no necesitan para nada el punto de entrada, y otros también trabajan bien con el protocolo de llamada __cdecl, que es el estándar de C y C++.
Con lo que sí que no suelen poder es con las extensiones de C++ como las clases, los parámetros por defecto o variables, el protocolo de llamada __fastcall y otras extensiones que muchos compiladores de C++ suministran, como gestión de excepciones y punteros relativos.
Esto no quiere decir que no podamos usar esas cosas en nuestra DLL, que sí podemos, lo que quiere decir es que el interfaz público con el que se debe comunicar la DLL no debe tenerlos. Luego, en nuestro código, podremos usar lo que queramos siempre que no salga fuera. Si usamos excepciones deberemos capturarlas dentro de nuestra DLL y no lanzarlas fuera a no ser que sean del tipo del sistema operativo (y en ese caso tampoco es recomendable).
Aunque pueda parecer extraño, ya que la mayoría de lenguajes modernos implementan clases, las de C++ están prohibidas en la parte pública de una DLL. Eso se debe a la forma en que C++ genera el código. Cuando C++ compila una clase, genera una lista de funciones globales del tipo "MiClase@MiMetodo@&D%&", que es la forma que tiene el compilador de empaquetar toda la información sobre el tipo de la función o de la variable. Él sabe cómo se llama el método, y lo llamará de la forma adecuada dentro del código fuente, pero externamente todos esos añadidos no son estándar y suelen variar de compilador a compilador (e incluso entre versiones del mismo), por lo que si exportamos una clase para que sea vista públicamente en nuestra DLL, la DLL exportará dicho listado de funciones.
Realmente se puede hacer, pero luego tenemos que figurarnos a qué método se corresponde cada función y simular el funcionamiento del objeto desde la aplicación que esté usando la DLL a no ser que nuestra aplicación esté escrita en C++ y compilada con el mismo compilador. En ese caso podemos exportar y usar una clase (o cualquier otro tipo de elemento), pero el consumo ha de ser interno.
Y a veces nos encontramos con esta misma situación cuando usamos una DLL de un tercero: éste la ha generado y comprobado con su compilador y en su sistema, y luego el usuario final se encuentra con un batiburrillo de estructuras, punteros, punteros a void y demás zarandajas que le hacen muy difícil usarla en su sistema.
Por eso lo de las reglas.
En una próxima entrada veremos los aspectos prácticos de todo esto.