Cross-platform C SDK logo

Cross-platform C SDK

Libraries

❮ 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 use of libraries will allow us to share common functionality between several projects. As an example, the NAppGUI SDK has been organized into several static link libraries. Each project will use one or the other depending on its requirements (more information in SDK reference). Since libraries export certain functions to be used by third parties, special care must be taken in organizing the code. We use the following file structure and recommend that you do the same:

  • *.c: Implementation files (definition of functions).
  • *.h: Public header files. Declaration of functions that the library user can use.
  • *.hxx: Declaration of public types: Basically struct and enum.
  • *.inl: Declaration of private functions. The user will not have access to them, only the internal modules of the library.
  • *.ixx: Declaration of private types. Those shared between the modules of the library, but not with the outside.
If a function is only needed within a *.c module, don't include it in the *.inl. Declare it as static within the same module. You will allow the compiler to perform optimizations.
Similarly, types that are only used as support within the module, declare them at the beginning of the *.c and not in the *.ixx.
For the sake of maintainability and scalability of your code, keep type and function declarations as private as possible. Declare public what is strictly necessary and private what cannot be included locally within the *.c.

1. Static libraries

To illustrate the use of libraries, we are going to take as an example the Die application and a new one called Dice, also very simple, whose task is to randomly draw 6 dice (Figure 1). Moving the slider will change the values.

Capture of the application Dice, which draws six dice at random.
Figure 1: Dice application.
The source code of Dice is in the folder src/demo/Dice of the distribution.

It is not very complicated to intuit that we could reuse the parametric drawing routine of our previous application. One way to do this would be to copy this routine from Die to Dice, but this is not advisable 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. We open the src/CMakeLists.txt and we add the following lines:

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

Where we have used the command staticLib(), which is the analog of desktopApp().

1
staticLib(libPath depends nrcMode)
  • libPath: Library directory within the solution: "casino" will create the project in the folder C:\nappgui_sdk\src\casino and "games/casino" in C:\nappgui_sdk\src\games\casino. The name of the project is determined by the last part of the pathname: "casino" in both cases.
  • depends: Library dependencies. As in desktop applications, it is only necessary to indicate the highest level (draw2d in this case). Each library is responsible for linking with those below. draw2d will include geom2d and so on. In SDK reference you have the complete dependency graph of NAppGUI.
  • nrcMode: It supports the three values: NRC_NONE, NRC_EMBEDDED and NRC_PACKED, as we saw in Resource distribution.

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

Scheme showing the dependencies of Die and Dice.
Figure 2: Application dependencies tree, focused on the casino library.

Pressing [Generate] in CMake, adds the casino library to our solution, as well as a link to it in both applications (Figure 3). As was the case when creating a new application, several default files appear, which are:

Visual Studio capture showing a new project with the casino library.
Figure 3: Static casino library, integrated into the solution.
  • casino.hxx: File of types. Here we will put the definitions of public types. In this example it will not be necessary.
Listing 1: demo/casino/casino.hxx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* casino */

#ifndef __CASINO_HXX__
#define __CASINO_HXX__

#include "draw2d.hxx"

/* TODO: Define data types here */

#endif
  • casino.h: 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 start and end code of the library, if necessary.
Listing 2: demo/casino/casino.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* casino */

#include "casino.hxx"

__EXTERN_C

void casino_start(void);

void casino_finish(void);

__END_C
  • casino.c: Implementation of general functions.
Listing 3: 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 two new files were created inside src/demo/casino, ddraw.c and ddraw.h where we will port the drawing function. We already saw how to Add files.

Listing 4: demo/casino/ddraw.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Die drawing */

#include "casino.hxx"

void die_draw(
        DCtx *ctx, 
        const real32_t x, 
        const real32_t y, 
        const real32_t width, 
        const real32_t height, 
        const real32_t padding,
        const real32_t corner,
        const real32_t radius,
        const uint32_t face);
Listing 5: demo/casino/ddraw.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* Die drawing */

#include "ddraw.h"
#include "draw2dall.h"

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

static const real32_t i_MAX_PADDING = 0.2f;

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

void die_draw(DCtx *ctx, const real32_t x, const real32_t y, const real32_t width, const real32_t height, const real32_t padding, const real32_t corner, const real32_t radius, const uint32_t face)
{
    color_t white = color_rgb(255, 255, 255);
    color_t black = color_rgb(0, 0, 0);
    real32_t dsize, dx, dy;
    real32_t rc, rr;
    real32_t p1, p2, p3;

    dsize = width < height ? width : height;    
    dsize -= bmath_floorf(2.f * dsize * padding * i_MAX_PADDING);
    dx = x + .5f * (width - dsize);
    dy = y + .5f * (height - dsize);
    rc = dsize * (.1f + .3f * corner);
    rr = dsize * (.05f + .1f * radius);
    p1 = 0.5f * dsize;
    p2 = 0.2f * dsize;
    p3 = 0.8f * dsize;

    draw_fill_color(ctx, white);
    draw_rndrect(ctx, ekFILL, dx, dy, dsize, dsize, rc);
    draw_fill_color(ctx, black);

    if (face == 1 || face == 3 || face == 5)
        draw_circle(ctx, ekFILL, dx + p1, dy + p1, rr);

    if (face != 1)
    {
        draw_circle(ctx, ekFILL, dx + p3, dy + p2, rr);
        draw_circle(ctx, ekFILL, dx + p2, dy + p3, rr);
    }

    if (face == 4 || face == 5 || face == 6)
    {
        draw_circle(ctx, ekFILL, dx + p2, dy + p2, rr);
        draw_circle(ctx, ekFILL, dx + p3, dy + p3, rr);
    }

    if (face == 6)
    {
        draw_circle(ctx, ekFILL, dx + p2, dy + p1, rr);
        draw_circle(ctx, ekFILL, dx + p3, dy + p1, rr);
    }
}

What does it really mean that Die and Dice have a dependency with a 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 block for both. Several things have happened within the Visual Studio solution:

  • The two applications know where the casino is located, so they can do #include "casino.h" without worrying about adding the route to Additional Include Directives of the project.
  • The binary code of the casino functions will be included in each executable in the linking process. Nor should we configure the *.lib at Additional Dependencies of the Visual Studio Linker. CMake already took care of doing it.
  • Any change made in the casino will force to recompile the applications due to the previous point. Again, Visual Studio will know how to do it in the most efficient way possible. It is not necessary to go one by one, with select Build->Build Solution everything necessary will be compiled and updated.

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


2. Dynamic libraries

Dynamic libraries are, to the executable code, what NRC_PACKED to resources. That is, the library code is linked and loaded at run time and not at compile time. Using static libraries, as we have just done in the previous section, both in Die.exe like in Dice.exe there will be a copy of the binary code of the function die_draw() that was added during the linking process. If we created a casino as a dynamic library, the casino.dll file would be generated (Dynamic-Link Library) that we should distribute together with Die.exe and Dice.exe. The result would be smaller executables since the common code of the DLL would be linked at runtime by both applications (Listing 6).

Listing 6: Hypothetical distribution of the applications, together with the .dll.
1
2
3
casino.dll        102,127
Die.exe            84,100
Dice.exe           73,430
At the moment, the current version of NAppGUI does not support the creation of DLLs. This type of project will be included in future revisions of the SDK.

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. This is precisely what operating systems do. For example, Die.exe will ultimately need to access the Windows API functions. If all the applications had to link the Windows binaries statically, their size would grow disproportionately and a lot of space would be wasted within the file system. The system implements its functions by DLLs (located in C:\Windows\System32) and applications access them at runtime.

Another great advantage of DLLs is the possibility of adding executable code after compiling the application. This is the operating mechanism of the plug-ins that many commercial applications use. 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 4). The problem with static libraries is that they are linked after the compilation phase, so it is necessary to have the original source code, recompile it and send it back to the end user. This makes it impossible to create extensions once the program has been distributed. Thanks to the DLLs this can be done.

Capture of the Rhinoceros 3D program plug-in list.
Figure 4: Rhinoceros 3D plug-in system, implemented by DLLs.

2.2. Disadvantages of DLLs

The main drawback of the use of DLLs is the incompatibility that can occur between different versions of a library. Going back to our two games, suppose we launched a new version of the application Dice.exe which implies changes in casino.dll. In that case, the distribution of our suite would be like this:

1
2
3
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 work because it is not compatible with the new version of the DLL. This problem brings many developers head-on and has been christened as DLL Hell. Since in this example we work on a "controlled" environment we could solve it without too many problems, creating a new version of all the applications running under casino.dll.v2, but this is not always possible.

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

Let's suppose now that our company develops just casino.dll and third companies are those that work on the final products. Now each product will have its production and distribution cycles (no-controlled 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 next scenario:

1
2
3
4
5
6
7
/Apps/Die
casino.dll        114,295  (v5)
Die.exe            86.100  (v8)

/Apps/Dice
casino.dll        106,386  (v2)
Dice.exe           72,105  (v1)

Seeing this we suspect that the benefits of using DLLs are not so good, especially in relation to space optimization and load times. The fact is that it can be aggravated even more. Normally, 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 each library with which it links. Using static libraries, it can reduces the size of the executable considerably (Figure 5), since the linker knows exactly what specific functions the application uses and adds the strictly necessary code. However, using DLLs, we must distribute the complete library for very few functions that the executable uses (Figure 6). In this case, space is being wasted and unnecessarily increasing application load times.

Scheme showing in static code link.
Figure 5: With static libraries optimizes the space and loading times of this application.
Scheme showing in dynamic code link.
Figure 6: With dynamic libraries this application takes more than it should and increase its load times.
❮ Back
Next ❯