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.

In the last two chapters we have seen the basics for creating cross-platform applications using SDK functions. Something usual in Software Engineering is the possibility of reusing portions of code between different projects. Own SDK reference it is composed of a set of independent blocks that form a hierarchy of dependencies. In this chapter we will see how to isolate common functionality within a library, as well as link applications with it.


1. Static libraries

Following the line opened in the two previous chapters with the Die application, we are going to create a new application called Dice, also very simple, whose purpose 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("casino" "draw2d" NRC_EMBEDDED)
desktopApp("Die" "die" "casino" NRC_EMBEDDED NO)
desktopApp("Dice" "dice" "casino" NRC_EMBEDDED NO)

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\src\casino and "games/casino" in C:\nappgui\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 Resources 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 again [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 3: 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 4: 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 5: 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 */
}

Now we are going to create two new files within src/casino, ddraw.c and ddraw.h where we will carry out the drawing function. We already saw how Adding files in previous chapters.

Listing 6: 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 7: 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 8).

Listing 8: 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 ❯