SDK Multiplataforma en C logo

SDK Multiplataforma en C

Librerías

❮ Anterior
Siguiente ❯

Lo único que debes saber absolutamente es donde está ubicada la librería. Albert Einstein


El uso de librerías nos va a permitir compartir funcionalidad común entre varios proyectos. Sirva como ejemplo, el SDK de NAppGUI se ha organizado en varias librerías de enlace estático. Cada proyecto utilizará unas u otras en función de sus requisitos (más información en Visión general). Debido a que las librerías exportan ciertas funciones para ser utilizadas por terceros, hay que poner especial cuidado en la organización del código. Nosotros utilizamos la siguiente estructura de archivos y te recomendamos que hagas lo mismo:

  • *.c: Ficheros de implementación (definición de funciones).
  • *.h: Ficheros de cabeceras públicas. Declaración de funciones que el usuario de la librería podrá utilizar.
  • *.hxx: Declaración de tipos públicos: Básicamente struct y enum.
  • *.inl: Declaración de funciones privadas. El usuario no tendrá acceso a ellas, tan solo los módulos internos de la librería.
  • *.ixx: Declaración de tipos privados. Aquellos compartidos entre los módulos de la librería, pero no con el exterior.
Si una función solo es necesaria dentro de un módulo *.c, no la incluyas en el *.inl. Declárala static dentro del mismo módulo. Permitirás que el compilador realice optimizaciones.
De igual forma, tipos que solo se utilicen como apoyo dentro del módulo, decláralos al inicio del *.c y no en el *.ixx.
En pro de la mantenibilidad y escalibidad de tu código, mantén las declaraciones de tipos y funciones lo más privado posible. Declara público lo estrictamente necesario y privado lo que no pueda incluirse localmente dentro del *.c.

1. Librerías estáticas

Para ilustrar el uso de librerías, vamos a volver a tomar como ejemplo la aplicación Die y una nueva denominada Dice, también muy sencilla, cuyo cometido es dibujar aleatoriamente 6 dados (Figura 1). Al mover el slider cambiarán los valores.

Captura de la aplicación Dice, que dibuja seis dados de forma aleatoria.
Figura 1: Aplicación Dice.
El código fuente de Dice lo tienes en la carpeta src/demo/dice de la distribución.

No es muy complicado intuir que podríamos reutilizar la rutina de dibujo paramétrico de nuestra aplicación anterior. Una forma de hacerlo sería copiando dicha rutina desde Die a Dice, pero esto no es lo más aconsejable ya que tendríamos dos versiones del mismo código que mantener. Otra opción, la más sensata, es mover la función de dibujo a una librería y vincularla en ambas aplicaciones. Esto es muy sencillo de realizar gracias, de nuevo, a CMake. Si abrimos el src/CMakeLists.txt veremos estas tres líneas:

1
2
3
staticLib("demo/casino" "draw2d" NRC_EMBEDDED)
desktopApp("Die" "demo/die" "demo/casino" NRC_EMBEDDED)
desktopApp("Dice" "demo/dice" "demo/casino" NRC_EMBEDDED)

Donde hemos utilizado el comando staticLib(), que es análogo a desktopApp().

1
staticLib(libPath depends nrcMode)
  • libPath: Directorio de la librería dentro de la solución: "casino" creará el proyecto en la carpeta C:\nappgui_sdk\src\casino y "demo/casino" en C:\nappgui_sdk\src\demo\casino. El nombre del proyecto lo determina el último tramo del pathname: "casino" en ambos casos.
  • depends: Dependencias de la librería. Al igual que en aplicaciones de escritorio, solo es necesario indicar las de más alto nivel (draw2d en este caso). Cada librería se encarga de enlazar con las que tiene por debajo. draw2d incluirá geom2d y así sucesivamente. En Visión general tienes el grafo completo de dependencias.
  • nrcMode: Admite los tres valores: NRC_NONE, NRC_EMBEDDED y NRC_PACKED, como ya vimos en Distribución de recursos.

Tanto Die como Dice han añadido una dependencia con casino (Figura 2) por medio del parámetro depends del comando desktopApp(). De esta forma CMake sabe que debe enlazar, además de las librerías propias de NAppGUI, la librería casino que es donde se encuentra código común de ambos proyectos.

Esquema que muestra las dependencias de Die y Dice.
Figura 2: Árbol de dependencias de las aplicaciones, centrado en la librería casino.

Al pulsar [Generate] en CMake, se añade la librería casino a nuestra solución, así como un vínculo a ella en ambas aplicaciones (Figura 3). Al igual que ocurría al crear una nueva aplicación, cuando se crea una librería aparecen varios archivos por defecto, que son:

Captura de Visual Studio que muestra un nuevo proyecto con la librería casino.
Figura 3: Librería estática casino, integrada en la solución.

casino.hxx: Fichero de tipos. Aquí pondremos las definiciones de tipos públicos. En este ejemplo no será necesario.

Listado 1: demo/casino/casino.hxx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* casino */

#ifndef __CASINO_HXX__
#define __CASINO_HXX__

#include "draw2d.hxx"

/* TODO: Define data types here */

#endif

casino.h: Fichero de cabecera. Aquí escribiremos la declaración de funciones generales. Por defecto, CMake crea dos: casino_start() y casino_finish(), donde implementaríamos código global de inicio y finalización de la librería, si fuera necesario.

Listado 2: demo/casino/casino.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* casino */

#include "casino.hxx"

__EXTERN_C

void casino_start(void);

void casino_finish(void);

__END_C

casino.c: Implementación de las funciones generales.

Listado 3: demo/casino/casino.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* casino */

#include "casino.h"

/*---------------------------------------------------------------------------*/

void casino_start(void)
{
    /*TODO: Implement library initialization code here */
}

/*---------------------------------------------------------------------------*/

void casino_finish(void)
{
    /*TODO: Implement library ending code here */
}

Posteriormente se crearon dos nuevos archivos dentro de src/demo/casino, ddraw.c y ddraw.h donde portaremos la función de dibujo. Ya vimos como Añadir archivos.

Listado 4: demo/casino/ddraw.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Die drawing */

#include "casino.hxx"

void die_draw(
        DCtx *ctx, 
        const real32_t x, 
        const real32_t y, 
        const real32_t width, 
        const real32_t height, 
        const real32_t padding,
        const real32_t corner,
        const real32_t radius,
        const uint32_t face);
Listado 5: demo/casino/ddraw.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* Die drawing */

#include "ddraw.h"
#include "draw2dall.h"

/*---------------------------------------------------------------------------*/

static const real32_t i_MAX_PADDING = 0.2f;

/*---------------------------------------------------------------------------*/

void die_draw(DCtx *ctx, const real32_t x, const real32_t y, const real32_t width, const real32_t height, const real32_t padding, const real32_t corner, const real32_t radius, const uint32_t face)
{
    color_t white = color_rgb(255, 255, 255);
    color_t black = color_rgb(0, 0, 0);
    real32_t dsize, dx, dy;
    real32_t rc, rr;
    real32_t p1, p2, p3;

    dsize = width < height ? width : height;    
    dsize -= bmath_floorf(2.f * dsize * padding * i_MAX_PADDING);
    dx = x + .5f * (width - dsize);
    dy = y + .5f * (height - dsize);
    rc = dsize * (.1f + .3f * corner);
    rr = dsize * (.05f + .1f * radius);
    p1 = 0.5f * dsize;
    p2 = 0.2f * dsize;
    p3 = 0.8f * dsize;

    draw_fill_color(ctx, white);
    draw_rndrect(ctx, ekFILL, dx, dy, dsize, dsize, rc);
    draw_fill_color(ctx, black);

    if (face == 1 || face == 3 || face == 5)
        draw_circle(ctx, ekFILL, dx + p1, dy + p1, rr);

    if (face != 1)
    {
        draw_circle(ctx, ekFILL, dx + p3, dy + p2, rr);
        draw_circle(ctx, ekFILL, dx + p2, dy + p3, rr);
    }

    if (face == 4 || face == 5 || face == 6)
    {
        draw_circle(ctx, ekFILL, dx + p2, dy + p2, rr);
        draw_circle(ctx, ekFILL, dx + p3, dy + p3, rr);
    }

    if (face == 6)
    {
        draw_circle(ctx, ekFILL, dx + p2, dy + p1, rr);
        draw_circle(ctx, ekFILL, dx + p3, dy + p1, rr);
    }
}

¿Que significa realmente que Die y Dice tengan una dependencia con casino? Que a partir de ahora ninguna de ellas se podrá compilar si hay algún error en el código de casino, ya que es un bloque fundamental para ambas. Dentro de la solución de Visual Studio han ocurrido varias cosas:

  • Las dos aplicaciones saben donde está ubicada casino, por lo que pueden hacer #include "casino.h" sin preocuparse por añadir la ruta al Additional Include Directives del proyecto.
  • El código binario de las funciones de casino se incluirá en cada ejecutable en el proceso de enlazado. Tampoco debemos configurar las *.lib en el Additional Dependencies del Linker de Visual Studio. CMake ya se encargó de hacerlo.
  • Cualquier cambio realizado en casino obligará a recompilar las aplicaciones debido al punto anterior. De nuevo, Visual Studio sabrá hacerlo de la forma más eficiente posible. No hace falta ir una a una, con seleccionar Build->Build Solution se compilará y actualizará todo lo necesario.

Como ya indicamos antes, casino también tiene una dependencia con Draw2D, la librería de dibujo vectorial de NAppGUI. A su vez draw2d depende de geom2d y así sucesivamente, hasta llegar a sewer, el paquete más bajo del SDK. Cuando desarrolles una nueva librería deberías vincularla con el menor número posible de dependencias o, dicho de otra forma, con las librerías de menor nivel dentro de la jerarquía que incluyan la funcionalidad necesaria. Esto mejorará la compilación y distribución, además de constituir una muy buena práctica de trabajo.


2. Librerías dinámicas

Las librerías dinámicas son, al código ejecutable, lo que NRC_PACKED a los recursos. Es decir, el código de la librería se enlaza y carga en tiempo de ejecución y no en tiempo de compilación. Utilizando librerías estáticas, como acabamos de hacer en el apartado anterior, tanto en Die.exe como en Dice.exe existirá una copia del código binario de la función die_draw() que fue añadida durante el proceso de enlazado. Si creásemos casino como librería dinámica, se generaría el archivo casino.dll (Dynamic-Link Library) que deberíamos distribuir junto con Die.exe y Dice.exe. El resultado serían ejecutables más pequeños ya que el código común de la DLL sería enlazado en tiempo de ejecución por ambas aplicaciones (Listado 6).

Listado 6: Hipotética distribución de las aplicaciones, junto con la .dll.
1
2
3
casino.dll        102,127
Die.exe            84,100
Dice.exe           73,430
Por el momento, la versión actual de NAppGUI no soporta la creación DLLs. Este tipo de proyecto será incluido en futuras revisiones del SDK.

2.1. Ventajas de las DLLs

Como hemos podido intuir en el ejemplo anterior, utilizando DLLs reduciremos el tamaño de los ejecutables, agrupando el código binario común. Esto es precisamente lo que hacen los sistemas operativos. Por ejemplo, Die.exe necesitará acceder, en última instancia, a las funciones del API de Windows. Si todas las aplicaciones tuviesen que enlazar de forma estática los binarios de Windows, su tamaño crecería desmesuradamente y se desperdiciaría mucho espacio dentro del sistema de archivos. El sistema implementa sus funciones mediante DLLs (ubicadas en C:\Windows\System32) y las aplicaciones acceden a ellas en tiempo de ejecución.

Otra gran ventaja de las DLLs es la posibilidad de añadir código ejecutable después de haber compilado la aplicación. Este es el mecanismo de funcionamiento de los plug-ins que muchas aplicaciones comerciales utilizan. Por ejemplo, el programa Rhinoceros 3D enriquece su funcionalidad gracias a nuevos comandos implementados por terceros y añadidos en cualquier momento mediante un sistema de plugins (.DLLs) (Figura 4). El problema de las librerías estáticas es que se enlazan tras la fase de compilación, por lo que es necesario disponer del código fuente original, recompilarlo y hacerlo llegar de nuevo al usuario final. Esto imposibilita la creación de ampliaciones una vez que el programa ha sido distribuido. Gracias a las DLLs esto puede llevarse a cabo.

Captura del listado de plug-ins del programa Rhinoceros 3D.
Figura 4: Sistema de plug-ins de Rhinoceros 3D, implementado mediante DLLs.

2.2. Desventajas de las DLLs

El principal inconveniente del uso de DLLs es la incompatibilidad que puede presentarse entre las diferentes versiones de una librería. Volviendo a nuestros dos juegos, supongamos que lanzamos una nueva versión de la aplicación Dice.exe que implica cambios en casino.dll. En ese caso, la distribución de nuestra suite quedaría así:

1
2
3
casino.dll        106,386  (v2)*
Die.exe            84,100  (v1)?
Dice.exe           78,491  (v2)*

Si no hemos sido muy cuidadosos, es muy probable que Die.exe ya no funcione al no ser compatible con la nueva versión de la DLL. Este problema trae de cabeza a muchos desarrolladores y ha sido bautizado como DLL Hell. Dado que en este ejemplo trabajamos sobre un entorno "controlado" podríamos solucionarlo sin demasiados problemas, creando una nueva versión de todas la aplicaciones funcionando bajo casino.dll.v2, pero esto no siempre es posible.

1
2
3
casino.dll        106,386  (v2)
Die.exe            84,258  (v2)
Dice.exe           78,491  (v2)

Supongamos ahora que nuestra compañía desarrolla tan solo casino.dll y son terceras empresas las que trabajan en los productos finales. Ahora cada producto tendrá sus ciclos de producción y distribución (entorno no controlado) por lo que, para evitar problemas, cada compañía incluirá una copia de la versión concreta de la DLL con la que funciona su producto. Esto podría dar lugar al siguiente escenario:

1
2
3
4
5
6
7
/Apps/Die
casino.dll        114,295  (v5)
Die.exe            86.100  (v8)

/Apps/Dice
casino.dll        106,386  (v2)
Dice.exe           72,105  (v1)

Viendo esto intuimos de que las bondades del uso de DLLs ya no lo son tanto, sobre todo en lo relativo a la optimización del espacio y tiempos de carga. El caso es que se puede agravar aún más. Normalmente, las librerías se escriben para que sean lo más genéricas posible y puedan dar servicio a muchas aplicaciones. En muchas ocasiones, una aplicación concreta utiliza sólo unas pocas funciones cada librería con las que enlaza. Utilizando librerías estáticas, se puede reducir considerablemente el tamaño del ejecutable (Figura 5), ya que el enlazador sabe perfectamente que funciones concretas utiliza la aplicación y añade el código estrictamente necesario. Sin embargo, utilizando DLLs, debemos distribuir la librería completa por muy pocas funciones que utilice el ejecutable (Figura 6). En este caso, se está desperdiciando espacio y aumentando innecesariamente los tiempos de carga de la aplicación.

Esquema que muestra en enlace estático de código.
Figura 5: Con librerías estáticas se optimiza el espacio y tiempos de carga de esta aplicación.
Esquema que muestra en enlace dinámico de código.
Figura 6: Con librerías dinámicas esta aplicación ocupa más de lo que debería y aumentan sus tiempos de carga.
❮ Anterior
Siguiente ❯