Generated Code

Warning

The Coco C++ generator is still in beta and is therefore subject to change. Specifically, the following features are yet to be finalised:

  • Termination: currently all Coco state machines must manually transition to the Terminated state in order to shutdown. We are considering adding an automatic form of termination to make this easier.
  • Mocks: the current version is available as a preview, and is still subject to change. We are keen to receive user feedback on this, and on other aspects of testing the generated C++ code before finalising.
  • Ports: there may be some technical changes to how the single-threaded-runtime-specific methods are implemented.

The Coco C++ generator is able to generate standards-compliant C++ code for every construct in a Coco module. The generated C++ code follows the structure of the Coco code in that for each Coco file of the form A/M.coco, two C++ files are created: A/M.h and A/M.cc. Each file will contain the C++ corresponding to all declarations that are contained in that module. This documentation gives an overview of the C++ that is created for each declaration and how to integrate it into the existing codebase.

See also

Code Generation for recommendations on how to structure Coco models to ensure that they can be easily integrated into existing code.

Code Generation Options for information on how to customise the generated code, including options to control:

Attributes for information on how to specify the mapping of external types to corresponding C++ types.

Testing for information on how to create GMock-compatible tests for the generated C++ code.

Code Index for details on automatically creating JSON indexes of the generated code, to allow for custom code generators to integrate with Coco’s generator (e.g. generation of Python bindings, or RPC bindings, etc).

Build System Integration for information on how to integrate the Coco command-line tooling into a build system, to automate generation of code as part of your existing build system.

Basic Imperative Language

Declaration Generated Code
Enum E where E is simple A C++ enum class declaration called E. For each member function f of the enum, a function called E_f is created at the same level that has parameter called self for the enum value.
Enum E where E is not simple

A C++ struct declaration called E that contains:

  • For each enum case C:
    • A struct C_ that contains a field for each field of the enum case;
    • A static function C that returns a value of type E corresponding to the case C.
  • A type-alias called type that represents the overall type used to model the enum, which will either be defined as std::variant<C1, ..., Cn> (where each Ci is one of the case structs). Note that if standard is CPP17 or later, then std::variant is used, otherwise coco::variant is used.
  • For each member function f, a static function f taking a parameter self of type type.

Additionally, if the enum satisfies the Eq trait, then operator== and operator!= are generated on each case struct.

For example, given the Coco declaration:

enum E {
  C1(x : Bool);
  C2(y: Int);
}

The following C++ will be generated:

struct E {
  struct C1 {
    bool x;
  };
  struct C2 {
    int y;
  };
  using type = std::variant<C1, C2>;
};
External Constant Nothing is generated, as this declaration must correspond to a pre-defined C++ declaration. Instead, @CPP.mapToValue should be used to specify what C++ value should be used instead.
External Function f If @CPP.mapToValue, or one of the other relevant C++ attributes is specified, then nothing is generated and instead the corresponding declaration is used. Otherwise, a C++ function declaration called f is created but only the header is generated: the function’s implementation is left to be defined by the user.
External Type T Nothing is generated, as this declaration must correspond to a pre-defined C++ declaration. Instead, @CPP.mapToType should be used to specify what C++ value should be used instead.
Function f A C++ function f is created. Unlike External Function, both the function’s header and implementation are created.
Struct

A C++ struct that contains:

  • For each field of the Coco struct, a corresponding field;
  • For each member function, a corresponding member function.
Type Alias If @CPP.mapToType is given then nothing is generated and instead the given type is used. Otherwise, a C++ using declaration is created.

Ports

For each Coco p Port P, three classes are created:

  • P that contains:
    • All nested type declarations that P contains.
  • PProvided that contains:
    • For each function interface declaration, a corresponding pure virtual function.
    • A function subscribe that takes a PRequired, and will register the given object to receive all signals emitted by this port.
    • A function unsubscribe that takes a PRequired, and will unregister the given class from receiving signals emitted by this port.
    • If this port has an outgoing signal, a declaration called Signal that represents one of the outgoing signals. This is generated as though it was an enum declaration.
  • PRequired that contains:
    • For each outgoing signal declaration, a corresponding virtual function with an empty body. Subclasses should override this if they are interested in handling the signal.
    • A virtual function connect that takes PProvided and unless should_subscribe is false, will call subscribe on the provided port to register this object to receive all signals from the given provided port. Subclasses of PRequired will typically store the provided port passed to this method in order to make function calls over the provided port in the future.
    • If the port has at least one outgoing signal, a function send that takes a value of type PProvided::Signal and calls the appropriate function on this object.

For example, given:

port P {
  function fn(x : Bool) : Nil
  outgoing signal sig(x : Bool)
  machine M { ... }
}

The generated C++ code contains:

class P {
public:
  struct Signal {
    struct sig_ {
      bool operator==(const sig_ &right) const { return x == right.x; }
      bool operator!=(const sig_ &right) const { return x != right.x; }

      bool x;
    };

    using type = coco::variant<sig_>;

    static type sig(bool x) { return sig_{x}; }
  };
};

class PProvided : public P, public coco::BaseProvidedPort {
public:
  virtual void fn(bool x) = 0;
  virtual void subscribe(PRequired &component) = 0;
  virtual void unsubscribe(PRequired &component) = 0;
};

class PRequired : public P, public coco::BasePort {
public:
  virtual void sig(bool x) {}
  virtual void connect(PProvided &component, bool should_subscribe = true);
  void send(const PProvided::Signal::type &signal);
};

Components

In Coco, there are three different types of components:

  • Implementation components which contain an explicit state machine defined in Coco;
  • Encapsulating components which contain no state machine, but instead contain instances of other components and connect them appropriately;
  • External components which contain no Coco-defined state machine, but instead correspond to code that will be written by hand.

Whilst the generated code is slightly different for each of these (particularly for external components, since they are user-defined), there is also a lot of commonality between them.

For all three types of components, each Coco declaration C will result in a C++ class called C being generated that:

  • Inherits from BaseComponent or one of its subclasses, if more applicable.
  • For each provided port p of the component (i.e. for each field of the component that is of type Provided<P>), a function p that returns a PProvided. This can be used to make function calls on this component via the port P, or to register to receiver signals from P.
  • For each provided port r of the component (i.e. for each field of the component that is of type Required<P>), a function r that returns a PRequired. This has to be called prior to starting this state machine, and will be used by this component to register to receive signals, and to make function calls.
  • A function called coco_start that starts the state machine, and overrides the corresponding method of coco::BaseComponent::coco_start(). This function must:
    • Not be called until all of the provided and required ports have been connected.
    • Be called before calling any of the functions on any of the provided ports.

Since all components inherit from coco::BaseComponent, there are various utility methods available. For example, coco::BaseComponent::set_logger_recursively() can be used to attacher a logger to all state machines that are children of a given state machine. Providing that the generated C++ code was generated with generator.cpp.alwaysEnableLogging, or the generated code is compiled with COCO_LOGGING_ENABLE defined (e.g. by passing -DCOCO_LOGGING_ENABLE as a compiler flag), this will cause all lifecycle events, state states, and transitions to be logged. For example:

c.set_logger_recursively(coco::StreamLogger::cout());

will cause Coco to log to stdout.

The lifecycle of a Coco component C has three phases:

Idle

After construction (in C++ terms), the Coco component is in an inactive state. Before moving to the next phase, all provided or required ports must be connected using the connect and subscribe methods on the ports. Once this has been done, coco::BaseComponent::coco_start() must be called. This causes all state machines inside C to be started, and all state machines that are required by C as well. For example:

Note coco_start is a synchronous function, and it will only return when all state machines have entered their initial states. Therefore, as soon as this function returns it is safe to make function calls on a provided port of C. Note that if an object has been added as a subscriber to C’s signals, then it is possible that it will receive a signal before coco_start has returned if the entry() functions send signals.

Running

After coco_start has returned, the state machine is then in the Running state and will process any function calls or signals that it receives. The state machine will then stay in the running state until it is considered to be Terminated, which depends on what sort of component it is:

  • An implementation component is considered to be terminated only when it has explicitly transitioned to the Terminated state, e.g. via setNextState(Terminated).
  • An encapsulating component is considered to be terminated when all state machines it owns have terminated.
  • An external component is considered to be terminated when it calls the coco_shutdown function.

It is considered an error to call the component’s destructor whilst the component is in this state.

Warning

In the multi-threaded runtime, calling the C++ destructor of a component without first causing the state machine to transition to the Terminated state will cause the C++ code to appear to deadlock, waiting for the component to shutdown.

Terminated
In this state, the component has finished and is ready to be destructed. Note that it is not possible to restart a terminated state machine.

Typically, a top-level Coco component is used like the following:

Comp c;
// c is now in the Idle state
// Connect each required port
c.required().connect(...);
// For each provided port, subscribe to interesting signals
c.client().subscribe(...);
// Optionally, set a component's name and a logger
c.set_name("root");
c.set_logger_recursively(...);
c.coco_start();

// c is now in the Running state, so call functions on it
c.client().fn();
...
// Call a function to ask c to terminate
c.client().terminate();

After the above code has completed, and assuming that the function terminate() ensures that all state machines terminate before returning, then Comp can be safely destroyed.

Implementation Components

For each Implementation Component C, a C++ class C is created that subclasses either BaseComponent or BaseSingleThreadedComponent depending on what runtime the component uses. Further, if the component has only one provided port P or only one required port P, then it will directly subclass the corresponding PProvided or PRequired class, respectively.

The public declarations of C will be:

  • As per a standard component, C will have functions defined on it for each provided or required port.
  • A default constructor that can be used to construct the component, but not start it.
  • If the component is subclassing either PProvided or PRequired, then it will override appropriate methods in order to implement the respective port.

Implementation components can be utilised as described in component.

External Components

Warning

When implementing external components, it is the responsibility of the user to ensure that the component correctly respects both the specifications of the ports, and of the Coco runtime. Failure to do this will result in undefined behaviour at runtime.

For each External Component, a C++ class called C is created that contains pure virtual methods that correspond to the various functions or signals that the external component can receive. Users are expected to subclass C in order to implement these functions. See Encapsulating Components for an example of how to use external components with Coco-provided code.

The public declarations of C will be:

  • As per a standard component, C will have functions defined on it for each provided or required port.

The protected declarations of C will be:

  • A virtual method called execute_start() that will be called as part of the processing of coco::BaseComponent::coco_start(). This can be overriden by subclasses to provide additional behaviour on startup. This method will only be called once all required ports of C have entered into the Running state, and therefore the subclass may make function calls on the required port if it wants to.

  • A method called coco_shutdown() that must be called by the subclass when the implementation should shutdown.

  • For each function f on a provided port p of C, a pure virtual function p_f will be created. In the subclass, this must be overriden to provide the implementation of f.

  • For each provided port p of type P, a field called p_ will be created whose type is Coco-generated subclass of PProvided. This subclass will contain additional methods to easily send signals to all connected subscribers:

    • For each outgoing signal s on P, the Coco-generated subclass will contain a function called send_s. Subclasses can use this function to send the corresponding signal to all currently connected subscribers, for example via p_.send_s().
  • For each outgoing signal s on a required port r of C, a virtual function r_s will be created. In the subclass, this can be overriden to provide the implementation of s8.

    Warning

    When implementing r_s(), the implementation must not call any method on any required port of C, otherwise in the multi-threaded runtime a deadlock will be created, and in the single-threaded runtime, the state of the required port may be corrupted. Coco assumes that components receive signals without blocking. Therefore, it is safest to implement r_s() by putting the signal onto some sort of queue to be processed later.

    Note that this method may be called prior to execute_start() being called if a required port can send a signal on startup.

For example, consider the following Coco:

port P {
  function fn(x : Bool) : Nil
  outgoing signal sig(x : Bool)
  machine M {
    fn(x : Bool) = sig(x)
    @unbounded
    @unreliable
    spontaneous = sig(false)
  }
}

external component E {
  client : Provided<P>;
}

The generated C++ code for E is:

class E : public coco::BaseComponent {
 public:
  E();

  void coco_start() override;
  PProvided& client() { return client_; }

protected:
  class clientImpl final : public PProvided {
   public:
    ...
    void send_sig(bool x) { ... }
  };

  virtual void client_fn(bool x) = 0;
  virtual void execute_start() {}

  clientImpl client_;
};

In the above, note that the client_ field is of type clientImpl, which contains a method called send_sig that will send the signal to all currently connected subscribers. Further, note the method client_fn which must be implemented in order to define the behaviour of calling fn over the client port.

As an example, consider the following class EImpl, which provides a valid implementation of E that correctly implements the provided port P:

class EImpl : public E {
 protected:
  void client_fn(bool x) override {
    client_.send_sig(x);
  }
};

Note that this uses the client_ variable rather than the client() member function: this is to get access to the send_sig function, which is not part of the public interface of E.

See also

Testing for details on how to generate GMock-compatible mocks for testing the generated code without creating an implementation of an external component.

Multi-threaded External Components

When implementing external components for the multi-threaded runtime, care needs to be taken in order to ensure that the component is thread-safe since it may be called by many different threads. For external components with a provided port, Coco automatically generates a protected variable called mutex_ (of type std::mutex), which is used to ensure thread safety. The generated code automatically acquires the mutex before calling execute_start, and before calling any of the pure virtual functions used to implement function calls on the provided port. For example, in the implementation of client_fn in the example above, it is guaranteed that the mutex will have already been acquired. Handwritten code must acquire the mutex_ before calling any of the generated send_ variables.

For external components with required ports, no mutexes are automatically generated, and users are responsible for implementing their own locking, if they need to.

Single-threaded External Components

When implementing external components for the single-threaded runtime, some additional function calls are required to send signals. For example, suppose that the above external component E was a single-threaded component, and then consider the following:

class EImpl : public E {
 public:
  void external_stimulus() {
    client_.send_sig(x);
    send_drain_queue();
  }
 protected:
  void client_fn(bool x) override {
    client_.send_sig(x);
  }
};

In the above, the function external_stimulus() corresponds to the spontaneous contained in the port P that E provides. Note that not only does it call send_sig to send the signal, but it also calls coco::BaseSingleThreadedComponent::send_drain_queue() in order to cause the component above it to process the signal. This function will sequentially process the queue of all components who may have received the signal. Note that the drain queue mechanism does require care, and the Coco-generated code requires that handwritten code obeys the following rules:

Encapsulating Components

For each encapsulating component C, a C++ class called C is created that subclasses either coco::BaseMultiThreadedEncapsulatingComponent or coco::BaseSingleThreadedEncapsulatingComponent, as appropriate.

The public declarations of C will be:

  • As per a standard component, C will have functions defined on it for each provided or required port.

  • If C contains no external components, then a default constructor that can be used to construct the component, but not start it.

  • If C contains any external components, or encapsulating components that themselves contain external components, then a struct called Arguments will be created with a member for each external component that C contains. In addition, the constructor of C will take a single parameter of type Arguments, in order to provide C with the implementations of the external components that should be used. For example, consider:

    external component E {
      p : Provided<P>;
    }
    
    component C {
      e : E;
      ...
      init() = { ... }
    }
    

    The generated C++ code for this will contain:

    class C {
     public:
      struct Arguments {
        E* e{};
      };
    
      C(const Arguments& arguments);
      ...
    };
    

    This can then be used as follows:

    /// A user-defined subclass of E
    class EImpl : public E { ... };
    
    void execute() {
      EImpl e;
      C::Arguments c_arguments;
      c_arguments.e = &e;
      C c(c_arguments);
      ...
    }
    

    The above will cause EImpl to be connected by C according to C’s init method.