Crear nueva librería
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.
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 casonappgui_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, indicamosNRC_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.
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).
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.
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.
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.
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.
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.
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; |
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.
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.
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.
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.
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: - Se ha realizado un
#include "ddraw.h"
, cabecera pública decasino
. - 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 deDice.exe
. - El uso de versión estática o dinámica de
casino
no implica cambios en el código deDice
. Tan solo tendríamos que cambiar las dependencias dentro dedesktopApp()
y recompilar la aplicación.
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]); } |
|
// 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 encasino_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 punterofunc_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 deDice2
. - 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).
|
dynamicLib("casino_d" "demo/casino" "draw2d" NRC_EMBEDDED) desktopApp("Dice2" "demo/dice2" "osapp" NRC_EMBEDDED) |
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) oDYLD_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 elCMakeLists.txt
de NAppGUI establecen automáticamente elRPATH
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
ounion
. 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
yenum
. - *.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á comostatic
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
.
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
.
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 ... |
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 |
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 |