Cross-platform C SDK logo

Cross-platform C SDK

Create new library

❮ Back
Next ❯
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. Take the NAppGUI SDK for example, which has been organized into various static or dynamic link libraries. For example Core implements functions related to strings, streams and data structures that can be reused in different applications.


1. Static libraries

To illustrate the use of libraries, we will use 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.

Screenshot of the Die app, which allows you to draw a die parametrically.
Figure 1: Aplicación Die.
Screenshot of the Dice app, drawing six dice at random.
Figure 2: Application Dice.
The source code for both applications is available at src/demo/die and src/demo/dice.

It is not very difficult to intuit that we could reuse the parametric drawing routine in both projects. One way to do this would be to copy the routine from Die to Dice, but this is not recommended as we would have two versions of the same code to maintain. Another option, the most sensible, is to move the drawing function to a library and link it in both applications. This is very easy to do thanks, again, to CMake. If we open the src/CMakeLists.txt we will see these three lines:

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

Where we have used the staticLib() command, which is analogous to desktopApp().

 
staticLib(libName path depends nrcMode)
  • libName: The name of the library.
  • path: Path relative to /src where the project will be located (in this case nappgui_src/src/demo/casino). Just like we saw when creating new apps, any path depth is supported.
  • depends: Library dependencies. As in applications, it is only necessary to indicate the highest level ones (draw2d in this case). Each library is responsible for linking with the ones below it. draw2d will include geom2d and so on. In NAppGUI API you have the complete dependency graph.
  • nrcMode: How the library's resources will be managed. For now, we specify NRC_EMBEDDED. We'll go deeper into them in the Resources chapter.
  • standard: Optionally, you can indicate the C/C++ Standard.

Both Die and Dice have added a dependency on casino (Figure 3) via the depends parameter of the desktopApp() command. In this way, CMake knows that it must link, in addition to osapp, the casino library, which is where the common code of both projects is found.

Schematic showing the dependencies of Die and Dice.
Figure 3: Application dependency tree, centered on the casino library.

Rebuilding with cmake -S ./src -B ./build adds the casino library to our solution, as well as a link to it in both (Figure 4) applications.

Visual Studio screenshot showing a new project with the casino library.
Figure 4: Static casino library, integrated into the solution.

As it happened when creating a new application, when a library is created, several files appear by default, which are:

casino.def: File that will define the _casino_api macro needed to export symbols. More information in Symbols and visibility.

Listing 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: Here we will define public types, such as enum or struct. At the moment casino does not contain public types.

Listing 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: Header file. Here we will write the declaration of general functions. By default, CMake creates two: casino_start() and casino_finish(), where we would implement global library start and end code, if necessary.

Listing 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: Implementation of general functions.

Listing 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 */
}

Later we create two new files inside src/demo/casino, ddraw.c and ddraw.h where we will implement the drawing function to to share. We already saw how to Adding files.

Listing 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;
Listing 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);
    }
}

What does it really mean that Die and Dice have a dependency on casino? That from now on none of them can be compiled 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, 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 of the casino functions will be included in each executable in the linking process. CMake has already taken 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 it in the most efficient way possible. We just have to run cmake --build ./build again to update all the binaries.

As we noted before, casino also has a dependency on Draw2D, NAppGUI's vector drawing library. In turn draw2d depends on geom2d and so on, up to sewer, the lowest package of the SDK. When you develop a new library you should link it with as few dependencies as possible, or, in other words, with the lowest level libraries within the hierarchy that include the necessary functionality. This will improve compilation and distribution, as well as being a very good working practice.


2. Dynamic libraries

Dynamic libraries are essentially the same as static libraries. The only thing that changes is the way they link to the (Figure 5) executable. In the static link, the library code is added to the executable itself, so the size of the latter will grow. In dynamic linking the library code is distributed in its own file (.dll, .so, .dylib) and is loaded just before the executable program.

Graph that compares the static and dynamic link of libraries.
Figure 5: Static or dynamic casino link.

The process to create dynamic libraries is exactly the same as the static ones. All we need to do is replace the staticLib() command with dynamicLib() in /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)

The parameters are exactly the same as in staticLib:

  • libName: The name of the library.
  • path: Path relative to /src where the project will be located.
  • depends: Library dependencies.
  • nrcMode: How the library's resources will be managed.
  • standard: Optionally, you can indicate the C/C++ Standard.

It is totally valid to create the static and dynamic version of a library. The only condition is to rename one of them, since it is not possible to have two projects with the same name in the same solution. Next, we've created two versions of casino, linking each with an executable.

 
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. 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 were to statically link Windows binaries, their size would grow inordinately and a lot of space within the file system would be wasted.

NAppGUI programming examples in its static link version.
Figure 6: The programming examples occupy 6.52 Mb in their static version.
NAppGUI programming examples in its static link version.
Figure 7: The programming examples occupy 4.08 Mb in their dynamic version.

Another great advantage of DLLs is memory savings 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 existing copy of casino.dll in memory. However, with static linking, there would be two copies of casino.lib in RAM: One built into Die.exe and one from Dice.exe.

2.2. Disadvantages of DLLs

The main drawback of using DLLs is the incompatibility that can arise between the different versions of a library. Suppose we release 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 Dice.exe application that involves changes to casino.dll. In that case, the layout 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 no longer works because it is not compatible with the new version of the DLL. This problem is causing many developers head and has been dubbed DLL Hell. Since in this example we work on a "controlled" environment we could solve it without too much trouble, 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 their 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 intuit that the benefits of using DLLs are not so good anymore, especially with regard to space optimization and load times. The fact is that it can get even worse. Typically, libraries are written to be as generic as possible and to serve many applications. On many occasions, a given application uses only a few functions from each library it links to. By using static libraries, the size of the (Figure 8) executable can be considerably reduced, since the linker knows exactly what specific functions the application uses and adds the code that is strictly necessary. However, using DLLs, we must distribute the entire library for very few functions that the (Figure 9) executable uses. In this case, you are wasting space and unnecessarily increasing application load times.

Schema showing static binding of code.
Figure 8: With static libraries the space and load times of this application are optimized.
Schema showing dynamic link code.
Figure 9: With dynamic libraries this application occupies more than it should and its load times increase.

2.3. Check links with DLLs

When an executable is launched, for example Die.exe, all dynamic libraries linked to it are loaded into memory (if they don't already exist). If there are any problems while loading, the executable will fail to start and the operating system will display some kind of error.

Links on Windows

Windows will display a (Figure 10) error message when it cannot load a DLL associated with an executable.

Error message issued by Windows when it cannot load a DLL.
Figure 10: Error loading DLL casino.

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 static link of 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

casino.dll no longer appears, having been statically linked inside 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 of the Linux kernel, the C standard library, and GTK.

Links on 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 importation 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. Returning to the code of Dice.exe, we have:
  •  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]);
    }
    
    • Made a #include "ddraw.h", public header of casino.
    • die_draw(), kDEF_PADDING, kDEF_CORNER, kDEF_RADIUS have been used.
  • The dynamic library casino.dll will be loaded automatically just before Dice.exe.
  • The use of static or dynamic version of casino does not imply changes in the code of Dice. We would just have to change the dependencies inside desktopApp() and recompile the application.
  •  
    
    // 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)
    

However, there is the possibility that the programmer is in charge of loading, unloading and accessing the symbols of the DLLs at any time. This is known as run-time binding or symbol-less binding. At src/demo/dice2 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_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);
}
  • Line 6 loads the casino_d library.
  • Line 7 accesses the die_draw function defined in casino_d.
  • Lines 11-13 access public variables of casino_d.
  • Lines 15-20 use die_draw via the func_draw pointer.
  • Line 21 unloads the casino_d library from memory.

As we can see, this loading at runtime does imply changes to the source code, but it also brings with it certain advantages that we can take advantage of.

  • The library is loaded when we need it, not at the start of the program. This is why it is very important that casino_d does not appear as a dependency of Dice2.
  •  
    
    dynamicLib("casino_d" "demo/casino" "draw2d" NRC_EMBEDDED)
    desktopApp("Dice2" "demo/dice2" "osapp" NRC_EMBEDDED)
    
  • 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 program Rhinoceros 3D enriches its functionality thanks to new commands implemented by third parties and added at any time through a system of plugins (.DLLs) (Figure 11).
  • Capture of the list of plug-ins of the Rhinoceros 3D program.
    Figure 11: Rhinoceros 3D plug-in system, implemented using DLLs.

2.5. Location of DLLs

When the operating system must load a dynamic library, it follows a certain search order. On Windows systems it searches in this order:

  • The same directory as the executable.
  • The current working directory.
  • El directorio %SystemRoot%\System32.
  • The %SystemRoot% directory.
  • 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) or DYLD_LIBRARY_PATH (macOS).
  • The directories specified in the rpath executable.
  • 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 dependency search directories inside the executable. This variable is known as RPATH and is not available on Windows. To query the value of the 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 NAppGUI's CMakeLists.txt automatically set the RPATH 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 occupy 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, or union. They help the programmer to organize the code and the compiler to validate it, but they do not generate any binary code. They do not exist from the point of view of the linker.
  • 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 inside a *.c module will be considered private symbols not visible in link time and where the compiler is free to perform 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 global functions and variables (extern), available to the user of the library.
  • *.hxx: Declaration of public types: struct, union and enum.
  • *.inl: Declaration of functions and private variables. Only modules internal to the library will have access to these symbols.
  • *.ixx: Declaration of private types. Those shared between the modules of the library, but not with the outside.
If a function is only needed inside a *.c module, it is not included in a *.inl. It will be marked as static within the *.c itself. This way it will not be visible to the linker and will allow the compiler to perform optimizations.
In the same way, types that are only used within a specific module will be declared at the beginning of the *.c and not in the *.ixx.
In favor 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 the 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 core.def, the macro _core_api is defined.

Listing 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

This macro must precede all functions and variables declared in the *.h. Projects based on /src/CMakeLists.txt will automatically define the CORE_IMPORT and NAPPGUI_SHARED_LIB macros whenever dynamic libraries are to be generated (exported) or when they are to be used by an executable (import). In the case of third-party programs (not generated by /src/CMakeLists.txt) the import macros must be defined (CORE_IMPORT, GUI_IMPORT , etc) before including the headers.

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. Checking in DLLs

We can see, from a dynamic library binary, which public symbols it exports. On Windows we will use dumpbin /exports dllname, on Linux nm -D soname and on macOS nm -gU dylibname.

Public symbols from 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
...
Public symbols from 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
Public symbols from 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
❮ Back
Next ❯