semf
Design Concepts

semf follows some concepts that are helpful to understand in order to work efficiently and with fun.

Relaxed Layered Architecture

querdenker engineering focuses on relaxed layered architectures. To work with semf, a basic understanding of this design concept absolutely makes sense.

Each layer is assigned a specific aspect - this can be a partial functionality, a closed component or even a class. The aim of layer models is to be able to better structure and ultimately reduce the complexity of software systems, which then results in maximizing the cohesion of individual layers. Well-known layer models are the three-tier architecture or multi-tier architecture.

One of the powerful features of the layered architecture pattern is the separation of concerns among components. Components within a specific layer only deal with logic that pertains to that layer. For example, components in the storage layer only deal with storage logic, whereas components residing in the communication layer only deal with communication logic. This type of component classification makes it easy to build effective roles and responsibility models into your architecture. It also simplifies developing, testing, governing, and maintaining applications using this architecture pattern due to well-defined component interfaces and limited component scope.

In this context 'relaxed' refers to the fact that signal flow is allowed to pass one or multiple layers.

Control a Led by Button Example

Signals & Slots

Introduction

Using modern programming techniques, we often want to inform objects about some change. More generally, we want objects of any kind to be able to communicate with one another. For example if an input level has changed we want to display that by toggling a led.

Micronctroller vendor libraries often achieve this kind of communication using callbacks. A callback is a pointer to a function. If you want a processing function to notify you about some event you pass the pointer to the callback function to the processing function. The processing function then calls the callback when appropriate. While successful frameworks using this method do exist, callbacks can be unintuitive and may suffer from problems in ensuring the type-correctness of callback arguments.

Signals and Slots

In semf, we orientated ourselves at successful technologies like QT and implemented our own ultra efficient signal-slot-system. A signal is emitted when a particular event occurs, like an external interrupt or when communication data was received. A slot is a function that is called in response to a particular signal. semf's components have many pre-defined signals and slots.

Signal Slot
  • The signal and slot mechanism is type safe:
    The signature of a signal must match the signature of the receiving slot.
  • Signals and slots are loosely coupled:
    A class which emits a signal neither knows nor cares which slots receive the signal. Signals and slots can take any number of arguments of any type.

Signals

Signals are emitted by an object when its internal state has changed in a way that might be interesting to the objects client or owner. Signals are public access functions and can be emitted from anywhere but we recommend to only emit them from the class that defines the signal and its subclasses.

When a signal is emitted the slots connected to it are always executed immediately just like a normal function call. Execution of the code following the emit statement will occur once all slots have returned.

If several slots are connected to one signal the slots will be executed one after the other in the order they have been connected.

Slots

A slot is called when a signal connected to it is emitted. Slots are normal C++ functions and can therefore also be called as usual; their only special feature is that signals can be connected to them.

Since slots are normal member functions, they follow the normal C++ rules when called directly. However, as slots, they can be invoked by any component, regardless of its access level, via a signal-slot connection. This means that a signal emitted from an instance of an arbitrary class can cause a private slot to be invoked in an instance of an unrelated class.

A simple example

class
void slotfunction1()
{
printf("slotfunction1() was called;\n");
}
Signal<> signal;
StaticSlot<> slot1(slotfunction1);
// ...
signal.connect(&slot1);
signal();

Output

slotfunction1() was called;

Multiple Slots Example

In semf you can easily connect multiple slots to a signal wihtout using dynamic memory allocation:

void slotfunction1()
{
printf("slotfunction1() was called;\n");
}
void slotfunction2()
{
printf("slotfunction2() was called;\n");
}
Signal<> signal;
StaticSlot<> slot1(slotfunction1);
StaticSlot<> slot2(slotfunction2);
// ...
signal.connect(&slot1);
signal.connect(&slot2);
signal();

Output:

slotfunction1() was called;
slotfunction2() was called;

Disconnecting Slots Example

Slots can also easily be disconnected by using the disconnect() or clear() functions. By using disconnect() only a specific slot is disconnected, in difference to that the clear() function removes all registered slots from a signal.

// ...
void slotfunction1()
{
printf("slotfunction1() was called;\n");
}
void slot2()
{
printf("slotfunction2() was called;\n");
}
Signal<> signal;
StaticSlot<> slot1(slotfunction1);
StaticSlot<> slot2(slotfunction2);
// ...
signal.connect(&slot1);
signal.connect(&slot2);
printf("First call:");
signal();
signal.disconnect(&slot2);
printf("\nSecond call:");
signal();
signal.clear();
printf("\nThird call:");
signal();

Output:

First call:
slot1() was called;
slot2() was called;
Second call:
slot1() was called;
Third call:

Connecting Method Example

The standard usecase in semf is programming in classes. In the following example a member of a simple class implementation is connected and disconnected from a signal.

class MyClass
{
public:
MyClass(int i) : m_i(i) {}
void method()
{
printf("%i\n", m_i);
}
SEMF_SLOT(m_slot, MyClass, *this, method);
private:
int m_i;
};
MyClass a(1);
MyClass b(2);
MyClass c(3);
Signal<> signal;
// ...
signal.connect(a.slot);
signal.connect(b.slot);
printf("First call:");
signal();
signal.disconnect(b.slot);
signal.connect(c.slot);
printf("\nSecond call:");
signal();
#define SEMF_SLOT(name, className, object, function,...)
Creates a semf slot.
Definition: slot.h:30

Output:

First call:
1
2
Second call:
1
3

Signals with parameters

In the past examples the slots had no input parameter. It’s also possible to throw signals with parameters. Like normal functions, signals have no limit for the number of parameters.

void slotfunction1(int i)
{
printf("%i\n", i);
}
void slotfunction2(char* s, int i)
{
printf("%s: %i\n", s, i);
}
Signal<int> signal1;
Signal<char*, int> signal2;
StaticSlot<> slot1(slotfunction1, int);
StaticSlot<> slot2(slotfunction2, char*, int);
// ...
signal1.connect(&slot1);
signal2.connect(&slot2);
signal1(5);
signal2("hallo", sizeof("hallo"));

Output:

5
hallo: 6

Convert C++ member functions to C function pointers

As usual in embedded software we are writing C++ code based on C libraries and frameworks. Writing object-oriented C++ while dealing with callbacks as C-Style functions or C++ static member functions can mess up your software design. Using signals in combination with lambdas can prevent that mess.

The following example uses FreeRTOS. Typically the function xTaskCreate takes a C function pointer as first parameter containing the task routine. By using signals and lambdas we are able to convert our member function to a C function pointer without loosing the context of the executing object.

class Task
{
public:
void start()
{
static Signal<void*> localRun;
static SEMF_SLOT(slot, Signal<void*>, run, emitSignal);
localRun.clear();
localRun.connect(&slot);
xTaskCreate([](void* param)
{
localRun(param);
}, "c++ task", 1024, nullptr, 1, nullptr);
}
Signal<void*> run;
};