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:
- The C++ standard that the generated code should
correspond to
. - The file extensions that files should have
generator.cpp.headerFileExtension
andgenerator.cpp.implementationFileExtension
.
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
Additionally, if the enum satisfies the For example, given the Coco declaration: 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:
|
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.
- All nested type declarations that
PProvided
that contains:- For each
function interface
declaration, a corresponding pure virtual function. - A function
subscribe
that takes aPRequired
, and will register the given object to receive all signals emitted by this port. - A function
unsubscribe
that takes aPRequired
, 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.
- For each
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 takesPProvided
and unlessshould_subscribe
is false, will callsubscribe
on the provided port to register this object to receive all signals from the given provided port. Subclasses ofPRequired
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 typePProvided::Signal
and calls the appropriate function on this object.
- For each
For example, given:
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 typeProvided<P>
), a functionp
that returns aPProvided
. This can be used to make function calls on this component via the portP
, or to register to receiver signals fromP
. - For each provided port
r
of the component (i.e. for each field of the component that is of typeRequired<P>
), a functionr
that returns aPRequired
. 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 ofcoco::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
andsubscribe
methods on the ports. Once this has been done,coco::BaseComponent::coco_start()
must be called. This causes all state machines insideC
to be started, and all state machines that are required byC
as well. For example:- An implementation state machine will firstly call
coco::BasePort::coco_start()
on all of its required ports to ensure they are running. Then it will enter its initial state, firing anyentry functions
. - An encapsulating component will call
coco::BaseComponent::coco_start()
on all the components it owns, in some order.
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 ofC
. Note that if an object has been added as a subscriber toC
’s signals, then it is possible that it will receive a signal beforecoco_start
has returned if theentry()
functions send signals.- An implementation state machine will firstly call
- 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. viasetNextState(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.- An implementation component is considered to be terminated only when it has explicitly transitioned to the
- 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
orPRequired
, 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 ofcoco::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 ofC
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 portp
ofC
, a pure virtual functionp_f
will be created. In the subclass, this must be overriden to provide the implementation off
.For each provided port
p
of typeP
, a field calledp_
will be created whose type is Coco-generated subclass ofPProvided
. This subclass will contain additional methods to easily send signals to all connected subscribers:- For each outgoing signal
s
onP
, the Coco-generated subclass will contain a function calledsend_s
. Subclasses can use this function to send the corresponding signal to all currently connected subscribers, for example viap_.send_s()
.
- For each outgoing signal
For each outgoing signal
s
on a required portr
ofC
, a virtual functionr_s
will be created. In the subclass, this can be overriden to provide the implementation ofs8
.Warning
When implementing
r_s()
, the implementation must not call any method on any required port ofC
, 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 implementr_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:
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:
C++ functions that correspond to spontaneous functions must only call
coco::BaseSingleThreadedComponent::send_drain_queue()
once they have emitted all signals. For example, given a port containing:spontaneous = { sig(); sig(); }
then the corresponding C++ must be implemented as:
void spontaneous_impl() { send_sig(); send_sig(); send_drain_queue(); }
and, specifically,
coco::BaseSingleThreadedComponent::send_drain_queue()
must not be called after the firstsend_sig
.When a component calls
coco::BaseSingleThreadedComponent::send_drain_queue()
, it must be prepared to accept function calls from components above, since these components may call functions on the current component in response to processing the signals in their queues.execute_start()
must not callcoco::BaseSingleThreadedComponent::send_drain_queue()
.An implementation of a provided port function (e.g.
client_fn()
) must not callcoco::BaseSingleThreadedComponent::send_drain_queue()
.
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 calledArguments
will be created with a member for each external component thatC
contains. In addition, the constructor ofC
will take a single parameter of typeArguments
, in order to provideC
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 byC
according toC
’sinit
method.