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
.msgfiles.Properties are metadata annotations of the syntax
@nameor@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 namegetFoo.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
abstractfields (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 theconst char*data type;nullptris 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:
Let the message compiler know about the type; and
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:
@iconassociates an icon with the field;@labeloverrides the text displayed as the field name;@groupis used for grouping related fields;@hintcan 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:
@toStringspecifies the code to convert the return type of the setter to a string;@fromStringspecifies 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).