Message Definitions

Introduction

In practice, various fields need to be added to cMessage or cPacket to make them useful. For example, when modeling communication networks, message/packet objects need to carry protocol header fields. Since the simulation library is written in C++, the natural way to extend cMessage/cPacket is by subclassing them. However, at least three items have to be added to the new class for each field (a private data member, a getter, and a setter method), and the resulting class needs to integrate with the simulation framework. This means that writing the necessary C++ code can be a tedious and time-consuming task.

OMNeT++ offers a more convenient way called message definitions. Message definitions provide a compact syntax to describe message contents, and the corresponding C++ code is automatically generated from the definitions. When needed, the generated class can also be customized via subclassing. Even when the generated class needs to be heavily customized, message definitions can still save the programmer a great deal of manual work.

The First Message Class

Let us begin with a simple example. Suppose we need a packet type that carries a source and a destination address as well as a hop count. The corresponding C++ code can be generated from the following definition in a MyPacket.msg file:

packet MyPacket
{
     int srcAddress;
     int destAddress;
     int remainingHops = 32;
};

It is the task of the OMNeT++ message compiler, opp_msgc or opp_msgtool, to translate the definition into a C++ class that can be instantiated from C++ model code. The message compiler is normally invoked for .msg files automatically, as part of the build process.

When the message compiler processes MyPacket.msg, it creates two files: MyPacket_m.h and MyPacket_m.cc. The generated MyPacket_m.h will contain the following class declaration (abbreviated):

class MyPacket : public cPacket {
  protected:
    int srcAddress;
    int destAddress;
    int remainingHops = 32;
  public:
    MyPacket(const char *name=nullptr, short kind=0);
    MyPacket(const MyPacket& other);
    MyPacket& operator=(const MyPacket& other);
    virtual MyPacket *dup() const override {return new MyPacket(*this);}
    ...

    // field getter/setter methods
    virtual int getSrcAddress() const;
    virtual void setSrcAddress(int srcAddress);
    virtual int getDestAddress() const;
    virtual void setDestAddress(int destAddress);
    virtual int getRemainingHops() const;
    virtual void setRemainingHops(int remainingHops);
};

As you can see, for each field, the generated class contains a protected data member, and a public getter and setter method. The names of the methods will begin with get and set, followed by the field name with its first letter converted to uppercase.

The MyPacket_m.cc file contains the implementation of the generated MyPacket class as well as “reflection” code (see cClassDescriptor) that allows inspection of these data structures under graphical user interfaces like Qtenv. The MyPacket_m.cc file should be compiled and linked into the simulation; this is normally taken care of automatically.

To use the MyPacket class from a C++ source file, the generated header file needs to be included:

#include "MyPacket_m.h"

...
MyPacket *pkt = new MyPacket("pkt");
pkt->setSrcAddress(localAddr);
...

Ingredients of Message Files

Message files contain the following components:

  • Packet, message, and class definitions are translated into C++ class definitions. The three types are very similar; they practically only differ in the choice of the default base class (cPacket, cMessage, and no base class, respectively).

  • Struct definitions are translated into C-like structs, where fields are represented with public data members (there are no getters and setters).

  • Enum definitions are translated into C++ enums.

  • Namespace declarations define the namespace for subsequent definitions.

  • Imports allow the reuse of definitions from other .msg files.

  • Properties are metadata annotations of the syntax @name or @name(...) that may occur at the file, class (packet, struct, etc.) definition, and field level as well. There are many predefined properties, and a large subset of them deals with the details of what C++ code to generate for the item they occur with. For example, @getter(getFoo) on a field requests that the generated getter function have the name getFoo.

  • C++ blocks are used for injecting literal C++ code fragments into the generated source files. The target (the place where to insert the code) can be specified.

The following sections describe all of the above elements in detail.

Classes, Messages, Packets, Structs

As shown above, the message description language allows you to generate C++ data classes and structs from concise descriptions that have a syntax resembling C structs. The descriptions contain the choice of the base class (message descriptions only support single inheritance), the list of fields the class should have, and possibly various metadata annotations that control the details of the code generation.

A description starts with one of the packet, message, class, struct keywords. The first three are very similar: they all generate C++ classes, and only differ on the choice of the default base class (and related details such as the argument list of the constructor). The fourth one generates a plain (C-style) struct.

Classes, Messages, Packets

For packet, the default base class is cPacket; or if a base class is explicitly named, it must be a subclass of cPacket. Similarly, for message, the default base class is cMessage, or if a base class is specified, it must be a subclass of cMessage.

For class, the default base class is none. However, it is often a good idea to choose cObject as the base class. [1]

Note

It is recommended to use cObject as the base class because it adds zero overhead to the generated class and, at the same time, makes the class more interoperable with the rest of the simulation library. cObject only defines virtual methods but no data members, so the only overhead would be the vptr; however, the generated class already has a vptr because the generated methods are also virtual.

The base class is specified with the extends keyword. For example:

packet FooPacket extends PacketBase
{
    ...
};

The generated C++ class will look like this:

class FooPacket : public PacketBase {
    ...
};

The generated class will have a constructor and also a copy constructor. An assignment operator (operator=()) and a cloning method (dup()) will also be generated.

The argument list of the generated constructor depends on the base class. For classes derived from cMessage, it will accept an object name and message kind. For classes derived from cNamedObject, it will accept an object name. The arguments are optional (they have default values).

class FooPacket : public PacketBase
{
  public:
    FooPacket(const char *name=nullptr, int kind=0);
    FooPacket(const FooPacket& other);
    FooPacket& operator=(const FooPacket& other);
    virtual FooPacket *dup() const;
    ...

Additional base classes can be added by listing them in the @implements class property.

Structs

Message definitions allow you to define C-style structs, where “C-style” means “containing only data and no methods”. These structs can be useful as fields in message classes.

The syntax is similar to that of defining messages:

struct Place
{
    int type;
    string description;
    double coords[3];
};

The generated struct has public data members and no getter or setter methods. The following code is generated from the above definition:

// generated C++
struct Place
{
    int type;
    omnetpp::opp_string description;
    double coords[3];
};

Note that string fields are generated with the opp_string C++ type, which is a minimalistic string class that wraps const char* and takes care of allocation/deallocation. It was chosen instead of std::string because of its significantly smaller memory footprint. (std::string is significantly larger than a const char* pointer because it also needs to store length and capacity information in some form.)

Inheritance is supported for structs:

struct Base
{
    ...
};

struct Extended extends Base
{
    ...
};

However, because a struct has no member functions, there are limitations:

  • Variable-size arrays are not supported;

  • Customization via inheritance and abstract fields (see later in Customizing the Class via Inheritance) cannot be used;

  • Classes subclassed from cOwnedObject cannot be used as fields in structs because structs cannot be owners.

Enums

An enum is declared with the enum keyword, using the following syntax:

enum PayloadType
{
   NONE = 0;
   VOICE = 1;
   VIDEO = 2;
   DATA = 3;
};

Enum values must be unique.

The message compiler translates an enum into a normal C++ enum, and also generates a descriptor that stores the symbolic names as strings. The latter makes it possible for Qtenv to display symbolic names for enum values.

Enums can be used in two ways. The first is simply to use the enum’s name as field type:

packet FooPacket
{
    PayloadType payloadType;
};

The second way is to tag a field of the type int or any other integral type with the @enum property and the name of the enum, like this:

packet FooPacket
{
    int16_t payloadType @enum(PayloadType);
};

In the generated C++ code, the field will have the original type (in this case, int16_t). However, additional code generated by the message compiler will allow Qtenv to display the symbolic name of the field’s value in addition to the numeric value.

Imports

Import directives are used to make definitions in one message file available to another one. Importing an MSG file makes the definitions in that file available to the file that imports it, but has no further side effect (and in particular, it will generate no C++ code).

To import a message file, use the import keyword followed by a name that identifies the message file within its project:

import inet.linklayer.common.MacAddress;

The imported name is interpreted as a relative file path (by replacing dots with slashes, and appending .msg), which is searched for in folders listed in the message import path, much like C/C++ include files are searched for in the compiler’s include path, Python modules in the Python module search path, or NED files in the NED path.

The message import path can be specified to the message compiler via a series of -I command-line options.

Namespaces

To place generated types into a namespace, add a namespace directive above the types in question:

namespace inet;

Hierarchical (nested) namespaces are declared using double colons in the namespace definition, similar to nested namespace definitions introduced in C++ in version C++17.

namespace inet::ieee80211;

The above code will be translated into multiple nested namespaces in the C++ code:

namespace inet { namespace ieee80211 {
...
}}

There can be multiple namespace directives in a message file. The effect of the namespace directive extends from the place of the directive until the next namespace directive or the end of the message file. Each namespace directive opens a completely new namespace, i.e. not a namespace within the previous one. An empty namespace directive (namespace;) returns to the global namespace. For example:

namespace foo::bar;
class A {}  // defines foo::bar::A

namespace baz;
class B {}  // defines baz::B

namespace;
class C {}  // defines ::C

Properties

Properties are metadata annotations of the syntax @name or @name(...) that may occur on file, class (packet, struct, etc.) definitions, and field levels. There are many predefined properties, and a large subset of them deals with the details of what C++ code to generate for the item they occur with. For example, @getter(getFoo) on a field requests that the generated getter function has the name getFoo.

Here is a syntax example. Note that class properties are placed in the fields list (fields and properties may be mixed in an arbitrary order), and field properties are written after the field name.

@foo;
class Foo {
  @customize(true);
  string value @getter(...) @setter(...) @hint("...");
}

Syntactically, the mandatory part of a property is the @ character followed by the property name. They are then optionally followed by an index and a parameter list. The index is a name in square brackets, and it is rarely used. The parameter list is enclosed in parentheses, and in theory, it may contain a value list and key-value list pairs, but almost all properties expect to find just a single value there.

For boolean properties, the value may be true or false; if the value is missing, true is assumed. Thus, @customize is equivalent to @customize(true).

As a guard against mistyping property names, properties need to be declared before they can be used. Properties are declared using the @property property, with the name of the new property in the index, and the type and other attributes of the property in the parameter list. Examples for property declarations, including the declaration of @property itself, can be seen by listing the built-in definitions of the message compiler (opp_msgtool -h builtindefs).

The complete list of properties understood by the message compiler and other OMNeT++ tools can be found in Appendix Appendix: Message Class/Field Properties.

Data Types

The following data types can be used for fields:

  • C/C++ primitive data types: bool, char, short, int, long, unsigned char, unsigned short, unsigned int, , float, double.

  • string. Getters and setters use the const char* data type; nullptr is not allowed. Setters store a copy of the string, not just the pointer.

  • C99-style fixed-size integer types: int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t. [2]

In addition, OMNeT++ class names such as simtime_t and cMessage are also made available without the need to import anything. These names are accepted both with and without spelling out the omnetpp namespace name.

Numeric fields are initialized to zero, booleans to false, and string fields to the empty string.

Fields

Scalar fields

A scalar field is one that holds a single value. It is defined by specifying the data type and the field name, for example:

int timeToLive;

For each field, the generated class will have a protected data member, and a public getter and setter method. The names of the methods will begin with get and set, followed by the field name with its first letter converted to uppercase. Thus, the above field will generate the following methods in the C++ class:

int getTimeToLive() const;
void setTimeToLive(int timeToLive);

Note

All methods are generated to be virtual, but we omit the virtual keyword here and in further examples.

The method names are derived from the field name, but they can be customized with the @getter and @setter properties, as shown below:

int timeToLive @getter(getTTL) @setter(setTTL);

The choice of C++ type used for the data member and the getter/setter methods can be overridden with the help of the @cppType property (and on a more fine-grained level, with @datamemberType, @argType and @returnType), although this is rarely useful.

Initial Values

Initial values for fields can be specified after an equal sign, like so:

int version = HTTP_VERSION;
string method = "GET";
string resource = "/";
bool keepAlive = true;
int timeout = 5*60;

Any phrase that is a valid C++ expression can be used as an initializer value. (The message compiler does not check the syntax of the values, it merely copies them into the generated C++ file.)

For array fields, the initializer specifies the value for individual array elements. There is no syntax for initializing an array with a list of values.

Overriding Initial Values from Subclasses

In a subclass, it is possible to override the initial value of an inherited field. The syntax is similar to that of a field definition with an initial value, only the data type is missing.

An example:

packet Ieee80211Frame
{
    int frameType;
    ...
};

packet Ieee80211DataFrame extends Ieee80211Frame
{
    frameType = DATA_FRAME;  // assignment of the inherited field
    ...
};

It may seem like the message compiler would need the definition of the base class to check the definition of the field being assigned. However, this is not the case. The message compiler trusts that such a field exists; or rather, it leaves the check to the C++ compiler.

What the message compiler actually does is derive a setter method name from the field name and generate a call to it into the constructor. Thus, the generated constructor for the above packet type would be something like this:

Ieee80211DataFrame::Ieee80211DataFrame(const char *name, int kind) :
    Ieee80211Frame(name, kind)
{
    this->setFrameType(DATA_FRAME);
    ...
}

This implementation also lets one initialize cMessage /cPacket fields such as message kind or packet length:

packet UDPPacket
{
    byteLength = 16;  // results in 'setByteLength(16);' being placed into ctor
};

Const Fields

A field can be marked as const by using the const keyword. A const field only has a (const) data member and a getter function, but no setter. The value can be provided via an initializer. An example:

const int foo = 24;

This generates a const int data member in the class, initialized to 24, and a getter member function that returns its value:

int getFoo() const;

Array fields cannot be const.

Note that a pointer field may also be marked const, but const is interpreted differently in that case: as a mutable field that holds a pointer to a const object.

One use of const is to implement computed fields. For that, the field needs to be annotated with the @custom or @customImpl property to allow for a custom implementation to be supplied for the getter. The custom getter can then encapsulate the computation of the field value. Customization is covered in section Customizing the Generated Class.

Note

To add actual constants (as opposed to getter-only fields) to a class, it is better to use a targeted cplusplus block to inject their definitions into the C++ class declaration.

Abstract Fields

Abstract fields are a way to allow custom implementation (such as storage, getter/setter methods, etc.) to be provided for a field. For a field marked as abstract, the message compiler does not generate a data member, and generated getter/setter methods will be pure virtual. It is expected that the pure virtual methods will be implemented in a subclass (possibly via @customize, see Customizing the Generated Class).

A field is declared abstract by using the abstract keyword or the @abstract property (the two are equivalent).

abstract bool urgentBit; // or: bool urgentBit @abstract;

The generated pure virtual methods:

virtual bool getUrgentBit() const = 0;
virtual void setUrgentBit(bool urgentBit) = 0;

Alternatives to abstract, at least for certain use cases, are @custom and @customImpl (see section Customizing the Generated Class).

Fixed-Size Arrays

Fixed-size arrays can be declared with the usual syntax of putting the array size in square brackets after the field name:

int route[4];

The generated getter and setter methods will have an extra k argument (the array index), and a third method that returns the array size is also generated:

int getRoute(size_t k) const;
void setRoute(size_t k, int route);
size_t getRouteArraySize() const;

When the getter or setter method is called with an index that is out of bounds, an exception is thrown.

The method names can be overridden with the @getter, @setter, and @sizeGetter properties. To use another C++ type for array size and indices instead of the default size_t, specify the @sizeType property.

Note

Use a singular noun for the field name instead of a plural noun (route[] instead of routes[]), otherwise, method names will look confusing (getRoutes(), appendRoutes(), etc., for methods that deal with a single route).

When a default value is given, it is interpreted as a scalar for filling the array with. There is no syntax for initializing an array with a list of values.

int route[4] = -1; // all elements set to -1

Variable-Size Arrays

If the array size is not known in advance, the field can be declared to have a variable size by using an empty pair in brackets:

int route[];

In this case, the generated class will have extra methods in addition to the getter and setter: one for resizing the array, one for getting the array size, plus methods for inserting an element at a given position, appending an element, and erasing an element at a given position.

int getRoute(size_t k) const;
void setRoute(size_t k, int route);
void setRouteArraySize(size_t size);
size_t getRouteArraySize() const;
void insertRoute(size_t k, int route);
void appendRoute(int route);
void eraseRoute(size_t k);

The default array size is zero. Elements can be added by calling the inserter or the appender method or resizing the array and setting individual elements.

Internally, all methods that change the array size (inserter, appender, resizer) always allocate a new array and copy existing values over to the new array. Therefore, when adding a large number of elements, it is recommended to resize the array first instead of calling the appender method multiple times.

The method names can be overridden with the @getter, @setter, @sizeGetter, @sizeSetter, @inserter, @appender, and @eraser field properties. To use another C++ type for array size and indices instead of the default size_t, specify the @sizeType property.

When a default value is given, it is used for initializing new elements when the array is expanded.

int route[] = -1;

Classes and Structs as Fields

Classes and structs may also be used as fields, not only primitive types and string. For example, given a class named IPAddress, one can write the following field:

IPAddress sourceAddress;

The IPAddress type must be known to the message compiler.

The generated class will contain an IPAddress data member, and the following member functions:

const IPAddress& getSourceAddress() const;
void setSourceAddress(const IPAddress& sourceAddress);
IPAddress& getSourceAddressForUpdate();

Note that in addition to the getter and setter, a mutable getter (get...ForUpdate) is also generated, which allows the stored value (object or struct) to be modified in place.

By default, values are passed by reference. This can be changed by specifying the @byValue property:

IPAddress sourceAddress @byValue;

This generates the following member functions:

virtual IPAddress getSourceAddress() const;
virtual void setSourceAddress(IPAddress sourceAddress);

Note that both member functions use pass-by-value, and that the mutable getter function is not generated.

Specifying const will cause only a getter function to be generated but no setter or mutable getter, as shown before in Const Fields.

Array fields are treated similarly, the difference being that the getter and setter methods take an extra index argument:

IPAddress route[];

The generated methods:

void setRouteArraySize(size_t size);
size_t getRouteArraySize() const;
const IPAddress& getRoute(size_t k) const;
IPAddress& getRouteForUpdate(size_t k);
void setRoute(size_t k, const IPAddress& route);
void insertRoute(size_t k, const IPAddress& route);
void appendRoute(const IPAddress& route);
void eraseRoute(size_t k);

Non-Owning Pointer Fields

The field type may be a pointer, both for scalar and array fields. Pointer fields come in two flavors: owning and non-owning. A non-owning pointer field just stores the pointer value regardless of the ownership of the object it points to, while an owning pointer holds the ownership of the object. This section discusses non-owning pointer fields.

Example:

cModule *contextModule;  // missing @owner: non-owning pointer field

The generated methods:

const cModule *getContextModule() const;
void setContextModule(cModule *contextModule);
cModule *getContextModuleForUpdate();

If the field is marked const, then the setter will take a const pointer, and the getForUpdate() method is not generated:

const cModule *contextModule;

The generated methods:

const cModule *getContextModule() const;
void setContextModule(const cModule *contextModule);

Owning Pointer Fields

This section discusses pointer fields that own the objects they point to, that is, are responsible for deallocating the object when the object containing the field (let’s refer to it as the “container” object) is deleted.

For all owning pointer fields in a class, the destructor of the class deletes the owned objects, the dup() method and the copy constructor duplicate the owned objects for the newly created object, and the assignment operator (operator=) does both: the old objects in the destination object are deleted, and replaced by clones of the objects in the source object.

When the owned object is a subclass of cOwnedObject that keeps track of its owner, the code generated for the container class invokes the take() and drop() methods at the appropriate times to manage the ownership.

Example:

cPacket *payload @owned;

The generated methods:

const cPacket *getPayload() const;
cPacket *getPayloadForUpdate();
void setPayload(cPacket *payload);
cPacket *removePayload();

The getter and mutable getter return the stored pointer (or nullptr if there is none).

The remover method releases the ownership of the stored object, sets the field to nullptr, and returns the object.

The setter method behavior depends on the presence of the @allowReplace property. By default (when @allowReplace is absent), the setter does not allow replacing the object. That is, when the setter is invoked on a field that already contains an object (the pointer is non-null), an error is raised: “A value is already set, remove it first with removePayload()”. One must call removePayload() before setting a new object.

When @allowReplace is specified for the field, there is no need to call the remover method before setting a new value because the setter method deletes the old object before storing the new one.

cPacket *payload @owned @allowReplace; // allow setter to delete the old object

If the field is marked const, then the getForUpdate() method is not generated, and the setter takes a const pointer.

const cPacket *payload @owned;

The generated methods:

const cPacket *getPayload() const;
void setPayload(const cPacket *payload);
cPacket *removePayload();

The name of the remover method (which is the only extra method compared to non-pointer fields) can be customized using the @remover property.

Literal C++ Blocks

It is possible to inject C++ code fragments directly into the generated code. This is done using the cplusplus keyword, optionally followed by a target in parentheses, and enclosing the code fragment in double curly braces.

The target specifies where to insert the code fragment in the generated header or implementation file; we will explain it in a moment.

The message compiler does not attempt to interpret the code fragment; instead, it simply copies it into the generated source file at the requested location. The code fragment should be formatted in a way that it does not contain a double closing curly brace (}}) as it would be interpreted as the end of the fragment block. [3]

cplusplus {{
#include "FooDefs.h"
#define SOME_CONSTANT 63
}}

The target can be h (the generated header file – this is the default), cc (the generated .cc file), the name of a type generated in the same message file (content is inserted in the declaration of the type, just before the closing curly brace), or a member function name of one such type.

cplusplus blocks with the target h are commonly used to insert #include directives, commonly used constants or macros (e.g., #defines), or, rarely, typedefs and other elements into the generated header. The fragments are pasted into the namespace which is open at that point. Note that includes should always be placed into a cplusplus(h) block above the first namespace declaration in the message file.

cplusplus blocks with the target cc allow you to insert code into the .cc file, for example, implementations of member functions. This is useful, for instance, with custom-implementation fields (@customImpl, see Custom-implementation Methods).

cplusplus blocks with a type name as the target allow you to insert new data members and member functions into the class. This is useful, for example, with custom fields (@custom, see Custom Fields).

To inject code into the implementation of a member function of a generated class, specify <classname>::<methodname> as the target. Supported methods include the constructor, copy constructor (use Foo& as the name), destructor, operator=, copy(), parsimPack(), parsimUnpack(), etc., and the per-field generated methods (setter, getter, etc.).

Using External C++ Types

The message compiler only allows the use of types it knows about for fields or base classes. If you want to use types that are not generated by the message compiler, you need to do the following:

  1. Let the message compiler know about the type; and

  2. Make sure its C++ declaration is available at compile time.

For the first step, you can use the @existingClass property. When a type (class or struct) is annotated with @existingClass, the message compiler remembers the definition but assumes that the class (or struct) already exists in the C++ code and does not generate it. (However, it will still generate a class descriptor, see section Descriptor Classes.)

Note

Support for C++-style type announcements is no longer part of the message definitions syntax; they were removed in OMNeT++ version 6.0.

The second step is achieved by adding a cplusplus block with an #include directive to the message file.

For example, suppose we have a hand-written ieee802::MACAddress class defined in MACAddress.h that we would like to use for fields in multiple message files. One way to make this possible is to add a MACAddress.msg file alongside the header with the following content:

// MACAddress.msg

cplusplus {{
#include "MACAddress.h"
}}

class ieee802::MACAddress  // a separate namespace decl would also work
{
    @existingClass;
    int8_t octet[6]; // assumes class has getOctet(k) and setOctet(k)
}

As exemplified above, for existing classes, it is possible to announce them with their namespace-qualified name; there is no need for a separate namespace line.

This message file can be imported into all other message files that need the MACAddress, for example, like this:

import MACAddress;

packet EthernetFrame {
    ieee802::MACAddress source;
    ieee802::MACAddress destination;
    ...
}

Customizing the Generated Class

There are several possibilities for customizing a generated class:

  • Using custom method names and custom field types

  • Using custom field types

  • Injecting code into existing member functions

  • Custom fields

  • Fields with custom-implementation methods

  • The Generation Gap pattern

  • Abstract fields

  • Special customizations, e.g. @str, @nopack, etc.

The following sections explore the aforementioned possibilities.

Customizing Method Names

The names and some other properties of generated methods can be influenced with metadata annotations (properties).

The following field properties exist for overriding method names: @getter, @setter, @getterForUpdate, @remover, @sizeGetter, @sizeSetter, @inserter, @appender and @eraser.

To override data types used by the data member and its accessor methods, use @cppType, @datamemberType, @argType, or @returnType.

To override the default size_t type used for array size and indices, use @sizeType.

Consider the following example:

packet IPPacket {
    int ttl @getter(getTTL) @setter(setTTL);
    Option options[] @sizeGetter(getNumOptions)
                     @sizeSetter(setNumOptions)
                     @sizetype(short);
}

The generated class would have the following methods (note the differences from the default names getTtl(), setTtl(), getOptions(), setOptions(), getOptionsArraySize(), getOptionsArraySize(); also note that indices and array sizes are now short):

virtual int getTTL() const;
virtual void setTTL(int ttl);
virtual const Option& getOption(short k) const;
virtual void setOption(short k, const Option& option);
virtual short getNumOptions() const;
virtual void setNumOptions(short n);

In some older simulation models, you may also see the use of the @omitGetVerb class property. This property tells the message compiler to generate getter methods without the “get” prefix, e.g. for a sourceAddress field it would generate a sourceAddress() method instead of the default getSourceAddress(). It is not recommended to use @omitGetVerb in new models because it is inconsistent with the accepted naming convention.

Injecting Code into Methods

Generally, literal C++ blocks (the cplusplus keyword) are the way to inject code into the body of individual methods, as described in Literal C++ Blocks.

The @beforeChange class property can be used to designate a member function that is to be called before any mutator code (in setters, non-const getters, assignment operator, etc.) executes. This can be used to implement, for example, a dirty flag or some form of immutability (i.e. freeze the state of the object).

Generating str()

The @str class property aims to simplify adding an str() method in the generated class. Having an str() method is often useful for debugging, and it also has a special role in class descriptors (see toString/fromString).

When @str is present, an std::string str() const method is generated for the class. The method’s implementation will contain a single return keyword, with the value of the @str property copied after it.

Example:

class Location {
    double lat;
    double lon;
    @str("(" + std::to_string(getLat()) + "," + std::to_string(getLon()) + ")");
}

It will result in the following str() method to be generated as part of the Location class:

std::string Location::str() const
{
    return "(" + std::to_string(getLat()) + "," + std::to_string(getLon()) + ")";
}

Custom-implementation Methods

When member functions generated for a field need customized implementation and method-targeted C++ blocks are not sufficient, the customImpl property can be of help. When a field is marked customImpl, the message compiler will skip generating the implementations of its accessor methods in the .cc file, allowing the user to supply their own versions.

Here is a simple example. The methods in it do not perform anything extra compared to the default generated versions, but they illustrate the principle.

class Packet
{
    int hopCount @customImpl;
}

cplusplus(cc) {{
int Packet::getHopCount() const
{
    return hopCount; // replace/extend with extra code
}

void Packet::setHopCount(int value)
{
    hopCount = value; // replace/extend with extra code
}
}}

Custom Fields

If a field is marked with @custom, the field will only appear in the class descriptor, but no code is generated for it at all. One can inject the code that implements the field (data member, getter, setter, etc.) via targeted cplusplus blocks (Literal C++ Blocks). @custom is a good way to go when you want the field to have a different underlying storage or different accessor methods than normally generated by the message compiler. (For the latter case, however, be aware that the generated class descriptor assumes the presence of certain accessor methods for the field, although the set of expected methods can be customized to a degree. See Descriptor Classes for details.)

The following example uses @custom to implement a field that acts as a stack (has push() and pop() methods), and uses std::vector as the underlying data structure.

cplusplus {{
#include <vector>
}}

class MPLSHeader
{
    int32_t label[] @custom @sizeGetter(getNumLabels) @sizeSetter(setNumLabels);
}

cplusplus(MPLSHeader) {{
  protected:
    std::vector<int32_t> labels;
  public:
    // expected methods:
    virtual void setNumLabels(size_t size) {labels.resize(size);}
    virtual size_t getNumLabels() const {return labels.size();}
    virtual int32_t getLabel(size_t k) const {return labels.at(k);}
    virtual void setLabel(size_t k, int32_t label) {labels.at(k) = label;}
    // new methods:
    virtual void pushLabel(int32_t label) {labels.push_back(label);}
    virtual int32_t popLabel() {auto l=labels.back();labels.pop_back();return l;}
}}

cplusplus(MPLSHeader::copy) {{
    labels = other.labels;
}}

The last C++ block is needed so that the copy constructor and the operator= method also copy the new field. (copy() is a member function where the common part of the above two are factored out, and the C++ block injects code in there.)

Customizing the Class via Inheritance

Another way of customizing the generated code is by employing what is known as the Generation Gap design pattern, proposed by John Vlissides. The idea is that the customization can be done while subclassing the generated class, overriding whichever member functions need to be different from their generated versions.

This feature is enabled by adding the @customize property to the class. Doing so will cause the message compiler to generate an intermediate class instead of the final one, and the user will subclass the intermediate class to obtain the real class. The name of the intermediate class is obtained by appending _Base to the class name. The subclassing code can be in an entirely different header and .cc file from the generated one, so this method does not require the use of cplusplus blocks.

Consider the following example:

packet FooPacket
{
   @customize(true);
   ...
};

The message compiler will generate a FooPacket_Base class instead of FooPacket. It is then the user’s task to subclass FooPacket_Base to derive FooPacket, while adding extra data members and adding/overriding methods to achieve the goals that motivated the customization.

There is a minimum amount of code you have to write for FooPacket, because not everything can be pre-generated as part of FooPacket_Base (e.g. constructors cannot be inherited). This minimum code, which usually goes into a header file, is the following:

class FooPacket : public FooPacket_Base
{
  private:
    void copy(const FooPacket& other) { ... }
  public:
    FooPacket(const char *s=nullptr, short kind=0) : FooPacket_Base(s,kind) {}
    FooPacket(const FooPacket& other) : FooPacket_Base(other) {copy(other);}
    FooPacket& operator=(const FooPacket& other) {if (this==&other) return *this;
        FooPacket_Base::operator=(other); copy(other); return *this;}
    virtual FooPacket *dup() const override {return new FooPacket(*this);}
};

Note

The above boilerplate code can be copied out of the generated C++ header, which contains it as a comment.

The generated constructor, copy constructor, operator=, dup() can usually be copied verbatim. The only method that needs custom code is copy(). It is shared by the copy constructor and operator=, and should take care of copying the new data members you added as part of FooPacket.

In addition to the above, the implementation (.cc) file should contain the registration of the new class:

Register_Class(FooPacket);

Using an Abstract Field

Abstract fields, introduced in Abstract Fields, are an alternative to @custom (see Custom Fields) for allowing a custom implementation (such as storage, getter/setter methods, etc.) to be provided for a field. For a field marked abstract, the message compiler does not generate a data member, and generated getter/setter methods will be pure virtual.

Abstract fields are most often used together with the Generation Gap pattern (see Customizing the Class via Inheritance), so that one can immediately supply a custom implementation.

The following example demonstrates the use of abstract fields for creating an array field that uses std::vector as the underlying implementation:

packet FooPacket
{
    @customize(true);
    abstract int foo[]; // impl will use std::vector<int>
}

If you compile the above code, in the generated C++ code you will only find abstract methods for foo, but no underlying data member or method implementation. You can implement everything as you like. You can then write the following C++ file to implement foo with std::vector (some details omitted for brevity):

#include <vector>
#include "FooPacket_m.h"

class FooPacket : public FooPacket_Base
{
  protected:
    std::vector<int> foo;

  public:
    // constructor and other methods omitted, see below
    ...
    virtual int getFoo(size_t k) {return foo[k];}
    virtual void setFoo(size_t k, int x) {foo[k]=x;}
    virtual void addFoo(int x) {foo.push_back(x);}
    virtual void setFooArraySize(size_t size) {foo.resize(size);}
    virtual size_t getFooArraySize() const {return foo.size();}
};

Register_Class(FooPacket);

Some additional boilerplate code is needed so that the class conforms to conventions, and duplication and copying work properly:

FooPacket(const char *name=nullptr, int kind=0) : FooPacket_Base(name,kind) {
}
FooPacket(const FooPacket& other) : FooPacket_Base(other.getName()) {
    operator=(other);
}
FooPacket& operator=(const FooPacket& other) {
    if (&other==this) return *this;
    FooPacket_Base::operator=(other);
    foo = other.foo;
    return *this;
}
virtual FooPacket *dup() {
    return new FooPacket(*this);
}

Descriptor Classes

For each generated class and struct, the message compiler also generates an associated descriptor class. This class carries “reflection” information about the new class. The descriptor class encapsulates virtually all the information that the original message definition contains, and exposes it via member functions. Reflection information allows inspecting object contents down to the field level in Qtenv, filtering objects by a filter expression that refers to object fields, serializing messages-packets in a readable form for the eventlog file, and has several further potential uses.

cClassDescriptor

The descriptor class is subclassed from cClassDescriptor. It has methods for enumerating fields (getFieldCount(), getFieldName(), getFieldTypeString(), etc.), for getting and setting a field’s value in string form (getFieldAsString(), setFieldAsString()) and as cValue (getFieldValue(), setFieldValue()), for exploring the class hierarchy (getBaseClassDescriptor(), etc.), for accessing class and field properties, and for similar tasks.

Classes derived from cObject have a virtual member function getDescriptor() that returns their associated descriptor. For other classes, it is possible to obtain the descriptor using cClassDescriptor::getDescriptorFor() with the class name as the argument.

Several properties control the creation and details of the class descriptor.

Controlling Descriptor Generation

The @descriptor class property can be used to control the generation of the descriptor class. @descriptor(readonly) instructs the message compiler not to generate field setters for the descriptor, and @descriptor(false) instructs it not to generate a descriptor class for the class at all.

Generating Descriptors For Existing Classes

It is also possible to use (or abuse) the message compiler for generating a descriptor class for an existing class. To do that, write a message definition for your existing class (for example, if it has int getFoo() and setFoo(int) methods, add an int foo field to the message definition), and mark it with @existingClass. This will tell the message compiler that it should not generate an actual class (as it already exists), only a descriptor class.

Field Metadata

When an object is shown in Qtenv’s Object Inspector pane, Qtenv obtains all the information it displays from the object’s descriptor. There are several properties that can be used to customize how a field appears in the Object Inspector:

  • @icon associates an icon with the field;

  • @label overrides the text displayed as the field name;

  • @group is used for grouping related fields;

  • @hint can be used to provide a short description of the field, which Qtenv displays in a tooltip.

Method Name Properties

Several of the properties which are for overriding field accessor method names (@getter, @setter, @sizeGetter, @sizeSetter, etc., see Customizing Method Names) have a secondary purpose. When generating a descriptor for an existing class (see @existingClass), those properties specify how the descriptor can access the field, i.e. what code to generate in the implementation of the descriptor’s various methods. In that use case, such properties may contain code fragments or a function call template instead of a method name.

toString/fromString

To be able to generate the descriptor’s getFieldValueAsString() member function, the message compiler needs to know how to convert the return type of the getter to std::string. Similarly, for setFieldValueAsString() it needs to know how to convert (or parse) a string to obtain the setter’s argument type. For the built-in types (int, double, etc.) this information is pre-configured, but for other types, the user needs to supply it via two properties:

  • @toString specifies the code to convert the return type of the setter to a string;

  • @fromString specifies the code to convert a string to the setter’s argument type.

These properties can be specified on the class (where it will be applied to fields of that type), or directly on fields. Multiple syntaxes are accepted:

  • If the value starts with a dot, it is interpreted as a member function call.

  • If the value contains a dollar sign, it serves as a placeholder for the value to be converted.

Example:

class IPAddress
{
    @existingClass;
    @opaque;
    @toString(.str());  // use IPAddress::str() to produce a string
    @fromString(IPAddress($));  // use constructor; '$' will be replaced by the string
}

If the @toString property is missing, the message compiler generates code that calls the str() member function on the value returned by the getter, provided that it knows for certain that the corresponding type has such a method (the type is derived from cObject, or has the @str property).

If there is no @toString property and no (known) str() method, the descriptor will return the empty string.

toValue/fromValue

Similarly to @toString/@fromString described in the previous section, the @toValue and @fromValue properties are used to define how to convert the field’s value to and from cValue for the descriptor’s getFieldValue() and setFieldValue() methods.

Field Modifiers

There are several boolean-valued properties that enable/disable various features in the descriptor:

  • @opaque: If true, it treats the field as an atomic (non-compound) type, i.e., having no descriptor class. When specified on a class, it determines the default for fields of that type.

  • @editable: If set, the value of the field (or value of fields that are instances of this type) can be set via the class descriptor’s setFieldValueFromString() and setFieldValue() methods.

  • @replaceable: If set, the field is a pointer whose value can be set via the class descriptor’s setFieldStructValuePointer() and setFieldValue() methods.

  • @resizable: If set, the field is a variable-size array whose size can be set via the class descriptor’s setFieldArraySize() method.

  • @readonly: This is simply a shorthand for @editable(false) @replaceable(false) @resizable(false).