Crear una DLL

Imagen DLL

Una de las mayores ventajas de los sistemas operativos modernos es que permiten usar librerías de enlace dinámico, que es lo que significa DLL.

C y C++ hacen uso de librerías desde su inicio. Las librerías son una de las formas de reutilizar código, lo que da una enorme potencia a los lenguajes de programación que pueden hacer uso de ellas.

Reutilizar código tiene muchas ventajas. Hace que no tengamos que reescribir las mismas tareas una y otra vez. Podemos usar código escrito por otros programadores. También se puede usar el mismo código en varios programas.

Hay varias formas de reutilizar código. Una es compilar un programa a partir de varios ficheros fuente. Algunos de esos ficheros pueden ser reutilizados en varias aplicaciones. Esta es la forma más simple, siempre y cuando tengamos acceso a los ficheros fuente.

Otro modo es usar código precompilado, es decir, incluir el código en la fase de enlazado a partir de ficheros objeto.

Dentro de esta última forma tenemos las librerías de enlace estático o librerías estáticas, que no se diferencian demasiado de los ficheros objeto. Para usar librerías estáticas necesitaremos incluir un fichero de cabecera que contenga las declaraciones de tipos, funciones y clases que se definen en la librería.

Las librerías de enlace estático, como las que hemos usado a menudo, y que forman parte de las librerías estándar de C y C++, destinadas al tratamiento de cadenas, funciones matemáticas, manipulación de ficheros, etc, se incluyen en la fase de enlazado y su código se añade al de nuestros programas en el mismo fichero ejecutable.

Por último disponemos de las librerías de enlace dinámico.

Al contrario que en las librerías estáticas, el código asociado a una DLL no se incluye en el fichero ejecutable de nuestra aplicación, sino que reside en otro fichero, y las direcciones de las funciones o clases incluidas en la DLL se obtienen de forma dinámica, es decir, durante la ejecución, y no durante la compilación del programa.

Al igual que con las librerías de enlace estático, para las DLL también tendremos que añadir un fichero de cabecera que declare los tipos, funciones y clases que se definan en la DLL.

Pero además del fichero de cabecera también tendremos que enlazar nuestro código con una librería de enlace estático. Esta librería es la encargada de calcular las direcciones de las funciones y métodos dentro de la DLL. Se trata solo de una tabla que contiene esas direcciones, por lo que no afecta demasiado al tamaño del ejecutable generado.

En algún momento, más pronto o más tarde, necesitaremos o querremos crear nuestras propias librerías, ya sea para usarla en nuestros programas o para compartir con otros desarrolladores y usar una DLL puede ser interesante en muchos casos.

Ventajas e inconvenientes

Las DLL tienen varias ventajas:

  • El fichero ejecutable es más ligero, ya que el código de la librería está en otro fichero diferente.
  • El mismo código en una librería puede ser ejecutado por varias aplicaciones que accedan a la misma DLL. Es decir, si varias aplicaciones usan la misma DLL, esa DLL solo se cargará una vez en memoria y su código puede ser compartido.

Entre las desventajas:

  • Un programa que use una DLL no funcionará si el fichero de la DLL no es accesible. Esto dificulta en cierta medida la instalación de aplicaciones. Este inconveniente no es demasiado serio, ya que probablemente nuestros programas ya estén usando DLLs sin que seamos conscientes de ello. En otro artículo explicaremos como averiguar qué DLL usa nuestra aplicación para poder incluirlas cuando sea necesario instalarlas en otros equipos.
  • Cuando se actualiza una DLL generalmente redunda en una mejora de prestaciones, pero en ocasiones puede provocar incompatibilidades o introducir bugs que pueden afectar a varias aplicaciones. Esto puede implicar que nuestras aplicaciones tengan que verificar si una versión concreta de una DLL está disponible en el sistema o no, y mostrar los mensajes de error correspondientes si es necesario.

Por el contrario, en librerías de enlace estático, el código se añade a cada ejecutable que las use, con lo que su tamaño aumenta, tanto en disco como en memoria. Ese código adicional solo estará disponible para ese programa, y no podrá ser compartido, pero no tendremos que preocuparnos por instalar ficheros adicionales o por conseguir una versión concreta de esos ficheros.

Crear DLL usando Code::Blocks

Cada IDE suele disponer de una plantilla para crear DLLs. En nuestro caso usaremos nuestro IDE de cabecera, Code::Blocks, que también dispone de una de esas plantillas.

Para empezar, crearemos un nuevo proyecto usando la plantilla "Dynamic Link Library", le asignaremos un título, y dejaremos las opciones para poder crear las versiones de depuración y de publicación, ya que probablemente sea necesario depurar nuestra DLL antes de liberar la versión de trabajo.

La plantilla creará dos ficheros; main.cpp y main.h, el segundo se usará, probablemente, para incluirlo en los programas que usen la DLL, y ambos en la creación de la propia DLL.

No te preocupes demasiado por el contenido de estos ficheros, probablemente no lo usaremos en su mayor parte, ya que mucho de él está orientado a aplicaciones que usen el API de Windows.

Como siempre, los ficheros de cabecera contendrán los prototipos de las clases y funciones, en este caso, que se incluirán en nuestra DLL.

Además de cualquier comentario que queramos añadir, como los datos de versión, fechas, revisiones, bugs y créditos, cualquier fichero de cabecera debe incluir detección y definición de una macro para la compilación condicional, como modo de evitar que las mismas declaraciones se incluyan varias veces desde diferentes ficheros fuente:

// Comentarios
#ifndef _IDENTIFICADOR_H_
#define _IDENTIFICADOR_H_

// includes

// macros

// declaraciones y prototipos

#endif // _IDENTIFICADOR_H_

Generalmente, el nombre de la macro suele coincidir con el nombre del fichero, usando mayúsculas y es costumbre añadir un carácter '_' al principio y al final, y sustituyendo al punto.

Dentro del bloque de compilación condicional añadiremos más secciones.

Ficheros include

Por supuesto, nada nos impide usar librerías externas, ya sean estáticas o incluso dinámicas. Para tener acceso a esas librerías incluiremos los ficheros de cabecera que consideremos necesarios, ya sean estándar o no.

Modificador export

No todas las funciones o clases que declaremos en el fichero de cabecera tienen que ser accesibles para las aplicaciones que las usen posteriormente. Algunas pueden ser privadas, para su uso exclusivo desde el interior de la propia DLL. Las funciones y tipos que queramos que puedan ser usadas por terceras aplicaciones deben ser exportadas por la DLL e importadas por la aplicación que use la DLL.

Para que el compilador sepa qué funciones y tipos deben ser accesibles tendremos que añadir un modificador a cada uno. Para exportar el modificador es __declspec(dllexport) y para importar __declspec(dllimport).

Por ejemplo:

int __declspec(dllexport) GetVersion();

Esto nos plantea un pequeño inconveniente si queremos usar el mismo fichero de cabecera para crear la DLL y para usarla en otros programas. Además, resulta engorroso usar esos modificadores, que resultan largos, entorpecen la lectura y es fácil escribirlos incorrectamente.

Lo que se suele hacer es crear una macro con un valor que dependa de la condición de si se está compilando la DLL o un ejecutable que la use. Esto se hace mediante directivas del preprocesador:

#ifdef BUILD_DLL
    #define DLL_EXPORT __declspec(dllexport)
#else
    #define DLL_EXPORT __declspec(dllimport)
#endif

Si nos fijamos en las opciones de proyecto creado con esta plantilla, en "Compiler settings", y "#defines", veremos que se define la macro "BUILD_DLL". Esto significa que cuando compilemos este proyecto esa macro estará definida, y "DLL_EXPORT" tomará el valor "__declspec(dllexport)". Por el contrario, cualquier otro proyecto para crear una aplicación que incluya este fichero de cabecera no habrá definido esta macro, y "DLL_EXPORT" tomará el valor "__declspec(dllimport)".

Ahora, cuando declaremos una función o tipo que queramos exportar usaremos la macro "DLL_EXPORT":

int DLL_EXPORT GetVersion();

Declaraciones

Los tipos y clases que declaremos en el fichero de cabecera con el modificador DLL_EXTERN estarán disponibles para cualquier aplicación que use la DLL, es decir, esas aplicaciones podrán crear objetos de esos tipos. Este será el comportamiento más normal si alguna función de nuestra DLL tiene parámetros o valores de retorno de este tipo.

También serán accesibles para las aplicaciones los miembros públicos de las clases y estructuras con el modificador DLL_EXTERN.

typedef DLL_EXPORT struct {
int x;
int y;
} punto2D;

#ifdef __cplusplus
extern "C"
{
#endif

void DLL_EXPORT ProcesaPunto(punto2D A);

#ifdef __cplusplus
}
#endif

Si queremos exportar solo algunos de los miembros de una clase o estructura, podemos usar el modificador únicamente en esos miembros.

class punto2D {
protected:
    int x;
    int y;
public:
    DLL_EXPORT punto2D(int xi=9, int yi=0);
    void SetX(int xo) { x=x0; }
    void SetY(int yo) { y=y0; }
};

Por supuesto, si exportamos todos los miembros públicos el resultado es el mismo. La utilidad de esto es limitada, salvo que queramos que ciertos métodos solo sean accesibles desde la propia DLL, o que en una clase derivada solo queramos dejar accesibles el constructor y destructor, por ejemplo.

En el ejemplo anterior, si la declaración de la clase se mantiene igual en la versión del fichero de cabecera para usar en otros programas, el constructor sigue siendo accesible

Los métodos que se definen en el fichero de cabecera, como SetX y SetY, no necesitan ser exportados (y si se hace el compilador ignorará el modificador), ya que el código que está en el fichero de cabecera se insertará inline en cualquier programa que lo use, y no se añadirá el código a la DLL.

Pero podemos declarar tipos o estructuras opacas, si no se especifica DLL_EXPORT, y definir un tipo puntero a esos tipos, de modo que funciones de la librería puedan intercambiar esos punteros, pero para las aplicaciones que usan la DLL sea imposible acceder a los campos de esos tipos:

typedef struct {
int x;
int y;
} punto2D;

typedef punto2D* p2D;

#ifdef __cplusplus
extern "C"
{
#endif

void DLL_EXPORT ObtienePunto(pD A);
void DLL_EXPORT LiberaPunto(pD A);
void DLL_EXPORT ProcesaPunto(pD A);

#ifdef __cplusplus
}
#endif

Por supuesto, en este caso necesitaremos dos versiones del fichero de cabecera, la versión del fichero de cabecera que se distribuye con la DLL no debe incluir las declaraciones de tipo que queramos que sean opacas.

Usar fichero .def

Cuando se usa el modificador __declspec(dllexport) en el archivo DLL se almacenan los nombres de las funciones. Pero esta no es la única forma de crear una DLL en Windows.

Es posible crear una DLL en la que las funciones se almacenan por ordinales, es decir, números enteros. Estas DLL tienen algunas ventajas sobre las nominales, pero no se pueden crear usando MinGW, de modo que no las trataremos aquí, de momento.

En cualquier caso, Code::Blocks creará un fichero .def, con los nombres de las funciones exportadas y sus ordinales, y aunque no necesitaremos usarlo en la mayor parte de los casos, puede darnos alguna información útil.

Ejemplo

Veamos un ejemplo práctico sencillo.

Crearemos una clase para manejar tiempos en formato H:m:s, que nos permita sumarlos y restarlos, y crearemos una DLL con ella que podremos usar en otros programas.

Usando Code::Blocks, empezaremos creando un proyecto DLL (Dynamic Link Library). Borraremos todo el contenido, ya que la estructura de ejemplo está destinada a DLL Windows que usen el API. De momento eso no nos interesa.

El fichero de cabecera, time.h, queda así:

#ifndef __TIME_H__
#define __TIME_H__

#ifdef BUILD_DLL
    #define DLL_EXPORT __declspec(dllexport)
#else
    #define DLL_EXPORT __declspec(dllimport)
#endif

// Funciones disponibles desde programas C
#ifdef __cplusplus
extern "C"
{
#endif

int DLL_EXPORT VersionMayor();                      /**< Valor de la versión mayor a.b < a */
int DLL_EXPORT VersionMenor();                      /**< Valor de la versión menor a.b < b */

#ifdef __cplusplus
}
#endif

// Clases:

class DLL_EXPORT Tiempo {
private:
    bool positivo;                                  /**< Signo, necesario para tiempos menores de 0 */
    int hora;                                       /**< Hora */
    unsigned int minuto, segundo;                   /**< Minutos y segundos */
    void Ajustar();                                 /**< Ajusta los valores de minutos y segundos entre 0 y 59 */
public:
    Tiempo(int h, unsigned int m, unsigned int s);  /**< Constructor */
    Tiempo(int s);                                  /**< Constructor a partir de un tiempo en segundos */
    Tiempo operator+(Tiempo &t);                    /**< Suma de tiempos */
    Tiempo operator-(Tiempo &t);                    /**< Resta de tiempos */
    operator int();                                 /**< Conversión a segundos */
    int Hora() { return hora; }                     /**< Interfaz; devuelve el valor de hora */
    unsigned int Minuto() { return minuto; }        /**< Interfaz; devuelve el valor de minutos */
    unsigned int Segundo() { return segundo; }      /**< Interfaz; devuelve el valor de segundos */
};
#endif // __TIME_H__

El fichero de definición, time.cpp:

#include "time.h"

int DLL_EXPORT VersionMayor()
{
    return 0;
}

int DLL_EXPORT VersionMenor()
{
    return 1;
}

Tiempo::Tiempo(int h, unsigned int m, unsigned int s) : positivo(h>=0), hora(h), minuto(m), segundo(s) {
    if(hora<0) hora = -hora;

    Ajustar();
}

Tiempo::Tiempo(int s) {
    if(s>=0) {
        positivo=true; // Positivo
        minuto=s/60;
        segundo=s%60;
        hora=minuto/60;
        minuto=minuto%60;
    } else { // Tiempos negativos
        positivo=false; // Negativo
        minuto=-s/60;
        segundo=-s%60;
        hora=minuto/60;
        minuto=minuto%60;
    }
}

Tiempo Tiempo::operator+(Tiempo &t) {
    int tf, ta, tb;
    ta = int(*this);
    tb = int(t);
    tf = ta+tb;
    return tf;
}

Tiempo Tiempo::operator-(Tiempo &t) {
    int tf, ta, tb;
    ta = int(*this);
    tb = int(t);
    tf = ta-tb;
    return tf;
}

Tiempo::operator int() {
    int ti = hora*3600+minuto*60+segundo;
    if(!positivo) ti = -ti;
    return ti;
}

void Tiempo::Ajustar() {
    // Convertir a segundos
    int s = int(*this);

    // Convertir a h:m:s
    *this = Tiempo(s);
}

Si compilamos el proyecto se crearán tres ficheros en la carpeta bin, Debug o Release, dependiendo de la versión que hayamos elegido:

  • ejemplodll.dll: La DLL que se ha generado.
  • libejemplodll.a: La librería de enlace estático necesaria para usar la DLL desde otros programas.
  • libejemplodll.def: El fichero de definición con las funciones exportadas a la DLL.

Para usar esta DLL en otros programas deberemos asegurarnos de tener acceso a los ficheros:

  • ejemplodll.dll: La DLL que se ha generado.
  • libejemplodll.a: La librería de enlace estático necesaria para usar la DLL desde otros programas.
  • time.h: El fichero de cabecera de la DLL.

En un proyecto real podemos definir variables globales en Code::Blocks con las rutas al fichero include y a la librería estática.

Nuestro proyecto debe tener acceso a la librería, de modo que tendremos que añadir la carpeta donde esté el fichero dll al path del sistema, copiarlo a una carpeta que ya esté en la ruta, o a la carpeta donde esté nuestro ejecutable.

El programa de ejemplo para usar esta DLL puede tener esta forma:

#include 
#include "time.h"

void Mostrar(Tiempo t);

int main()
{
    Tiempo t(-5345);
    Tiempo t2(-5345);
    Tiempo t3 = t2-t;
    Tiempo t4 = t2+t;
    Tiempo t5(-4, 23, 11);

    Mostrar(t);
    Mostrar(t2);
    Mostrar(t3);
    Mostrar(t4);
    Mostrar(t5);
    Mostrar(t5+t4);

    std::cin.get();

    return 0;
}

void Mostrar(Tiempo t) {
    std::cout << t.Hora() << ":" << t.Minuto() << ":" << t.Segundo() << std::endl;
}
Opciones de enlazado
Opciones de enlazado

Rutas de enlazado
Rutas de enlazado

Deberemos añadir en las opciones de proyecto, en la opciones de enlazado la librería estática "ejemplodll" y en la ruta del enlazador la carpeta donde esté ese fichero, en este ejemplo, la carpeta local.

Para el ejemplo que se incluye se han copiado los tres ficheros: time.h, ejemplodll.dll y libejemplodll.a a la carpeta raíz del ejemplo.

Nombre Fichero Fecha Tamaño Contador Descarga
DLL de ejemplo ejemplodll.zip 2024-10-13 1715 bytes 6
Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo de uso de DLL tiempo_ver.zip 2024-10-13 8149 bytes 6

Bibliografía

Creación de archivos DLL de C/C++ en Visual Studio.