SDK Multiplataforma en C logo

SDK Multiplataforma en C

Crear nueva librería

❮ 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 código común entre varios proyectos. Sirva como ejemplo el SDK de NAppGUI, que se ha organizado en varias librerías de enlace estático o dinámico. Por ejemplo Core implementa funciones relacionadas con strings, streams y estructuras de datos que pueden ser reutilizadas en diferentes aplicaciones.


1. Librerías estáticas

Para ilustrar el uso de librerías, vamos a utilizar dos aplicaciones incluidas en los ejemplos de NAppGUI: Die (Figura 1) y Dice (Figura 2). En ambas se debe poder dibujar la silueta de un dado.

Captura de la aplicación Die, que permite dibujar un dado paramétricamente.
Figura 1: Aplicación Die.
Captura de la aplicación Dice, que dibuja seis dados de forma aleatoria.
Figura 2: Aplicación Dice.
El código fuente ambas aplicaciones lo tienes en src/demo/die y src/demo/dice.

No es muy complicado intuir que podríamos reutilizar la rutina de dibujo paramétrico en ambos proyectos. 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:

 
staticLib("casino" "demo/casino" "draw2d" NRC_EMBEDDED)
desktopApp("Die" "demo/die" "osapp;casino" NRC_EMBEDDED)
desktopApp("Dice" "demo/dice" "osapp;casino" NRC_EMBEDDED)

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

 
staticLib(libName path depends nrcMode)
  • libName: El nombre de la librería.
  • path: Ruta relativa a /src donde se ubicará el proyecto (en este caso nappgui_src/src/demo/casino). Al igual que vimos al crear nuevas aplicaciones, se admite cualquier profundidad de ruta.
  • depends: Dependencias de la librería. Al igual que en aplicaciones, 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 NAppGUI API tienes el grafo completo de dependencias.
  • nrcMode: Cómo se administrarán los recursos de la librería. Por el momento, indicamos NRC_EMBEDDED. Profundizaremos en ellos en el capítulo Recursos.
  • standard: Opcionalmente se puede indicar el Estándar C/C++.

Tanto Die como Dice han añadido una dependencia con casino (Figura 3) por medio del parámetro depends del comando desktopApp(). De esta forma CMake sabe que debe enlazar, además de osapp, 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 3: Árbol de dependencias de las aplicaciones, centrado en la librería casino.

Al re-generar con cmake -S ./src -B ./build, se añade la librería casino a nuestra solución, así como un vínculo a ella en ambas aplicaciones (Figura 4).

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

Al igual que ocurría al crear una nueva aplicación, cuando se crea una librería aparecen varios archivos por defecto, que son:

casino.def: Fichero que definirá la macro _casino_api necesaria para la exportación de símbolos. Más información en Símbolos y visibilidad.

Listado 1: demo/casino/casino.def
 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
/* casino library import/export */

#if defined(NAPPGUI_SHARED)
    #if defined(NAPPGUI_BUILD_CASINO_LIB)
        #define NAPPGUI_CASINO_EXPORT_DLL
    #else
        #define NAPPGUI_CASINO_IMPORT_DLL
    #endif
#endif

#if defined(__GNUC__)
    #if defined(NAPPGUI_CASINO_EXPORT_DLL)
        #define _casino_api __attribute__((visibility("default")))
    #else
        #define _casino_api
    #endif
#elif defined(_MSC_VER)
    #if defined(NAPPGUI_CASINO_IMPORT_DLL)
        #define _casino_api __declspec(dllimport)
    #elif defined(NAPPGUI_CASINO_EXPORT_DLL)
        #define _casino_api __declspec(dllexport)
    #else
        #define _casino_api
    #endif
#else
    #error Unknown compiler
#endif

casino.hxx: Aquí definiremos tipos públicos, como enum o struct. Por el momento casino no contiene tipos públicos.

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

#ifndef __CASINO_HXX__
#define __CASINO_HXX__

#include <draw2d/draw2d.hxx>
#include "casino.def"

/* 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 3: demo/casino/casino.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* casino */

#include "casino.hxx"

__EXTERN_C

_casino_api void casino_start(void);

_casino_api void casino_finish(void);

__END_C

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

Listado 4: 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 creamos dos nuevos archivos dentro de src/demo/casino, ddraw.c y ddraw.h donde implementaremos la función de dibujo a compartir. Ya vimos como Añadir archivos.

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

#include "casino.hxx"

_casino_api 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);

_casino_api extern const real32_t kDEF_PADDING;

_casino_api extern const real32_t kDEF_CORNER;

_casino_api extern const real32_t kDEF_RADIUS;
Listado 6: 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
55
56
57
/* Die drawing */

#include "ddraw.h"
#include <draw2d/draw2dall.h>

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

static const real32_t i_MAX_PADDING = 0.2f;
const real32_t kDEF_PADDING = .15f;
const real32_t kDEF_CORNER = .15f;
const real32_t kDEF_RADIUS = .35f;

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

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 módulo fundamental para ambas. Dentro del proyecto de compilación (Visual Studio, Xcode, Makefile, etc) han ocurrido varias cosas:

  • Las dos aplicaciones saben donde está ubicada casino, por lo que pueden hacer #include "casino.h" sin preocuparse de su ubicación.
  • El código binario de las funciones de casino se incluirá en cada ejecutable en el proceso de enlazado. CMake ya se encargó de vincular la librería con los ejecutables.
  • Cualquier cambio realizado en casino obligará a recompilar las aplicaciones debido al punto anterior. De nuevo, el proyecto de compilación sabrá como hacerlo de la forma más eficiente posible. Tan solo deberemos volver a lanzar cmake --build ./build para actualizar todos los binarios.

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, en esencia, lo mismo que las estáticas. Lo único que cambia es la forma con la que se vinculan al ejecutable (Figura 5). En el enlace estático, el código de la librería se añade al propio ejecutable, por lo que el tamaño de este último crecerá. En el enlace dinámico el código de la librería se distribuye en su propio archivo (.dll, .so, .dylib) y se carga justamente antes que el programa ejecutable.

Gráfico que compara el enlace estático y dinámico de librerías.
Figura 5: Enlace estático o dinámico de casino.

El proceso para crear librerías dinámicas es exactamente igual que las estáticas. Lo único que tenemos que hacer es sustituir el comando staticLib() por dynamicLib() en /src/CMakeLists.txt.

 
dynamicLib("casino" "demo/casino" "draw2d" NRC_EMBEDDED)
desktopApp("Die" "demo/die" "osapp;casino" NRC_EMBEDDED)
desktopApp("Dice" "demo/dice" "osapp;casino" NRC_EMBEDDED)
 
dynamicLib(libName path depends nrcMode)

Los parámetros son exactamente los mismos que en staticLib:

  • libName: El nombre de la librería.
  • path: Ruta relativa a /src donde se ubicará el proyecto.
  • depends: Dependencias de la librería.
  • nrcMode: Cómo se administrarán los recursos de la librería.
  • standard: Opcionalmente se puede indicar el Estándar C/C++.

Es totalmente válido crear la versión estática y dinámica de una librería. La única condición es la de renombrar una de ellas, ya que no es posible tener dos proyectos con el mismo nombre en la misma solución. A continuación, hemos creado dos versiones de casino, vinculando cada una de ellas con un ejecutable.

 
staticLib("casino" "demo/casino" "draw2d" NRC_EMBEDDED)
dynamicLib("casino_d" "demo/casino" "draw2d" NRC_EMBEDDED)

// Use the static version of 'casino'
desktopApp("Die" "demo/die" "osapp;casino" NRC_EMBEDDED)

// Use the dynamic version of 'casino'
desktopApp("Dice" "demo/dice" "osapp;casino_d" NRC_EMBEDDED)

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 (Figura 6), (Figura 7). 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.

Ejemplos de programación de NAppGUI en su versión de enlace estático.
Figura 6: Los ejemplos de programación ocupan 6.52 Mb en su versión estática.
Ejemplos de programación de NAppGUI en su versión de enlace estático.
Figura 7: Los ejemplos de programación ocupan 4.08 Mb en su versión dinámica.

Otra gran ventaja de las DLLs es el ahorro de memoria en tiempo de ejecución. Por ejemplo, si cargamos Die.exe, se cargará casino.dll al mismo tiempo. Pero si después cargamos Dice.exe, ambas compartirán la copia de casino.dll existente en memoria. Sin embargo, con enlace estático, existirían dos copias de casino.lib en la memoria RAM: Una integrada en Die.exe y otra en Dice.exe.

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. Supongamos que lanzamos una primera versión de los tres productos:

 
casino.dll        102,127 (v1)
Die.exe            84,100 (v1)
Dice.exe           73,430 (v1)

Unos meses después, 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í:

 
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).

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

Esto no siempre será posible. 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:

 
/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 8), 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 9). 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 8: 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 9: Con librerías dinámicas esta aplicación ocupa más de lo que debería y aumentan sus tiempos de carga.

2.3. Comprobar vínculos con DLLs

Cuando se lanza un ejecutable, por ejemplo Die.exe, se cargan en memoria todas las librerías dinámicas vinculadas con él (en el caso de que no existan previamente). Si hay algún problema durante dicha carga, el ejecutable no podrá arrancar y el sistema operativo mostrará algún tipo error.

Vínculos en Windows

Windows mostrará un aviso de error (Figura 10) cuando no pueda cargar una DLL asociada a un ejecutable.

Mensaje de error emitido por Windows cuando no puede cargar una DLL.
Figura 10: Error en la carga de la DLL casino.

Si queremos ver que DLLs están vinculadas con un ejecutable, utilizaremos el comando dumpbin.

 
dumpbin /dependents Die.exe

Dump of file Die.exe

File Type: EXECUTABLE IMAGE

  Image has the following dependencies:

    casino.dll
    KERNEL32.dll
    USER32.dll
    GDI32.dll
    SHELL32.dll
    COMDLG32.dll
    gdiplus.dll
    SHLWAPI.dll
    COMCTL32.dll
    UxTheme.dll
    WS2_32.dll

Vemos, al principio, la dependencia con casino.dll. Las demás son librerías de Windows relacionadas con el kernel y la interfaz de usuario. En el caso de que hagamos un enlace estático de casino:

 
staticLib("casino" "demo/casino" "draw2d" NRC_EMBEDDED)
desktopApp("Die" "demo/die" "osapp;casino" NRC_EMBEDDED)
 
dumpbin /dependents Die.exe

Dump of file Die.exe

File Type: EXECUTABLE IMAGE

  Image has the following dependencies:

    KERNEL32.dll
    USER32.dll
    GDI32.dll
    SHELL32.dll
    COMDLG32.dll
    gdiplus.dll
    SHLWAPI.dll
    COMCTL32.dll
    UxTheme.dll
    WS2_32.dll

Ya no aparece casino.dll, al haber sido enlazada de forma estática dentro de Die.exe.

Vínculos en Linux

En Linux ocurre algo similar, obtendremos un error si no es posible cargar una librería dinámica (*.so).

 
:~/$ ./Die
./Die: error while loading shared libraries: libcasino.so: cannot open shared object file: No such file or directory

Para comprobar que librerías están vinculadas con un ejecutable utilizamos el comando ldd.

 
~/$ ldd ./Die
linux-vdso.so.1 (0x00007fff58036000)
libcasino.so => libcasino.so (0x00007f6848bf4000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6848bba000)
libgtk-3.so.0 => /lib/x86_64-linux-gnu/libgtk-3.so.0 (0x00007f6848409000)
libgdk-3.so.0 => /lib/x86_64-linux-gnu/libgdk-3.so.0 (0x00007f6848304000)
libpangocairo-1.0.so.0 => /lib/x86_64-linux-gnu/libpangocairo-1.0.so.0 (0x00007f68482f2000)
libpango-1.0.so.0 => /lib/x86_64-linux-gnu/libpango-1.0.so.0 (0x00007f68482a3000)
libcairo.so.2 => /lib/x86_64-linux-gnu/libcairo.so.2 (0x00007f684817e000)
libgdk_pixbuf-2.0.so.0 => /lib/x86_64-linux-gnu/libgdk_pixbuf-2.0.so.0 (0x00007f6848156000)
libgio-2.0.so.0 => /lib/x86_64-linux-gnu/libgio-2.0.so.0 (0x00007f6847f75000)
libgobject-2.0.so.0 => /lib/x86_64-linux-gnu/libgobject-2.0.so.0 (0x00007f6847f15000)
libglib-2.0.so.0 => /lib/x86_64-linux-gnu/libglib-2.0.so.0 (0x00007f6847dec000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f6847c9d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6847aa9000)
...

Donde vemos que Die depende de libcasino.so. Las demás son dependencias del kernel de Linux, de la librería estándar de C y de GTK.

Vínculos en macOS: Utilizamos el comando otool.

 
% otool -L ./Die.app/Contents/MacOS/Die
@rpath/libcasino.dylib
/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa
/System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers
/usr/lib/libc++.1.dylib
/usr/lib/libSystem.B.dylib
/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics
/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
/usr/lib/libobjc.A.dylib

2.4. Carga de DLLs en tiempo de ejecución

Hasta ahora, la importación de los símbolos de las DLLs se resuelven en tiempo de compilación o, mejor dicho, en tiempo de enlace. Esto significa que:

  • Los ejecutables pueden acceder directamente a las variables globales y funciones definidas en la DLL. Volviendo al código de Dice.exe, tenemos:
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    #include "ddraw.h"
    ...
    static void i_OnRedraw(App *app, Event *e)
    {
        const EvDraw *params = event_params(e, EvDraw);
        color_t green = color_rgb(102, 153, 26);
        real32_t w = params->width / 3;
        real32_t h = params->height / 2;
        real32_t p = kDEF_PADDING;
        real32_t c = kDEF_CORNER;
        real32_t r = kDEF_RADIUS;
        draw_clear(params->ctx, green);
        die_draw(params->ctx, 0.f, 0.f, w, h, p, c, r, app->face[0]);
        die_draw(params->ctx, w, 0.f, w, h, p, c, r, app->face[1]);
        die_draw(params->ctx, 2 * w, 0.f, w, h, p, c, r, app->face[2]);
        die_draw(params->ctx, 0.f, h, w, h, p, c, r, app->face[3]);
        die_draw(params->ctx, w, h, w, h, p, c, r, app->face[4]);
        die_draw(params->ctx, 2 * w, h, w, h, p, c, r, app->face[5]);
    }
    
    • Se ha realizado un #include "ddraw.h", cabecera pública de casino.
    • Se han utilizado die_draw(), kDEF_PADDING, kDEF_CORNER, kDEF_RADIUS.
  • La librería dinámica casino.dll se cargará de forma automática justamente antes de Dice.exe.
  • El uso de versión estática o dinámica de casino no implica cambios en el código de Dice. Tan solo tendríamos que cambiar las dependencias dentro de desktopApp() y recompilar la aplicación.
  •  
    
    // Source code in demo/dice has no changes
    
    // Option 1 - Static link of casino
    staticLib("casino" "demo/casino" "draw2d" NRC_EMBEDDED)
    desktopApp("Dice" "demo/dice" "osapp;casino" NRC_EMBEDDED)
    
    // Option 2 - Dynamic link of casino
    dynamicLib("casino" "demo/casino" "draw2d" NRC_EMBEDDED)
    desktopApp("Dice" "demo/dice" "osapp;casino" NRC_EMBEDDED)
    

No obstante, existe la posibilidad que de sea el programador el encargado de cargar, descargar y acceder a los símbolos de las DLLs en cualquier momento. Esto se conoce como enlace en tiempo de ejecución o enlace sin importación de símbolos. En src/demo/dice2 tenemos una nueva versión de Dice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef void(*FPtr_ddraw)(DCtx*, const real32_t, const real32_t, const real32_t, const real32_t, const real32_t, const real32_t, const real32_t, const uint32_t);

static void i_OnRedraw(App *app, Event *e)
{
    const EvDraw *params = event_params(e, EvDraw);
    DLib *casino = dlib_open(NULL, "casino_d");
    FPtr_ddraw func_draw = dlib_proc(casino, "die_draw", FPtr_ddraw);
    color_t green = color_rgb(102, 153, 26);
    real32_t w = params->width / 3;
    real32_t h = params->height / 2;
    real32_t p = *dlib_var(casino, "kDEF_PADDING", real32_t);
    real32_t c = *dlib_var(casino, "kDEF_CORNER", real32_t);
    real32_t r = *dlib_var(casino, "kDEF_RADIUS", real32_t);
    draw_clear(params->ctx, green);
    func_draw(params->ctx, 0.f, 0.f, w, h, p, c, r, app->face[0]);
    func_draw(params->ctx, w, 0.f, w, h, p, c, r, app->face[1]);
    func_draw(params->ctx, 2 * w, 0.f, w, h, p, c, r, app->face[2]);
    func_draw(params->ctx, 0.f, h, w, h, p, c, r, app->face[3]);
    func_draw(params->ctx, w, h, w, h, p, c, r, app->face[4]);
    func_draw(params->ctx, 2 * w, h, w, h, p, c, r, app->face[5]);
    dlib_close(&casino);
}
  • La línea 6 carga la librería casino_d.
  • La línea 7 accede a la función die_draw definida en casino_d.
  • Las líneas 11-13 acceden a variables públicas de casino_d.
  • Las líneas 15-20 utilizan die_draw a través del puntero func_draw.
  • La línea 21 descarga la librería casino_d de memoria.

Como vemos, esta carga en tiempo de ejecución sí que implica cambios en el código fuente, pero también trae consigo ciertas ventajas de las que podemos sacar partido.

  • La librería se carga en el momento que la necesitamos, no al inicio del programa. Por esto es muy importante que casino_d no aparezca como dependencia de Dice2.
  •  
    
    dynamicLib("casino_d" "demo/casino" "draw2d" NRC_EMBEDDED)
    desktopApp("Dice2" "demo/dice2" "osapp" NRC_EMBEDDED)
    
  • Podemos tener diferentes versiones de casino y elegir cual utilizar en tiempo de ejecución. Este es el mecanismo de funcionamiento de los plug-ins utilizados por muchas aplicaciones. 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 11).
  • Captura del listado de plug-ins del programa Rhinoceros 3D.
    Figura 11: Sistema de plug-ins de Rhinoceros 3D, implementado mediante DLLs.

2.5. Ubicación de DLLs

Cuando el sistema operativo debe cargar una librería dinámica, sigue cierto orden de búsqueda. En sistemas Windows busca en este orden:

  • El mismo directorio que el ejecutable.
  • El directorio de trabajo actual.
  • El directorio %SystemRoot%\System32.
  • El directorio %SystemRoot%.
  • Los directorios especificados en la variable de entorno PATH.

Por otro lado, en Linux y macOS:

  • Los directorios especificados en la variable de entorno LD_LIBRARY_PATH (Linux) o DYLD_LIBRARY_PATH (macOS).
  • Los directorios especificados en el ejecutable rpath.
  • Los directorios del sistema /lib, /usr/lib, etc.

Aquí tenemos una gran diferencia entre Windows y Unix, ya que en estos últimos es posible añadir dentro del ejecutable directorios de búsqueda de dependencias. Esta variable se conoce como RPATH y no está disponible en Windows. Para consultar el valor del RPATH:

 
// In Linux
~/$ readelf -d ./Die | grep RUNPATH
 0x000000000000001d (RUNPATH)            Library runpath: [${ORIGIN}]

 // In macOS
otool -l ./Die.app/Contents/MacOS/Die
...
Load command 25
          cmd LC_RPATH
      cmdsize 40
         path @executable_path/../../.. (offset 12)
...
Los ejecutables generados por el CMakeLists.txt de NAppGUI establecen automáticamente el RPATH para encontrar las dependencias dinámicas en el mismo directorio que los ejecutables en Linux o los bundles en macOS.

3. Símbolos y visibilidad

En el proceso de enlace tras la compilación de la librería, se denomina símbolo a aquellos elementos que pueden generar código máquina u ocupar espacio en el binario final. Estos son métodos, funciones y variables globales. No se consideran símbolos:

  • Las definiciones de tipos como enum, struct o union. Estos ayudan al programador a organizar el código y al compilador a validarlo, pero no generan código binario alguno. No existen desde el punto de vista del enlazador.
  • Las variables locales. Estas se crean y se destruyen automáticamente en el Segmento Stack durante la ejecución del programa. No existen en tiempo de enlace.

Por otro lado, todas las funciones y variables globales declaradas como static dentro de un módulo *.c serán considerados símbolos privados no visibles en tiempo de enlace y donde el compilador es libre de realizar las optimizaciones oportunas. Con esto en mente, el código dentro de NAppGUI se organiza de la siguiente forma:

  • *.c: Fichero de implementación. Definición de símbolos (funciones y variables globales).
  • *.h: Fichero de cabecera pública. Declaración de funciones y variables globales (extern), disponibles para el usuario de la librería.
  • *.hxx: Declaración de tipos públicos: struct, union y enum.
  • *.inl: Declaración de funciones y variables privadas. Solo los módulos internos de la librería tendrán acceso a estos símbolos.
  • *.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 se incluye en un *.inl. Se marcará como static dentro del mismo *.c. De esta forma no estará visible para el enlazador y se permitirá al compilador realizar optimizaciones.
De igual forma, tipos que solo se utilicen dentro de un módulo concreto, se declararán al inicio del *.c y no en el *.ixx.
En pro de la mantenibilidad y escalibidad del código, se mantendrán las declaraciones de tipos y funciones lo más privado posible.

3.1. Exportación en DLLs

Cuando generamos una librería de enlace dinámico, además de incluir los símbolos públicos en una o varias cabeceras *.h deberemos marcarlos explícitamente como exportables. La macro de exportación se declara en el archivo *.def de cada librería. Por ejemplo en core.def, de define la macro _core_api.

Listado 7: core.def
 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
/* Core library import/export */

#if defined(NAPPGUI_SHARED)
    #if defined(NAPPGUI_BUILD_CORE_LIB)
        #define NAPPGUI_CORE_EXPORT_DLL
    #else
        #define NAPPGUI_CORE_IMPORT_DLL
    #endif
#endif

#if defined(__GNUC__)
    #if defined(NAPPGUI_CORE_EXPORT_DLL)
        #define _core_api __attribute__((visibility("default")))
    #else
        #define _core_api
    #endif
#elif defined(_MSC_VER)
    #if defined(NAPPGUI_CORE_IMPORT_DLL)
        #define _core_api __declspec(dllimport)
    #elif defined(NAPPGUI_CORE_EXPORT_DLL)
        #define _core_api __declspec(dllexport)
    #else
        #define _core_api
    #endif
#else
    #error Unknown compiler
#endif

Esta macro deberá anteponerse a todas las funciones y variables declaradas en los *.h. Los proyectos basados en el /src/CMakeLists.txt definirán automáticamente las macros CORE_IMPORT y NAPPGUI_SHARED_LIB siempre que se vayan generar librerías dinámicas (exportar) o cuando se vayan a utilizar por un ejecutable (importar). En el caso de programas de terceros (no generados por /src/CMakeLists.txt) se deberán definir las macros de importación (CORE_IMPORT, GUI_IMPORT, etc) antes de incluir las cabeceras.

stream.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* Data streams */

#include "core.hxx"

__EXTERN_C

_core_api Stream *stm_from_block(const byte_t *data, const uint32_t size);

_core_api Stream *stm_memory(const uint32_t size);

_core_api Stream *stm_from_file(const char_t *pathname, ferror_t *error);

...

_core_api extern Stream *kSTDIN;

_core_api extern Stream *kSTDOUT;

_core_api extern Stream *kSTDERR;

__END_C

3.2. Comprobación en DLLs

Podemos ver, a partir del binario de una librería dinámica, que símbolos públicos exporta. En Windows utilizaremos dumpbin /exports dllname, en Linux nm -D soname y en macOS nm -gU dylibname.

Símbolos públicos de core.dll (Windows).
 
C:\>dumpbin /exports core.dll
2    1 00001000 array_all
3    2 00001010 array_bsearch
4    3 00001090 array_bsearch_ptr
5    4 00001120 array_clear
6    5 000011C0 array_clear_ptr
7    6 00001260 array_copy
8    7 00001340 array_copy_ptr
9    8 00001420 array_create
10    9 00001430 array_delete
11    A 00001530 array_delete_ptr
12    B 00001640 array_destopt
13    C 00001650 array_destopt_ptr
14    D 00001660 array_destroy
15    E 000016F0 array_destroy_ptr
16    F 00001790 array_esize
17   10 000017A0 array_find_ptr
18   11 000017D0 array_get
...
Símbolos públicos de libcore.so (Linux).
 
$ nm -D ./libcore.so
0000000000011f85 T array_all
000000000001305c T array_bsearch
000000000001316d T array_bsearch_ptr
0000000000011832 T array_clear
00000000000118a1 T array_clear_ptr
0000000000011009 T array_copy
000000000001115d T array_copy_ptr
0000000000010fdd T array_create
0000000000012649 T array_delete
000000000001276b T array_delete_ptr
0000000000011668 T array_destopt
0000000000011746 T array_destopt_ptr
00000000000115c3 T array_destroy
00000000000116ad T array_destroy_ptr
0000000000011b87 T array_esize
0000000000012dd3 T array_find_ptr
0000000000011e8c T array_get
Símbolos públicos de libcore.dylib (macOS).
 
% nm -gU ./libcore.dylib
00000000000029f0 T _array_all
0000000000003c90 T _array_bsearch
0000000000003d60 T _array_bsearch_ptr
00000000000024c0 T _array_clear
00000000000025d0 T _array_clear_ptr
0000000000001c20 T _array_copy
0000000000001dd0 T _array_copy_ptr
0000000000001b50 T _array_create
00000000000030f0 T _array_delete
0000000000003350 T _array_delete_ptr
00000000000022f0 T _array_destopt
0000000000002470 T _array_destopt_ptr
0000000000002120 T _array_destroy
0000000000002340 T _array_destroy_ptr
00000000000028b0 T _array_esize
0000000000003980 T _array_find_ptr
00000000000028f0 T _array_get
❮ Anterior
Siguiente ❯