On average, a software-controlled system consists out of 75 percent of decision logic, typically modelled with Dezyne. The remaining lines of code are handwritten and consists mostly of the project’s ordinary code, data processing and algorithms. When modelling your control system there comes a point that the Dezyne components need to interact with the system via the software environment. Say, it needs to control electronics and hardware. To accomplish this there are several ways, where each way has its strong point depending on the situation what is being asked from it.
- Injectables
- Direct interface implementation
- Foreign component
1. Injectables
The first category is named injectables because these stateless interfaces can be literally injected into the Dezyne locator (a primer facility) from where the components can retrieve them directly without instantiation or creation of bindings in the system component.
Say, you have an all synchronous and stateless interface named IConfiguration:
import Types.dzn;
interface IConfiguration
{
in void GetToastingTime(out MilliSeconds toastingTime);
behavior
{
on GetToastingTime: {}
} // behavior
} // interface
Then a component can require this interface via the locator as it is expected to be injected with the keyword injected in between ‘requires’ and the interface name ‘IConfiguration’. Usage of the injected interface is not different from normal required interfaces.
component Controller
{
provides IController api;
requires IHeater heater;
requires ITimer timer;
requires injected IConfiguration cfg;
behavior
{
enum State {Ready, Toasting};
State s = State.Ready;
[s.Ready]
{
on api.Toast():
{
heater.Start();
MilliSeconds toastingTime;
cfg.GetToastingTime(toastingTime);
timer.Create(toastingTime);
s = State.Toasting;
}
}
[...]
...
Important to know is the possibility that injected interfaces can be shared across multiple components in the Dezyne system. But since there is only a single instance of the injected interface, one should model it stateless. Because when it would be stateful, multiple acting components are not aware of state updates by other components and therefore this causes unwanted and confusing situations.
Injectables are ideal for software facilities like configuration, logging and small helpers for calculation or creation of data types for example. Also, injectables need no bindings to be arranged in Dezyne System components. Since software facilities are used often by many components this will spare a lot of repeating typing effort.
The handwritten implementation of the injectable interface is straightforward. It comes down to instantiate a Dezyne port of the ‘IConfiguration’ type and assign all ‘in event’ functors:
#pragma once
#include "IConfiguration.hh"
class IConfigurationGlue
{
public:
IConfigurationGlue()
: m_port{{{"api", nullptr, nullptr, nullptr}, {"", nullptr, nullptr, nullptr}}}
{
m_port.in.GetToastingTime = [this](size_t& toastingTime) {
toastingTime = 60 * 1000;
};
m_port.check_bindings();
}
IConfiguration& GetInjectablePort() { return m_port; }
private:
IConfiguration m_port;
};
In your handwritten project, instantiate the IConfigurationGlue class and ‘inject’ it into the Dezyne locator with its set() method:
dzn_locator.set(configurationGlue.GetInjectablePort());
What this method will do is keep a reference to the object in the locator; like a dictionary where the interface type acts as key. Important is to store the instance yourself. For learning purposes, when inspecting the generated code of the using component one can see that in the constructor member initializer list, the interface is fetched from the locator:
[...]
, cfg(dzn_locator.get<::IConfiguration>())
[...]
2. Direct interface implementation
The second flavour is in general the preferred way (according to the article author) of ‘glueing’ handwritten code. The implementation looks a lot like an injectable interface but this time the assignment of the in and out events are directly performed on the required port of the using component. This means it is not necessary to instantiate an own Dezyne port and ‘component meta’ in the handwritten component source file.
As example for the IHeater interface:
interface IHeater
{
in void Start();
in void Stop();
behavior
{
bool heating = false;
[!heating] on Start: heating = true;
[heating] on Stop: heating = false;
} // behavior
} // interface
The corresponding handwritten code looks like this:
#pragma once
#include "IHeater.hh"
class IHeaterGlue
{
public:
IHeaterGlue() {}
void SetupPeerPort(IHeater& port)
{
m_peerPort = port;
port.in.Start = [this] {
// e.g. send electronics the signal to switch on heating element
};
port.in.Stop = [this] {
// e.g. send electronics the signal to switch off heating element
};
}
private:
std::optional<std::reference_wrapper<IHeater>> m_peerPort;
};
Via the constructor of the glueing class it is possible to ‘dependency inject’ parameters and other distinguishing data that the handwritten code needs to be able communicate with the system environment. With this option of dependency injection there is a nice variation point to inject Mock objects of the system environment so that the handwritten component can be easily unit tested to ensure it calls the correct functions in the environment.
Further in this code a dedicated function SetupPeerPort() accepts a reference to the peer required port whose ‘in events’ will be assigned with lambda functions. The reference to this other port is stored in the class because it is needed at the moment the class wants to emit ‘out events’ (when defined in the interface of course). To do this, one needs access to the port, to call the ‘out event’ of choice. The developer may choose to place all this logic inside the constructor but in practice the order of building classes by the software project mostly lead to this separation into a dedicated class member function.
Next in your handwritten project, instantiate the IHeaterGlue class and the Dezyne system and connect the glue with the corresponding required port of the Dezyne system:
heaterGlueInstance.SetUpPort(dzn_system.heater);
Do not forget to call the check_bindings() helper that comes with Dezyne generated code to validate that each functor of the in and out event tables has been filled in completely. This helping function will throw an exception on detecting missing assignments. In practice this can happen when extending Dezyne models but forgetting to update the corresponding handwritten code. When the software project is being run, immediately during the construction phase this handy check will alert the developer for omissions.
A rule of thumb for handwritten components is that they should be implemented as stateless as possible. Meaning, let Dezyne take care of all state ‘record keeping’ in the interface and component models. Hence, adding states/booleans in handwritten components can be prone to errors since they are not in scope of the model checker.
Another point of interest is that the location of glueing interfaces is happening on the outer borders of the Dezyne system. In combination with a thread-safe shell you will achieve a safe encapsulation of the Dezyne system into which out events can be triggered by the system environment. This happens in a safe way because the dzn_pump will first redirect all incoming events (in and out) via its own FIFO queue before delivering to the Dezyne components. One must keep in mind that when the system environment is sending out events via the handwritten components, they are decoupled as such that they become asynchronous events. When the developer really wants to work with synchronous events then he/she must use the third kind of handwritten components, the Foreign Component.
3. Foreign Component
The third variant of code integration is the Foreign Component method. An example of this sort of handwritten component is the Timer component that offers the ITimer interface. When we have a look at its definition you will notice the absence of the behaviour clause. Dezyne interprets this that the behaviour is implemented ‘foreign’, by hand:
import ITimer.dzn;
component Timer
{
provides ITimer api;
} // foreign component
When generating code for such component the Dezyne code generator creates a base class with pure virtual methods that need to be overridden with a developer implementation. The namespace of this base class is prefixed with skel to indicate it provides for the initial skeleton that needs to be derived from:
#pragma once
#include "dzn/pump.hh"
#include "FCTimer.hh"
struct Timer : ::skel::Timer
{
explicit Timer(const dzn::locator& dzn_locator);
Timer(const Timer&) = delete;
Timer(Timer&&) = delete;
Timer& operator=(const Timer&) = delete;
Timer& operator=(Timer&&) = delete;
~Timer() override = default;
void api_Sleep(size_t waitingTimeMs) override;
void api_Create(size_t timeoutValueMs) override;
void api_Cancel() override;
private:
dzn::pump& m_pump;
};
After that the developer implements the class member functions including a constructor:
#include <chrono>
#include <thread>
#include "Timer.hh"
Timer::Timer(const dzn::locator& dzn_locator)
: ::skel::Timer(dzn_locator)
, m_pump(dzn_locator.get<dzn::pump>())
{
}
void Timer::api_Sleep(size_t waitingTimeMs)
{
std::this_thread::sleep_for(std::chrono::milliseconds(waitingTimeMs));
}
void Timer::api_Create(size_t timeoutValueMs)
{
m_pump.handle(reinterpret_cast<size_t>(this), timeoutValueMs, [&]() noexcept {
api.out.Timeout();
});
}
void Timer::api_Cancel()
{
m_pump.remove(reinterpret_cast<size_t>(this));
}
In this particular case of the Timer component, it utilizes the timing service of the dzn_pump. The dzn_pump is part of the thread-safe safe code generation option. Also what you can recognise in this example is that during construction, the dzn_pump is fetched from the locator and then its reference is stored as class member for use of the timing service. This means that the Timer foreign component is dependent on the fact that the Dezyne System is equipped with a dzn_pump. As mentioned earlier, this happens automatically when letting Dezyne create a thread-safe shell.
As announced in the second kind of glueing with direct Interface implementation, the Foreign Component is also capable of emitting synchronous out events to its using component. The reason why this is possible is because a Foreign component is closely ‘sitting’ in between normal Dezyne components. If a thread-safe shell is generated, again, the Foreign Component is residing within its ‘walls’. And not just outside like with direct interface implementation. Synchronous out events can have its advantage as it they can carry parameter arguments as a way to give back data to the caller synchronously. But caution must be taken that the triggering of such synchronous out event must not originate from a spontaneous system environment trigger. In such scenario the synchronous out event actually becomes asynchronous and this will violate the Dezyne execution runtime since the model didn’t specify that kind of behaviour. Therefore, extra attention must be paid to modelling and handwriting of Foreign Components to ensure it fits in the Dezyne execution runtime.
Conclusion
With three options to glue Dezyne generated code with its software environment, a developer has multiple choices. In practice you we see a mix of all three. For software facilities that have an all synchronous (only in-events) and a stateless interface, the injectable option is the best fit. For communication with the software environment to control electronics and so on, the developer can choose from the direct interface implementation and foreign components. Each one has its advantage and some say it can even be a matter of aesthetics. That is all fine as long as the developer understands and follows the execution semantics of the Dezyne runtime.