Create new library
This page has been automatically translated using the Google Translate API services. We are working on improving texts. Thank you for your understanding and patience.
The only thing that you absolutely have to know, is the location of the library. Albert Einstein
The use of libraries will allow us to share common code between several projects. An example is the NAppGUI SDK, which has been organized into several static or dynamic link libraries that can be reused by different applications.
1. Static libraries
We are going to rescue two applications included in the NAppGUI examples: Die
(Figure 1) and Dice
(Figure 2). In both you must be able to draw the silhouette of a dice.
It is not very complicated to intuit that we could reuse the parametric drawing routine in both projects. One way to do this would be to copy said routine from Die to Dice, but this is not the most advisable since we would have two versions of the same code to maintain. Another option, the smartest, is to move the drawing function to a library and link it in both applications.
Download the complete example from this link. The structure of the project is very similar to what was seen in the previous chapter, starting with the main CMakeLists.txt
:
1 2 3 4 5 6 7 8 9 |
cmake_minimum_required(VERSION 3.0) project(Dice) find_package(nappgui REQUIRED) include("${NAPPGUI_ROOT_PATH}/prj/NAppProject.cmake") include("${NAPPGUI_ROOT_PATH}/prj/NAppCompilers.cmake") nap_config_compiler() nap_project_library(casino casino) nap_project_desktop_app(Die die) nap_project_desktop_app(Dice dice) |
- Line 1: Set the minimum version of CMake.
- Line 2: Project name.
- Line 3: Locate the NAppGUI-SDK installation.
- Line 4: Includes the
NAppProject.cmake
module. - Line 5: Includes the
NAppCompilers.cmake
module. - Line 6: Configure the compiler.
- Line 7: Create a target library in the
casino
directory. - Line 8: Create a target application in the
die
directory. - Line 9: Create a target application in the directory
dice
.
Notice that the nap_project_library()
command precedes applications. This is because CMake needs to process dependencies before the projects that use them.
|
nap_project_library(libName path) |
libName
: Name of the library.path
: Relative path where the project is located.
As with application projects, the first time you run nap_project_library()
, a series of default files are created. Later they can be edited, deleted or added more as we have just seen in the case of applications.
In die/CMakeLists.txt
and dice/CMakeLists.txt
we see the link with casino
:
|
nap_desktop_app(Die "casino" NRC_EMBEDDED) |
|
nap_desktop_app(Dice "casino" NRC_NONE) |
For now, don't worry about the constants NRC_EMBEDDED
and NRC_NONE
. In Resource processing we will see them in detail. You can build and compile the project in the usual way:
|
// Windows cmake -S . -B build -DCMAKE_INSTALL_PREFIX=C:/nappgui cmake --build build --config Debug // macOS cmake -G Xcode -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local/nappgui cmake --build build --config Debug // Linux cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr/local/nappgui cmake --build build |
In build/Debug/bin
you will have the executables. Both Die and Dice have added a dependency on casino (Figure 3) via the dependList
parameter from the nap_desktop_app()
command. This way CMake knows that it must link, in addition to NAppGUI-SDK (NAPPGUI_LIBRARIES
), the casino library, which is where common code from both projects is found (Figure 4).
What does it really mean that Die and Dice have a dependency on casino? From now on, none of them will be able to compile if there is an error in the casino code, since it is a fundamental module for both. Within the build project (Visual Studio, Xcode, Makefile, Ninja, etc.) several things have happened:
- Both applications know where casino is located, so they can do
#include "casino.h"
without worrying about its location. - The binary code for the casino functions will be included in each executable in the linking process. CMake already took care of linking the library with the executables.
- Any changes made to casino will force the applications to be recompiled due to the previous point. Again, the build project will know how to do this as efficiently as possible. We will only have to re-launch
cmake --build build
to update all the binaries.
2. Dynamic libraries
Dynamic libraries are, in essence, the same as static ones. The only thing that changes is the way they link to the executable (Figure 5). In static linking, the library code is added to the executable itself, so its size will grow. In dynamic linking, the library code is distributed in its own file (.dll
, .so
, .dylib
) and is loaded directly before the executable program.
To create the dynamic version of casino
, open casino/CMakeLists.txt
and change the buildShared
parameter of nap_library()
from NO
to YES
.
|
nap_library(casino "" YES NRC_NONE) target_include_directories(casino PUBLIC "${NAPPGUI_INCLUDE_PATH}") |
After re-generating and re-compiling the solution, you will notice that a new casino.dll
appears in build/Debug/bin
. This dll
will be shared by Die.exe
and Dice.exe
, something that did not happen when compiling the static version.
|
12/18/23 04:38 PM <DIR> . 12/18/23 03:59 PM <DIR> .. 12/18/23 04:38 PM 53,248 casino.dll 12/18/23 04:38 PM 92,672 Dice.exe 12/18/23 04:38 PM 102,400 Die.exe |
2.1. Advantages of DLLs
As we have been able to intuit in the previous example, using DLLs we will reduce the size of the executables, grouping the common binary code (Figure 6), (Figure 7). This is precisely what operating systems do. For example, Die.exe
will ultimately need to access Windows API functions. If all applications had to statically link Windows binaries, their size would grow disproportionately and a lot of space would be wasted within the file system.
Another great advantage of DLLs is the saving of memory at runtime. For example, if we load Die.exe
, casino.dll
will be loaded at the same time. But if we then load Dice.exe
, both will share the copy of casino.dll
existing in memory. However, with static linking, there would be two copies of casino.lib
in RAM: One embedded in Die.exe
and another in Dice.exe
.
2.2. Disadvantages of DLLs
The main drawback of using DLLs is the incompatibility that may arise between the different versions of a library. Suppose we launch a first version of the three products:
|
casino.dll 102,127 (v1) Die.exe 84,100 (v1) Dice.exe 73,430 (v1) |
A few months later, we released a new version of the application Dice.exe
that involves changes to casino.dll
. In that case, the distribution of our suite would look like this:
|
casino.dll 106,386 (v2)* Die.exe 84,100 (v1)? Dice.exe 78,491 (v2)* |
If we have not been very careful, it is very likely that Die.exe
will no longer work as it is not compatible with the new version of the DLL. This problem bothers many developers and has been named DLL Hell. Since in this example we are working in a "controlled" environment we could solve it without too many problems, creating a new version of all the applications running under casino.dll(v2)
.
|
casino.dll 106,386 (v2) Die.exe 84,258 (v2) Dice.exe 78,491 (v2) |
This will not always be possible. Now suppose that our company develops only casino.dll
and third parties work on the final products. Now each product will have its production and distribution cycles (uncontrolled environment) so, to avoid problems, each company will include a copy of the specific version of the DLL with which its product works. This could lead to the following scenario:
|
/Apps/Die casino.dll 114,295 (v5) Die.exe 86.100 (v8) /Apps/Dice casino.dll 106,386 (v2) Dice.exe 72,105 (v1) |
Seeing this we sense that the benefits of using DLLs are no longer so great, especially in relation to the optimization of space and loading times. The fact is that it can get even worse. Typically, libraries are written to be as generic as possible and can serve many applications. In many cases, a specific application uses only a few functions in each library with which it links. Using static libraries, the size of the executable (Figure 8) can be considerably reduced, since the linker knows perfectly well which specific functions the application uses and adds the strictly necessary code. However, using DLLs, we must distribute the complete library no matter how few functions the executable uses (Figure 9). In this case, you are wasting space and unnecessarily increasing application loading times.
2.3. Check links with DLLs
When an executable is launched, for example Die.exe
, all the dynamic libraries linked to it are loaded into memory (if they do not previously exist). If there is a problem during loading, the executable will not be able to start and the operating system will display some type of error.
Links in Windows
Windows will display an error message (Figure 10) when it cannot load a DLL associated with an executable.
If we want to see which DLLs are linked to an executable, we will use the dumpbin
command.
|
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 |
We see, at the beginning, the dependency with casino.dll
. The rest are Windows libraries related to the kernel and the user interface. In the case that we make a casino
static link:
|
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 |
casino.dll
no longer appears, having been statically linked within Die.exe
.
Links in Linux
In Linux something similar happens, we will get an error if it is not possible to load a dynamic library (*.so
).
|
:~/$ ./Die ./Die: error while loading shared libraries: libcasino.so: cannot open shared object file: No such file or directory |
To check which libraries are linked to an executable we use the ldd
command.
|
~/$ 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) ... |
Where we see that Die
depends on libcasino.so
. The rest are dependencies on the Linux kernel, the C standard library and GTK.
Links in macOS: We use the otool
command.
|
% 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. Loading DLLs at runtime
Until now, the import of DLL symbols is resolved at compile time or, rather, at link time. This means that:
- Executables can directly access global variables and functions defined in the DLL. Going back to the code of
Dice.exe
, we have: - A
#include "ddraw.h"
has been made, public header ofcasino
. - The symbols
die_draw()
,kDEF_PADDING
,kDEF_CORNER
,kDEF_RADIUS
, defined inddraw.h
, have been used. - The dynamic library
casino.dll
will load automatically just beforeDice.exe
. - Using a static or dynamic version of
casino
does not imply changes to theDice
code. We would only have to change thecasino/CMakeLists.txt
and recompile the solution.
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]); } |
|
# Static library nap_library(casino "" NO NRC_NONE) target_include_directories(casino PUBLIC "${NAPPGUI_INCLUDE_PATH}") # Dynamic library nap_library(casino "" YES NRC_NONE) target_include_directories(casino PUBLIC "${NAPPGUI_INCLUDE_PATH}") |
However, there is the possibility that the programmer is in charge of loading, downloading and accessing the symbols of the DLLs at any time. This is known as run-time linking or non-symbol import linking. In (Listing 1) we have a new version of 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"); 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); } |
- Line 6 loads the
casino
library. - Line 7 accesses the function
die_draw
defined incasino
. - Lines 11-13 access public
casino
variables. - Lines 15-20 use
die_draw
through thefunc_draw
pointer. - Line 21 unloads the
casino
library from memory.
As we see, this loading at runtime does imply changes to the source code, but it also brings with it certain advantages:
- The library is loaded when we need it, not at the beginning of the program. This is why it is very important that
casino
does not appear as a dependency ofDice
. - We can have different versions of
casino
and choose which one to use at runtime. This is the working mechanism of the plug-ins used by many applications. For example, the Rhinoceros 3D program enriches its functionality thanks to new commands implemented by third parties and added at any time through a system of plugins (.DLLs) (Figure 11).
|
nap_desktop_app(Dice "" NRC_NONE) |
2.5. Location of DLLs
When the operating system must load a dynamic library, it follows a certain search order. On Windows systems search in this order:
- The same directory as the executable.
- The current working directory.
- The directory
%SystemRoot%\System32
. - The directory
%SystemRoot%
. - The directories specified in the
PATH
environment variable.
On the other hand, on Linux and macOS:
- The directories specified in the environment variable
LD_LIBRARY_PATH
(Linux) orDYLD_LIBRARY_PATH
(macOS). - The directories specified in the executable
rpath
. - The system directories
/lib, /usr/lib, etc
.
Here we have a big difference between Windows and Unix, since in the latter it is possible to add dependencies search directories within the executable. This variable is known as RPATH
and is not available on Windows. To check the value of 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) ... |
Executables generated by thenap_desktop_app()
command automatically set theRPATH
to find dynamic dependencies in the same directory as executables on Linux or bundles on macOS.
3. Symbols and visibility
In the linking process after the compilation of the library, those elements that can generate machine code or take up space in the final binary are called symbol. These are methods, functions and global variables. Symbols are not considered:
- Type definitions such as
enum
,struct
orunion
. These help the programmer organize the code and the compiler validate it, but they do not generate any binary code. They do not exist from the linker's point of view. - Local variables. These are automatically created and destroyed in the Stack Segment during program execution. They do not exist at link time.
On the other hand, all functions and global variables declared as static
within a module *.c
will be considered private symbols not visible in link time and where the compiler is free to make the appropriate optimizations. With this in mind, the code within NAppGUI is organized as follows:
- *.c: Implementation file. Definition of symbols (functions and global variables).
- *.h: Public header file. Declaration of functions and global variables (
extern
), available to the library user. - *.hxx: Declaration of public types:
struct
,union
andenum
. - *.inl: Declaration of functions and private variables. Only the internal modules of the library will have access to these symbols.
- *.ixx: Declaration of private types. Those shared between the library modules, but not with the outside.
If a function is only needed within a*.c
module, it is not included in a*.inl
. It will be marked asstatic
within the same*.c
. This way it will not be visible to the linker and will allow the compiler to perform optimizations.
Likewise, types that are only used within a specific module will be declared at the beginning of*.c
and not in*.ixx
.
For the sake of code maintainability and scalability, type and function declarations will be kept as private as possible.
3.1. Export in DLLs
When we generate a dynamic link library, in addition to including public symbols in one or more *.h
headers, we must explicitly mark them as exportable. The export macro is declared in the *.def
file of each library. For example in casino.def
, the macro _casino_api
is defined.
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 28 29 30 31 |
/* casino library import/export */ /* clang-format off */ #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 /* clang-format on */ |
This macro must precede all public functions and variables declared in the *.h
of the library. Projects based nap_desktop_app()
will define the macros NAPPGUI_XXXXX_EXPORT_DLL
when the DLL is compiled and NAPPGUI_XXXXX_IMPORT_DLL
when the DLL is used in other targets. This way, the export and import of symbols will be done correctly on all platforms.
3.2. Checking in DLLs
We can see, from the binary of a dynamic library, what public symbols it exports. On Windows we will use dumpbin /exports dllname
, on Linux nm -D soname
and on macOS nm -gU dylibname
.
core.dll
symbols (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
symbols (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
symbols (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 |