Cross-platform C SDK logo

Cross-platform C SDK

Hello, C++!

❮ 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.

Web servers are written in C, and if they're not, they're written in Java or C++, which are C derivatives, or Python or Ruby, which are implemented in C. Rob Pike


Object-oriented programming (encapsulation, inheritance and polymorphism) is a very powerful tool for modeling certain type of problems. However, in NAppGUI think it is wrong to impose a hierarchy of classes at the SDK level, since it is too low level. The SDK is closer to the operating system and the machine than to real-world problems solved by applications, where an object-oriented strategy can be more successful.

Although NAppGUI has been designed to create "pure" C applications, it is possible to use C++ or combine both. We'll give some tips, porting our Hello, World! application to C++ (Figure 1).

Capture of the program Hello, world! in C++, Windows version. Capture of the program Hello, world! in C++, macOS version.
Figure 1: Migration of Hello, World! to C++.

1. Encapsulation

NAppGUI does not impose any class hierarchy, which leaves the programmer the freedom to perform the encapsulation through their own classes. Obviously, given C is included in C++, we can call any SDK C function within a class function member. For example, we can encapsulate the main window in this way.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class MainWindow
{
public:
    MainWindow();
    ~MainWindow();

private:
    static void i_OnClose(MainWindow *window, Event *e);
    static void i_OnButton(MainWindow *window, Event *e);
    Panel *i_panel(void);

    Window *window;
    TextView *text;
    uint32_t clicks;
};

As you can see, with respect to C version, i_panel no longer need a parameter since they use the implicit this pointer to access the class members.


2. Class Callbacks

The event handlers are C functions whose first parameter is a pointer to the object receiving the message. This works in the same way using static functions within a C++ class:

1
2
3
4
5
...
static void i_OnClose(MainWindow *window, Event *e);
...
window_OnClose(this->window, listener(this, i_OnClose, MainWindow));
...

However, we may want to use member functions as callbacks, using implicit this pointer as the receiver. To do this, we force our MainWindow to implement the IListener interface and use the macro listen instead of listener().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class MainWindow : public IListener
{
...
    void i_OnClose(Event *e);
    void i_OnButton(Event *e);
...
};

void MainWindow::i_OnButton(Event *e)
{
    String *msg = str_printf("Button click (%d)\n", this->clicks);
    ...
}
...
button_OnClick(button, listen(this, MainWindow, i_OnButton));
...
IListener is a C++ interface that allows to use class members as event handlers.

It is also possible send the event to a different object (and of different class) to the control owner. We have to indicate the receiver as the first parameter of listen, as we see below. Pressing the close button will be processed in App class and not in MainWindow.

 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
class App : public IListener
{
public:
    App();
    ~App();
    void i_OnClose(Event *e);

private:
    MainWindow *main_window;
};

class MainWindow : public IListener
{
public:
    MainWindow(App *app);
}

MainWindow::MainWindow(App *app)
{
   ...
   window_OnClose(this->window, listen(app, App, i_OnClose));
   ...
}

void App::i_OnClose(Event *e)
{
    osapp_finish();
}
We can establish as an event receiver, any object that implements the IListener interface.

3. Combine C and C++ modules

A C/C++ project selects the compiler based on the file extension. For *.c will use the C compiler and for *.cpp the C++ compiler. The same project can combine modules in both languages if we consider the following.

3.1. Use C from C++

No problem if function declarations in the C header are between the macros: __EXTERN_C and __END_C.

1
2
3
4
5
6
7
__EXTERN_C

real32_t mymaths_add(const real32_t a, const real32_t b);

real32_t mymaths_sub(const real32_t a, const real32_t b);

__END_C
__EXTERN_C and __END_C are alias for extern "C" {}. This tells to C++ compiler not to use name mangling with C Functions.

3.2. Use C++ from C

C does not understand the keyword class and will give a compiler error when including C++ headers. It is necessary to define a C wrapper over C++ code.

Listing 6: mywindow.h
1
2
3
4
5
6
7
8
9
__EXTERN_C

typedef struct _mywin_t MyWindow;

MyWindow *mywindow_create();

void mywindow_move(MyWindow *window, const real32_t x, const real32_t y);

__END_C
Listing 7: mywindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class MainWindow
{
public:
    MainWindow();
    void move(const real32_t x, const real32_t y);
};

MyWindow *mywindow_create()
{
    return (MyWindow*)new MainWindow();
}

void mywindow_move(MyWindow *window, const real32_t x, const real32_t y)
{
    ((MainWindow*)window)->move(x, y);
}

4. New and delete overload

C++ uses the new and delete operators to create dynamic object instances. We can make allocations using Heap, the NAppGUI built-in Heap - Memory manager manager, in order to optimize C++ and control the Memory Leaks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MainWindow : public IListener
{
    ...
    void *operator new(size_t size) 
    { 
        return (void*)heap_malloc((uint32_t)size, "MainWindow"); 
    }

    void operator delete(void *ptr, size_t size) 
    { 
        heap_free((byte_t**)&ptr, (uint32_t)size, "MainWindow");
    }
    ...
};

5. The whole Hello C++

Listing 9: demo/hellocpp/main.cpp
  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
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/* NAppGUI C++ Hello World */
    
#include "nappgui.h"

class App;

class MainWindow : public IListener
{
public:
    MainWindow(App *app);
    ~MainWindow();

    void *operator new(size_t size) { return (void*)heap_malloc((uint32_t)size, "MainWindow"); }
    void operator delete(void *ptr, size_t size) { heap_free((byte_t**)&ptr, (uint32_t)size, "MainWindow"); }

private:
    void i_OnButton(Event *e);
    Panel *i_panel(void);

    Window *window;
    TextView *text;
    uint32_t clicks;
};

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

class App : public IListener
{
public:
    App();
    ~App();
    void i_OnClose(Event *e);
    void *operator new(size_t size) { return (void*)heap_malloc((uint32_t)size, "App"); }
    void operator delete(void *ptr, size_t size) { heap_free((byte_t**)&ptr, (uint32_t)size, "App"); }

private:
    MainWindow *main_window;
};

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

void MainWindow::i_OnButton(Event *e)
{
    String *msg = str_printf("Button click (%d)\n", this->clicks);
    textview_writef(this->text, tc(msg));
    str_destroy(&msg);
    this->clicks += 1;
    unref(e);
}

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

Panel *MainWindow::i_panel(void)
{
    Panel *panel = panel_create();
    Layout *layout = layout_create(1, 3);
    Label *label = label_create();
    Button *button = button_push();
    TextView *textv = textview_create();
    this->text = textv;
    label_text(label, "Hello!, I'm a label");
    button_text(button, "Click Me!");
    button_OnClick(button, IListen(this, MainWindow, i_OnButton));
    layout_label(layout, label, 0, 0);
    layout_button(layout, button, 0, 1);
    layout_textview(layout, textv, 0, 2);
    layout_hsize(layout, 0, 250);
    layout_vsize(layout, 2, 100);
    layout_margin(layout, 5);
    layout_vmargin(layout, 0, 5);
    layout_vmargin(layout, 1, 5);
    panel_layout(panel, layout);
    return panel;
}

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

void App::i_OnClose(Event *e)
{
    osapp_finish();
    unref(e);
}

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

MainWindow::MainWindow(App *app)
{
    Panel *panel = i_panel();
    this->window = window_create(ekWNSTD, &panel);
    this->clicks = 0;
    window_title(this->window, "Hello, C++!");
    window_origin(this->window, v2df(500, 200));
    window_OnClose(this->window, IListen(app, App, i_OnClose));
    window_show(this->window);
}

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

MainWindow::~MainWindow()
{
    window_destroy(&this->window);
}

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

App::App(void)
{
    this->main_window = new MainWindow(this);
}

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

App::~App()
{
    delete this->main_window;
}

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

static App *i_create(void)
{
    return new App();
}

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

static void i_destroy(App **app)
{
    delete *app;
    *app = NULL;
}

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

#include "osmain.h"
osmain(i_create, i_destroy, "", App)

6. C++ Math templates

In NAppGUI there are two versions for all mathematical types and functions (Listing 10): float (real32_t) and double (real64_t). We can use one or other as appropriate in each case.

Listing 10: bmath.h header (partial).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* Math funcions */

#include "osbs.hxx"

__EXTERN_C

real32_t bmath_cosf(const real32_t angle);

real64_t bmath_cosd(const real64_t angle);

real32_t bmath_sinf(const real32_t angle);

real64_t bmath_sind(const real64_t angle);

extern const real32_t kBMATH_PIf;
extern const real64_t kBMATH_PId;
extern const real32_t kBMATH_SQRT2f;
extern const real64_t kBMATH_SQRT2d;

__END_C
All functions and types in simple precision end with the suffix "f" and double precision in "d".

When we implement more complex geometric or algebraic functions, it is not easy to be clear in advance what the correct precision is. When in doubt we can always choose to use double, but this will have an impact on performance, especially by the use of memory bandwidth. Consider the case of 3D meshes with thousands of vertices. It would be great to have both versions and be able to use one or the other according to each specific case.

Unfortunately, the "pure" C language does not allow programming with generic types, apart from using horrible and endless macros. We must implement both versions (float and double), with the associated maintenance cost. C++ solves the problem thanks to the templates (template<>). The counterpart is that, normally, we must "open" the implementation and include it in the header .h, since the compiler doesn't know how to generate the machine code until the template is instantiated with an specific data type. This collides head-on with our Standards, especially in the information hidding. Next we will see how to use the C++ templates to get the best of both cases: Generic programming, hidden implementations and keepping "clean" headers.

Like there is a *.h header for each mathematical module, also exist a *.hpp counterpart only usable from C++ modules (Listing 11).

Listing 11: bmath.hpp header (partial).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Math funcions */

#include "osbs.hxx"

template<typename real>
struct BMath
{
    static real(*cos)(const real angle);

    static real(*sin)(const real angle);

    static const real kPI;
    static const real kSQRT2;
};

These templates contain function pointers, whose implementations are hidden in bmath.cpp. In (Listing 12) we have an example of use.

Listing 12: Implementation of a generic algorithm.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include "bmath.hpp"

template<typename real>
static void i_circle(const real r, const uint32_t n, V2D<real> *v)
{
    real a = 0, s = (2 * BMath<real>::kPI) / (real)n;
    for (uint32_t i = 0; i < n; ++i, a += s)
    {
        v[i].x = r * BMath<real>::cos(a);
        v[i].y = r * BMath<real>::sin(a);
    }
}

This algorithm is implemented within a C++ module (Listing 13), but we want to be able to call it from other modules, both C and C++. For this we will define the two headers: *.h (Listing 14) and *.hpp (Listing 15).

Listing 13: mymath.cpp. Implementation.
 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
#include "mymath.h"
#include "mymath.hpp"
#include "bmath.hpp"

template<typename real>
static void i_circle(const real r, const uint32_t n, V2D<real> *v)
{
    real a = 0, s = (2 * BMath<real>::kPI) / (real)n;
    for (uint32_t i = 0; i < n; ++i, a += s)
    {
        v[i].x = r * BMath<real>::cos(a);
        v[i].y = r * BMath<real>::sin(a);
    }
}

void mymath_circlef(const real32_t r, const uint32_t n, V2Df *v)
{
    i_circle<real32_t>(r, n, (V2D<real32_t>*)v);
}

void mymath_circled(const real64_t r, const uint64_t n, V2Dd *v)
{
    i_circle<real64_t>(r, n, (V2D<real64_t>*)v);
}

template<>
void(*MyMath<real32_t>::circle)(const real32_t, const uint32_t, V2D<real32_t>*) = i_circle<real32_t>;

template<>
void(*MyMath<real64_t>::circle)(const real64_t, const uint32_t, V2D<real64_t>*) = i_circle<real64_t>;
Listing 14: mymath.h. C Header.
1
2
3
4
5
6
7
8
9
#include "geom2d.hxx"

__EXTERN_C

void mymath_circlef(const real32_t r, const uint32_t n, V2Df *v);

void mymath_circled(const real64_t r, const uint64_t n, V2Dd *v);

__END_C
Listing 15: mymath.hpp. C++ Header.
1
2
3
4
5
6
7
#include "v2d.hpp"

template<typename real>
struct MyMath
{
    void (*circle)(const real r, const uint32_t n, V2D<real> *v);
};

Now we can use our math library in C and C++, both in float like in double precision (Listing 16).

Listing 16: Use of mymaths in generic C++ algorithms.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include "mymath.hpp"
#include "t2d.hpp"

template<typename real>
static void i_ellipse(const real r1, const real r2, const uint32_t n, V2D<real> *v)
{
    T2D<real> transform;
    T2D<real>::scale(&transform, r1, r2);

    MyMath<real>::circle(1, n, v);

    for (uint32_t i = 0; i < n; ++i)
        T2D<real>::vmult(&transform, &v[i]);
}
❮ Back
Next ❯