Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Papyrus-RT/Developer/Design/0.8/Codegen Code Pattern

PapyrusForRealTime-Logo-Icon.png


Structural Code Pattern



Introduction

This documents the code pattern that is realized in the PingPong-001 project in the CodePatterns repository. This code pattern was created with a focus on the structural elements of UML-RT models. The structural elements are the capsules, port, connectors, and protocols. It is specifically not the state machine or contained elements which are labelled the "behavioural" elements.

Overview

The PingPong-001 model is a simple Top-level capsule containing two Capsule roles. One role, the Pinger sends a ping message whenever it receives a pong message. It also sends a ping message as part of its initialization. The other role, the Ponger sends a pong message whenever it receives a ping. This is show in the following figure.

PapyrusRT-CodePatternStructure-01.png

The PingPongProtocol has one message with a single data item and one message with two data values. This is just to ensure that the code pattern is not limited to only one value.

The rest of this document describes the parts of the code pattern. For reference I've included parts of the code pattern directly within this document. However, the code pattern in the repository should be used as the master copy -- this document will likely become out of date when compared against that master copy.

Protocols and Ports

The code pattern for a protocol is designed to allow user-level action code to send signals with:

portName().signalName( data1, data2 ).send()

Signal functions

This is implemented by generating a Protocol class containing a function for each signal. In this example the protocol class would have a function called signalName, accepting the data1 and data2 parameters. The type of the parameters matches the type specified in the UML-RT model.

The return value of the class is an instance of UMLRTSignal. This is a class defined in the RTS which implements the #send function.

OutSignals and InSignals

Each protocol class defines two nested classes, one each for the outgoing and incoming signals. Each of these classes contains the corresponding signal functions (as described in the previous section). Each class also has a signal function for each bi-directional signal.

NOTE: NOTE: The section describing PortRoles has more detail on how these generated signal functions are used. For now it is sufficient to know that they are not directly invoked by user code.

The first parameter to the generated signal functions is a pointer to the port at the far end of the connection. The rest of the parameters are generated based on the signal's declared data.

The implementation of the function uses an RTS function to allocate an instance of UMLRTSignal using the far end port pointer that was passed. It then marshalls the data parameters into the UMLRTSignal instance. Finally, the signal is returned as a non-const reference.

Warning: How is the UMLRTSignal instance released?

The following code example shows the generated function for the ping signal in the PingPongProtocol. The function is generated within the OutSignals class which is generated within the PingPongProtocol class.

class PingPongProtocol
{
public:
    class OutSignals
    {
    public:
        UMLRTSignal & ping( UMLRTPort * farEnd, int param1, char param2 ) const
        {
            UMLRTSignal * signal
                = UMLRTSignal::allocate(
                    0 /*signalId*/,
                    UMLRTSignal::PRIORITY_NORMAL,
                    farEnd,
                    sizeof( param1 ) + sizeof( param2 ) );
            if( ! signal )
                return UMLRTSignal::invalid;

            uint8_t * payload = signal->getPayload();
            memcpy(payload, &param1, sizeof(param1));

            payload += sizeof(param1);
            memcpy(payload, &param2, sizeof(param2));

            return *signal;
        }
    };

    // ... 

The InSignals class is not shown because the pattern exactly follows the one for OutSignals; only the class name is different.

Base and Conjugate typedefs

The OutSignals and InSignals classes are not directly used by the generated code. For clarity we generate Base and Conjugate types which are typedefs to the OutSignals and InSignals classes.

    // ...

    typedef OutSignals Base;
    typedef InSignals Conjugate;
};

These typedefs are used when generating a port role that uses one of the protocols. There is more detail in the section on port roles.

NOTE: We should consider using a namespace instead of a class for the generated protocol.

Port Roles

Port roles exist only as a way to present a simple interface to user code. A simple example shows the reason. If the user wants to send signal out of port then we want the API to be:

port().signal().send();

The implementation problem is that #send needs to get the port instance from the return value of #port.

The solution is to have the #port function return an instance of a generated class that contains that pointer and also implements the protocol's interface.

In the PingPong-001 model this looks like:

class PingPongProtocol_baserole : protected UMLRTPortRole, private PingPongProtocol::Base { /* ... */ };

There is alot happening in this small class, I'll discuss each point in turn.

UMLRTPortRole as a base class

The UMLRTPortRole class is a POD and contains exactly one data member (a pointer to the far end port instance). This means that instances of this class can be returned by value exactly as efficiently as returning a normal pointer. There is no extra data to be passed, or C++ synthesized constructors and destructors to be invoked.

The base class is specified with protected inheritance which means that members of that class do not become public API in the derived class.

Generated Protocol role as a base class

The generated protocol class was created with no data members. This means it does not contribute any size to the derived class. Therefore it does not decrease the efficiency of returning by value.

The base class is specified with private inheritance which means that the generated signal functions are not exposed as public or private members of the derived class.

Summary

At this point there is a generated class that can be efficiently passed by value and that is able to invoke the generated signal functions. The class contains a pointer to the port at the far end of the connection (which is needed as the first parameter of the generated signal functions).

Generated functions

The final step is to generate signal functions into this port role class. This time the only function parameters are the data values that were specified for the signal. These functions will be directly invoked by the user code.

The implementation of these functions is a single line that invokes the previously generated signal functions. The contained port pointer is passes as the first argument and the data parameters are passed as the remaining arguments.

class PingPongProtocol_baserole : protected UMLRTPortRole, private PingPongProtocol::Base
{
public:
    PingPongProtocol_baserole( UMLRTPort * far ) : UMLRTPortRole( far ) { }
    UMLRTSignal & ping( int param1, char param2 ) const
    {
        return PingPongProtocol::Base::ping( farEnd, param1, param2 );
    }
};

The signal function implementation is specifically created to be as inline-friendly as possible.

Compiled result

The result is that the sample user code statement:

port().signal().send();

Will produce machine code to do the following:

  1. The inlined implementation of #port will put a pointer to the far end port instance onto the stack.
  2. The #signal function will directly invoke the Protocol's generated signal function which will allocate a signal and marshall data.
  3. The RTS function #send will take care of sending the allocated UMLRTSignal instance.

Capsules

There are two distinct realizations for Capsules:

  • From the user's perspective a Capsule is a fully-functional C++ class that contains the state machine. It also contains any extra attributes or operations that were specified in the model.
  • From the RTS's perspective a CapsuleInstance is a statically initialized data structure in a table that has been registered with the RTS. The area of most interest to the RTS are pointers to the port instances.

User's Perspective

The first category is realized by a generated class whose name exactly matches the Capsule name in the model. It contains variables and functions for attributes and operations that are specified in the model.

NOTE: Are capsules allowed to specify attributes?


NOTE: At this point there is no distinction for composite capsules, they simply follow the same pattern but have empty implementations for #initialize and #inject.


This class is derived from the RTS class UMLRTCapsuleType. The generator provides implementations for the #initialize and #inject virtual functions. The first is called by the RTS when initializing all capsules in the system. The second is called when a port instance receives a signal; it contains the generated state machine.

The generated class contains a pointer to the data structure that was created for the capsule instance. This pointer is passed to the classes constructor. Since the instances are statically allocated the pointer will be fully populated by the time the class constructor is invoked.

Port Roles

The generator creates a protected function for each port that is defined on the Capsule. The function returns (by-value) an instance of the class that was generated for the port role. The instance is created using a port instance that is accessed from the capsule instance that was provided to the constructor.

Here is the class that would be generated for the Pinger capsule in the PingPong-001 code pattern model.

class Pinger : public UMLRTCapsuleType
{
public:
    Pinger( UMLRTCapsuleInstance * ci ) : capsuleInstance( ci ) { } 

    // User code from the initial transition’s action code.
    virtual void initialize()
    {
        // BEGIN USER CODE
        /* ... */
        // END USER CODE
    }

    virtual void inject( UMLRTPort * recvPort, UMLRTSignal * signal )
    {
        /* demarshall data from signal */
        /* state machine implementation */
    }

protected:
    const PingPongProtocol_baserole pingPort()
    {
        return PingPongProtocol_baserole( capsuleInstance->ports[0].farEnd );
    }

private:
    UMLRTCapsuleInstance * capsuleInstance;
};

It would be possible to replace the generated port functions with variables. However, it seems a shame to waste space by duplicating storage of every port pointer.

RTS Perspective

The final step is to provide the port and capsule instances. A capsule instance is a pointer to the RTS class UMLRTCapsuleInstance. The capsule instance stores an array of instances of the RTS class UMLRTPort.

The generator references port and capsule instances as their index within an array. This removes initialization order dependencies.

The ports for a single capsule instance should be stored in a contiguous array. This is not a hard requirement, but it seems to difficult to reason about performance of any other data layout.

Capsule instances can be stored in one or more arrays. In the extreme the capsule instances are not required to be in an array, there could be a separate variable for each instance.

The sample in the code pattern shows a system where there is an array of capsule instances for each controller. This layout means that capsules that are managed by the same controller are close to each other in memory. The goal is to test a theory that this improves runtime performance by making more effective use of the cache. Annotated code pattern

The first step is to create forward declarations for all capsule instance arrays. The test model has two controllers and therefore two arrays:

extern UMLRTCapsuleInstance controller1_capsuleInstances[];
extern UMLRTCapsuleInstance controller2_capsuleInstances[];

Capsule instances (as specified by the RTS) point to their controller, so it is created next:

static UMLRTController controller1( "Controller1", controller1_capsuleInstances, 2 );

Capsule instances also point to their array of port instances, it is generated next:

static UMLRTPort Top_pinger_ports[]
= {
    {
        0, // will be a generated enumerator for the Pinger CapsuleType
        &controller1_capsuleInstances[1],
        &controller2_capsuleInstances[0].ports[0]
    }
};

The final data required by the capsule instance is the capsule type. This is the user-level class that was described in the "User's Perspective" section:

static Pinger top_pinger( &controller1_capsuleInstances[1] );

There is now enough data to create the array of capsule instances:

UMLRTCapsuleInstance controller1_capsuleInstances[]
= {
    {
        &top,
        &controller1,
        NULL,
        0
    },
    {
        &top_pinger,
        &controller1,
        Top_pinger_ports,
        1
    }
};

The same pattern is followed for other controllers.

Conclusion

This document describes the initial proposal for structural code pattern. The goal of documenting the pattern in this early state is to get feedback on various aspects of the design.

Copyright © Eclipse Foundation, Inc. All Rights Reserved.