Purpose

This text briefly describes the C++ code that is generated from Dezyne models and the integration thereof. Though the syntax and data structures in other languages are different the concepts of the generated code and its integration are the same.

Introduction

Every wellformed Dezyne model can be automatically converted into a corresponding wellformed C++ representation. A verified Dezyne model can be automatically converted into a corresponding C++ representation which when executed exhibits the same behaviour as one can observe in the Dezyne simulation and verification of said model.

In Dezyne there are three model types: interface, component and system.

In this chapter we cover the code which is generated from these models as well as the way the generated code might be integrated.

Interfaces

Dezyne turns an interface such as:

interface myInterface
{
  in void event1();
  enum Result {OK,NOK};
  in Result event2();
  out void event3();

  behaviour
  {
    on event1: {}
    on event2: reply(Result.OK);
    on event2: reply(Result.NOK);
    on optional: event3;
  }
}

into the following C++ class representation:

struct myInterface
{
  struct Result {enum type{OK,NOK}; };
  struct
  {
    std::function  event1;
    std::function  event2;
  } in;
  struct
  {
    std::function  event3;
  } out;
};

Each event in an interface is a slot to which a value of something with the appropriate callable signature can be assigned. The behaviour section of an interface does not contribute to the code being generated. A callable value in C++ is either: a function pointer or a functor. For example:

void foo(){}

MyInterface port;

port.out.event3 = foo;
port.in.event1 = port.out.event3;

Note that the last statement above short circuits the in_event to the out_event as is described in the Dezyne interface model.

Components

A component is an aggregation of interfaces supplemented with a behaviour section that defines what happens if an event is invoked. The processing of an event may lead to other events being generated

A component implements its “provided in” and “required out” interface functions; The corresponding interface function references are initialized in the constructor of the component. These are the events directed at the component.

The “provided out” interface functions are implemented by a client of the component; the interface function references must be initialized after the component is instantiated. In this way the Dezyne component can call a function of the caller of the component.

The “required in” interface functions are implemented by called components; the interface function references must be initialized after the component is instantiated. In this way the Dezyne component can call a supporting hand written function.

component myComponent
{
  provides myInterface provided;
  requires myInterface required;

  /* behavioral internals description */
  behaviour
  {
    on provided.event1(): required.event1();
    on provided.event2():
    {
        myInterface.Result res = required.event2();
        reply(res);
    }
    on required.event3(): provided.event3();
  }
}

in which case the corresponding C++ representation will look like this:

struct myComponent
{
  myInterface provided;
  myInterface required;

  myComponent()
  {
    provided.in.event1 = [this]{provided_event1(); };
    provided.in.event2 = [this]{provided_event2(); };
    required.out.event3 = [this]{required_event3(); };
  }

  void provided_event1() {required.in.event1(); }

  myInterface::Result::type provided_event2(){
    return required.in.event2();
  }

  void required_event3() {provided.out.event3(); }
};

Systems

Along the same lines a Dezyne system may aggregate components and other systems and bind them together by their ports. For example:

component MySystem
{
  provides MyInterface provided_port;
  requires MyInterface required_port;

  system
  {
    MyComponent top;
    MyComponent middle;
    MyComponent bottom;

    provided_port <=> top.provided_port;
    top.required_port <=> middle.provided_port;
    middle.required_port <=> bottom.provided_port;
    bottom.required_port <=> required_port;
  }
}

or depicted in a diagram:

             O
            -|-
            / \

 +-----------+-----------+
 |  system   |           |
 |   +-------+-------+   |
 |   |               |   |
 |   |      top      |   |
 |   |               |   |
 |   +-------+-------+   |
 |           |           |
 |   +-------+-------+   |
 |   |               |   |
 |   |    middle     |   |
 |   |               |   |
 |   +-------+-------+   |
 |           |           |
 |   +-------+-------+   |
 |   |               |   |
 |   |     bottom    |   |
 |   |               |   |
 |   +-------+-------+   |
 |           |           |
 +-----------+-----------+

             O
            -|-
            / \

Integration

Constructing such a system using Dezyne is straightforward. Every model can be automatically converted into code and by the hierarchical nature of Dezyne all components and systems slot together automatically. Two facilities support this: the Dezyne runtime and the Dezyne locator. Both are provided by Dezyne.

The Dezyne runtime implements the execution semantics and is an essential element of an operational system. It takes care of decoupling the events between the caller and the callee when this is required.

The Dezyne locator is a container for references used throughout the system [1]. The locator allows injecting the implementation behind a port deep into the system from the outside.

With these elements integration involves only a few steps:

  • Construct the runtime

  • Instantiate an object of the system

  • Bind the calls from the system to the hand written code

  • Include calls to the system in the appropriate locations

In C++ the main function for this system might look like this:

#include "MySystem.hh"

#include "dezyne/runtime.hh"
#include "dezyne/locator.hh"

int main()
{
  //construct the runtime
  dezyne::locator loc;
  dezyne::runtime rt;
  dezyne::illegal_handler illegal_handler;
  loc.set(rt);
  loc.set(illegal_handler);

  //construct the system
  MySystem system(loc);
  system.dzn_meta,name = "MySystem"; // optional, used for debug trace info

  //connect the callbacks from the system to the hand written code
  //c++11 uses lambdas
  system.provided_port.out.event = []{
    std::cout << "system.provided_port.out.event" << std::endl;
  };
  system.required_port.in.event  = []{
    std::cout << "system.required_port.in.event" << std::endl;
  };

  //and finally fire some of the external events
  //at the top of the hierarchy
  system.provided_port.in.event();
  //and at the bottom of the hierarchy
  system.required_port.out.event();
}

In the example you can see that the locator facility is also responsible for passing an instance of the runtime into the system.

An illegal_handler can be provided by the user which then overrides the default illegal_handler. This function is called if the runtime hits an illegal situation which can only happen if hand written code violates the specification of an interface. The default implementation maps onto an assert.

Dezyne offers a command line instruction to generate an example main file based on a (system) component for any of the supported languages, see the command line help ("dzn --help").

Injection example

interface Foo
{
  in void bar();
  behaviour
  {
    on bar:{}
  }
}

component MyComponent2
{
  provides Foo provided_port;
  requires injected Foo required_port;
  behaviour { /* ... */ }
}
int main()
{
  dezyne::locator loc;
  dezyne::runtime rt;
  loc.set(rt);

  Foo foo;
  foo.in.bar = []{/*no op*/};

  loc.set(foo);

  MyComponent comp(loc);

  comp.provided_port.in.in_event();
}

Pitfalls

Dezyne can only guarantee correctness of the operation of components if their context is well behaving. This means that the context must adhere to the behaviour as specified in its interfaces. If an interface is implemented in hand written code then the author of the code is reponsible for the correctness. In other words Dezyne relies on its interface contracts for functionality implemented by hand.

Example 1

interface DeviceIF
{
  enum Result { OK, NOK };
  in Result turnon();
  in void turnoff();
  out void led_ok();

  behaviour
  {
    enum State { Off, On };
    State state = State.Off;

    [state.Off]
    {
      on turnon: { reply( Result.OK ); state = State.On; }
      on turnon: { reply( Result.NOK); }
      on turnoff: {}
    }
    [state.On]
    {
      on turnon: illegal;
      on turnoff: { state = State.Off; }
    }
    on optional: {led_ok;}
  }
}

The turnon event is bound to the function below and the callback of led_ok leads to the triggering of the event led_ok in the Dezyne component.

uint8_t turnon(DeviceIF* self)
{
    // set the bit to switch the Led on
    .....
    // immediate callback
    self->out.led_ok(self);

    return DeviceIF_Result_OK;
}

At first looks everything is fine. The function does some hardware setting to switch on the Led and then does a callback into Dezyne. The callback is optional in the interface and defined outside any guard so it can happen any time. The hand written code always triggers this event which is ok for an optional event. However, according to the definition of the turnon event, the ocurrance of the led_ok during the turnon is not allowed. Doing so might lead to unspecified and hence unverified side effects.

Example 2

The Dezyne interface code is the same but in the hand written code an extra condition is added that controls whether or not the Led is switched on.

uint8_t turnon(DeviceIF* self)
{
    // test some hardware condition
    if (....) {
        // set the bit to switch the Led on
        .....
    }

    return DeviceIF_Result_OK;
}

Though this code formally adheres to the interface, conceptually something is very wrong here since the state maintained in Dezyne does not always reflect the actual state of the hardware. This may impact assumptions in the Dezyne model. An interface that is the reflection of an outside component (a hardware or software component outside Dezyne) should be a complete and correct representation for all the aspects that are dealt with inside the Dezyne model. See also the article about armouring interfaces (in the news section on www.verum.com).


1. For a low footprint system it would probably need to be optimized out. The C code generator has an option to do so.