SDK Multiplataforma en C logo

SDK Multiplataforma en C

Librerías

❮ Anterior
Siguiente ❯

En los dos últimos capítulos hemos visto los fundamentos para crear aplicaciones multiplataforma utilizando funciones del SDK. Algo habitual en Ingeniería del Software es la posibilidad de reutilizar porciones de código entre diferentes proyectos. El propio Referencia del SDK está compuesto por un conjunto de bloques independientes que forman una jerarquía de dependencias. En este capítulo veremos como aislar funcionalidad común dentro de una librería, asi como vincular aplicaciones con ella.


1. Librerías estáticas

Siguiendo la línea abierta en los dos capítulos anteriores con la aplicación Die, vamos a crear una nueva aplicación 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. Abrimos el src/CMakeLists.txt y añadimos las siguientes líneas:

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

Donde hemos utilizado el comando staticLib(), que es el análogo de 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\src\casino y "games/casino" en C:\nappgui\src\games\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 Referencia del SDK tienes el grafo completo de dependencias de NAppGUI.
  • 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 de nuevo [Generate] en CMake, 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, 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 3: 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 4: 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 5: 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 */
}

Ahora vamos a crear dos nuevos archivos dentro de src/casino, ddraw.c y ddraw.h donde portaremos la función de dibujo. Ya vimos como Añadir archivos en capítulos anteriores.

Listado 6: 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 7: 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 Referencia 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 8).

Listado 8: 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 ❯