[Prev] [Next] [TOC] [Chapters]

6 Message Definitions

6.1 Introduction

In practice, one needs to add various fields 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 of extending cMessage/cPacket is via subclassing them. However, at least three items has 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, which 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 offer 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.

6.1.1 The First Message Class

Let us begin with a simple example. Suppose that you need a packet class that carries source and destination addresses as well as a hop count. You may then write a MyPacket.msg file with the following contents:

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

It is the task of the message compiler to generate C++ classes that can be instantiated from C++ model code. The message compiler is normally invoked automatically for .msg files during build.

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

class MyPacket : public cPacket {
    ...
    virtual int getSrcAddress() const;
    virtual void setSrcAddress(int srcAddress);
    ...
};

In order 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);
...

The MyPacket_m.cc file will contain implementation of the generated MyPacket class as well as “reflection” code 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.

The following sections describe the message syntax and features in detail.

6.2 Messages and Packets

6.2.1 Defining Messages and Packets

Message and packet contents can be defined in a syntax resembling C structs. The keyword can be message or packet; they cause the generated C++ class to be derived from cMessage and cPacket, respectively. (Further keywords, class and struct, will be covered later.)

An example packet definition:

packet FooPacket
{
    int sourceAddress;
    int destAddress;
    bool hasPayload;
};

Saving the above code into a FooPacket.msg file and processing it with the message compiler, opp_msgc, will produce the files FooPacket_m.h and FooPacket_m.cc. The header file will contain the declaration of the generated C++ class.

The generated class will have a constructor that optionally accepts object name and message kind, and also a copy constructor. An assignment operator (operator=()) and cloning method (dup()) will also be generated.

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

For each field in the above description, 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, FooPacket will contain the following methods:

    virtual int getSourceAddress() const;
    virtual void setSourceAddress(int sourceAddress);
    virtual int getDestAddress() const;
    virtual void setDestAddress(int destAddress);
    virtual bool getHasPayload() const;
    virtual void setHasPayload(bool hasPayload);

Note that the methods are all declared virtual to allow overriding them.

String fields can also be declared:

packet HttpRequestMessage
{
    string method; // "GET", "POST", etc.
    string resource;
};

The generated getter and setter methods will return and accept const char* pointers:

virtual const char *getMethod() const;
virtual void setMethod(const char *method);
virtual const char *getResource() const;
virtual void setResource(const char *resource);

The generated object will have its own copy of the string, so it not only stores the const char* pointer.

6.2.2 Field Data Types

Data types for fields are not limited to int and bool. Several C/C++ and other data types can be used:

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

6.2.3 Initial Values

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

packet RequestPacket
{
    int version = HTTP_VERSION;
    string method = "GET";
    string resource = "/";
    int maxBytes = 100*1024*1024; // 100MiB
    bool keepAlive = true;
};

Macros and expressions are also accepted as initalizer values, as the code above demonstrates. The message compiler does not check the syntax of the values, it merely copies them into the generated C++ file. If there are errors in them, they will be reported by the C++ compiler.

Field initialization statements will be placed into the constructor of the generated class.

6.2.4 Enums

Using a @enum property, a field of the type int or any other integral type can be declared to take its value from an enum. The message compiler will then generate code that allows graphical user interfaces display the symbolic value of the field.

Example:

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

The enum itself has to be declared separately. An enum is declared with the enum keyword, using the following syntax:

enum PayloadType
{
   NONE = 0;
   UDP = 1;
   TCP = 2;
   SCTP = 3;
};

Enum values need to be unique.

The message compiler translates an enum into a normal C++ enum, plus creates an object which stores text representations of the constants. The latter makes it possible for Tkenv and Qtenv to display symbolic names.

If the enum to be associated with a field comes from a different message file, then the enum must be announced and its generated header file be included. An example:

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

enum PayloadType;

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

6.2.5 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:

packet SourceRoutedPacket
{
    int route[4];
};

The generated getter and setter methods will have an extra k argument, the array index:

virtual long getRoute(unsigned k) const;
virtual void setRoute(unsigned k, long route);

When these methods are called with an index that is out of bounds, an exception will be thrown.

6.2.6 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:

packet SourceRoutedPacket
{
    int route[];
};

In this case, the generated class will have two extra methods in addition to the getter and setter methods: one for setting the array size, and another one for returning the current array size.

virtual long getRoute(unsigned k) const;
virtual void setRoute(unsigned k, long route);
virtual unsigned getRouteArraySize() const;
virtual void setRouteArraySize(unsigned n);

The set...ArraySize() method internally allocates a new array. Existing values in the array will be preserved (copied over to the new array.)

The default array size is zero. This means that set...ArraySize(n) needs to be called before one can start filling array elements.

6.2.7 Classes and Structs as Fields

In addition to primitive types, classes, structs and their typedefs may also be used as fields. For example, given a class named IPAddress, one can write the following:

packet IPPacket
{
    int version = 4;
    IPAddress src;
    IPAddress dest;
};

The IPAddress type must be known to the message compiler, and also at compile time to the C++ compiler; section [6.6] will describe how to achieve that.

The generated class will contain IPAddress data members (that is, not pointers to IPAddress objects), and the following getter and setter methods will be generated for them:

virtual IPAddress& getSrc();
virtual const IPAddress& getSrc() const;
virtual void setSrc(const IPAddress& src);

virtual IPAddress& getDest();
virtual const IPAddress& getDest() const;
virtual void setDest(const IPAddress& dest);

6.2.8 Pointer Fields

Pointer fields where the setters and the destructor would delete the previous value are not supported yet. However, there are workarounds, as described below.

You can create a typedef for the pointer and use the typedef name as field type. Then you'll get a plain pointer field where neither the setter nor the destructor deletes the old value (which is a likely memory leak).

Example (section [6.6] will explain the details):

cplusplus {{ typedef Foo *FooPtr; }} // C++ typedef
class noncobject FooPtr; // announcement for the message compiler

packet Bar
{
    FooPtr fooPtr;  // leaky pointer field
};

Then you can customize the class via C++ inheritance and reimplement the setter methods in C++, inserting the missing delete statements. Customization via C++ inheritance will be described in section [6.7.2].

6.2.9 Inheritance

By default, messages are subclassed from cMessage or cPacket. However, you can explicitly specify the base class using the extends keyword (only single inheritance is supported):

packet Ieee80211DataFrame extends Ieee80211Frame
{
    ...
};

For the example above, the generated C++ code will look like this:

// generated C++
class Ieee80211DataFrame : public Ieee80211Frame {
    ...
};

6.2.10 Assignment of Inherited Fields

Message definitions allow for changing the initial value of an inherited field. The syntax is similar to that of a field definition with initial value, only the data type is missing.

An example:

packet Ieee80211Frame
{
    int frameType;
    ...
};

packet Ieee80211DataFrame extends Ieee80211Frame
{
    frameType = DATA_FRAME;  // assignment of 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, it is not the case. The message compiler trusts that such field exists; or rather, it leaves the check to the C++ compiler.

What the message compiler actually does is derives a setter method name from the field name, and generates 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
};

6.3 Classes

Until now we have only seen message and packet descriptions, which generate classes derived from cMessage or cPacket. However, it is also useful to be able to generate classes and structs, for building blocks for messages, as control info objects (see cMessage's setControlInfo() and for other purposes. This section covers classes; structs will be described in the next section.

The syntax for defining classes is almost the same as defining messages, only the class keyword is used instead of message / packet. The base class can be specified with the extends keyword, and defaults to cObject.

Examples:

class TCPCommand  // same as "extends cObject"
{
    ...
};

class TCPOpenCommand extends TCPCommand
{
    ...
};

The generated code:

// generated C++
class TCPCommand : public cObject
{
    ...
};

class TCPOpenCommand : public TCPCommand
{
    ...
};

6.4 Structs

Message definitions allow one to define C-style structs, “C-style” meaning “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;
    opp_string description; // minimal string class that wraps a const char*
    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 (the sizeof of opp_string is the same as that of a const char* pointer).

Inheritance is supported for structs:

struct Base
{
    ...
};

struct Extended extends Base
{
    ...
};

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

6.5 Literal C++ Blocks

It is possible to have C++ code placed directly into the generated code, more precisely, into the generated header file. This is done with the cplusplus keyword and a double curly braces. As we'll see in later sections, cplusplus blocks are customarily used to insert #include directives, typedefs, #define macros and other elements into the generated header.

Example:

cplusplus {{
#include <vector>
#include "foo.h"
#define FOO_VERSION 4
typedef std::vector<int> IntVector;
}}

The message compiler does not try to make sense of the text in the body of the cplusplus block, it just simply copies it into the generated header file.

6.6 Using C++ Types

The message compile only knows about the types defined within the same msg file, and the built-in types. To be able to use other types, for example for fields or as base class, you need to do two things:

  1. Let the message compiler know about the type by announcing it; and
  2. Make sure its C++ declaration will be available at compile time

The next two sections describe how to do each.

6.6.1 Announcing Types to the Message Compiler

To use a C++ type (class, struct a typedef) defined outside the msg file, that type needs to be announced to the message compiler. Type annoucements have a similar syntax to those in C++:

struct Point;
class PrioQueue;  // implies it is derived from cOwnedObject! see below
message TimeoutMessage;
packet TCPSegment;

However, with the class keyword, the message compiler needs to know the whether the class is derived (directly or indirectly) from cOwnedObject, cNamedObject, cObject or none of the above, because it affects code generation. The ancestor class can be declared with the extends keyword, like this:

class IPAddress extends void;  // does not extend any "interesting" class
class ModulePtr extends void;  // ditto
class IntVector extends void;  // ditto
class IPCtlInfo extends cObject;
class FooOption extends cNamedObject;
class PrioQueue extends cOwnedObject;
class IPAddrExt extends IPAddress;  // also OK: IPAddress has been announced

An alternative to extends void is the noncobject modifier:

class noncobject IPAddress; // same as "extends void"

By default, that is, when extends is missing, it is assumed that the class is derived from cOwnedObject. Thus, the following two announcements are equivalent:

class PrioQueue;
class PrioQueue extends cOwnedObject;

6.6.2 Making the C++ Declarations Available

In addition to announcing types to the message compiler, their C++ declarations also need to be available at compile time so that the generated code will actually compile. This can be ensured using cplusplus blocks that insert includes, typedefs, class/struct declarations, etc. into the generated header file:

cplusplus {{
#include "IPAddress.h"
typedef std::vector<int> IntVector;
}}

A cplusplus block is also needed if the desired types are defined in a different message file. The block should contain an include directive to pull in the header file generated from the other message file. It is currently not supported to import types from other message files directly,

Example:

cplusplus {{
#include "TCPSegment_m.h"  // make types defined in TCPSegment.msg available
                           // for the C++ compiler
}}

6.6.3 Putting it Together

Suppose you have header files and message files that define various types:

// IPAddress.h
class IPAddress {
   ...
};

// Location.h
struct Location {
    double lon;
    double lat;
};
// AppPacket.msg
packet AppPacket {
   ...
}

To be able to use the above types in a message definition (and two more, an IntVector and a module pointer), the message file should contain the following lines:

cplusplus {{
#include <vector>
#include "IPAddress.h"
#include "Location.h"
#include "AppPacket_m.h"
typedef std::vector<int> IntVector;
typedef cModule *ModulePtr;
}};

class noncobject IPAddress;
struct Location;
packet AppPacket;
class noncobject IntVector;
class noncobject ModulePtr;

packet AppPacketExt extends AppPacket {
    IPAddress destAddress;
    Location senderLocation;
    IntVector data;
    ModulePtr originatingModule;
}

6.7 Customizing the Generated Class

6.7.1 Customizing Method Names

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

The names of the getter and setter methods can be changed with the @getter and @setter properties. For variable-size array fields, the names of array size getter and setter methods can be changed with @sizeGetter and @sizeSetter.

In addition, the data type for the array size (by default unsigned int) can be changed with @sizetype property.

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.

6.7.2 Customizing the Class via Inheritance

Sometimes you need the generated code to do something more or do something differently than the version generated by the message compiler. For example, when setting an integer field named payloadLength, you might also need to adjust the packet length. That is, the following default (generated) version of the setPayloadLength() method is not suitable:

void FooPacket::setPayloadLength(int payloadLength)
{
    this->payloadLength = payloadLength;
}

Instead, it should look something like this:

void FooPacket::setPayloadLength(int payloadLength)
{
    addByteLength(payloadLength - this->payloadLength);
    this->payloadLength = payloadLength;
}

According to common belief, the largest drawback of generated code is that it is difficult or impossible to fulfill such wishes. Hand-editing of the generated files is worthless, because they will be overwritten and changes will be lost in the code generation cycle.

However, object oriented programming offers a solution. A generated class can simply be customized by subclassing from it and redefining whichever methods need to be different from their generated versions. This practice is known as the Generation Gap design pattern. It is enabled with the @customize property set on the message:

packet FooPacket
{
   @customize(true);
   int payloadLength;
};

If you process the above code with the message compiler, the generated code will contain a FooPacket_Base class instead of FooPacket. Then you would subclass FooPacket_Base to produce FooPacket, while doing your customizations by redefining the necessary methods.

class FooPacket_Base : public cPacket
{
  protected:
    int src;
    // make constructors protected to avoid instantiation
    FooPacket_Base(const char *name=nullptr);
    FooPacket_Base(const FooPacket_Base& other);
  public:
    ...
    virtual int getSrc() const;
    virtual void setSrc(int src);
};

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 is the following (you will find it the generated C++ header too, as a comment):

class FooPacket : public FooPacket_Base
{
  public:
    FooPacket(const char *name=nullptr) : FooPacket_Base(name) {}
    FooPacket(const FooPacket& other) : FooPacket_Base(other) {}
    FooPacket& operator=(const FooPacket& other)
        {FooPacket_Base::operator=(other); return *this;}
    virtual FooPacket *dup() const {return new FooPacket(*this);}
};

Register_Class(FooPacket);

Note that it is important that you redefine dup() and provide an assignment operator (operator=()).

So, returning to our original example about payload length affecting packet length, the code you'd write is the following:

class FooPacket : public FooPacket_Base
{
    // here come the mandatory methods: constructor,
    // copy constructor, operator=(), dup()
    // ...

    virtual void setPayloadLength(int newlength);
}

void FooPacket::setPayloadLength(int newlength)
{
    // adjust message length
    addByteLength(newlength - getPayloadLength());

    // set the new length
    FooPacket_Base::setPayloadLength(newlength);
}

6.7.3 Abstract Fields

The purpose of abstract fields is to let you to override the way the value is stored inside the class, and still benefit from inspectability in graphical user interfaces.

For example, this is the situation when you want to store a bitfield in a single int or short, and yet you want to present bits as individual packet fields. It is also useful for implementing computed fields.

A field is declared abstract by using abstract keyword:

packet FooPacket
{
   @customize(true);
   abstract bool urgentBit;
};

For an abstract field, the message compiler generates no data member, and generated getter/setter methods will be pure virtual:

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

Usually you'll want to use abstract fields together with the Generation Gap pattern, so that you can immediately redefine the abstract (pure virtual) methods and supply your implementation.

6.8 Using Standard Container Classes for Fields

One often wants to use standard container classes (STL) as fields, such as std::vector, std::stack or std::map. The following sections describe two ways this can be done:

  1. via a typedef;
  2. by defining the field as abstract, and customizing the generated class.

6.8.1 Typedefs

The basic idea is that if we create a typedef for the desired type, we can use it for fields just as any other type. Example:

cplusplus {{
#include <vector>
typedef std::vector<int> IntVector;
}}

class noncobject IntVector;

packet FooPacket {
    IntVector addresses;
};

The generated class will have the following methods:

virtual IntVector& getAddresses();
virtual const IntVector& getAddresses() const;
virtual void setAddresses(const IntVector& addresses);

Thus, the underlying std::vector<int> is exposed and you can directly manipulate it from C++ code, for example like this:

FooPacket *pk = new FooPacket();
pk->getAddresses().push_back(1);
pk->getAddresses().push_back(5);
pk->getAddresses().push_back(9);
// or:
IntVector& v = pk->getAddresses();
v.push_back(1);
v.push_back(5);
v.push_back(9);

It is easy. However, there are also some drawbacks:

  1. The message compiler won't know that your field is actually a data structure, so the generated reflection code won't be able to look into it;
  2. The fact that STL classes are directly exposed may be a mixed blessing; on one hand this makes it easier to manipulate its contents, but on the other hand it violates the encapsulation principle. Container classes work best when they are used as “nuts and bolts” for your C++ program, but they shouldn't really be used as public API.

6.8.2 Abstract Fields

This approach uses abstract fields. We exploit the fact that std::vector and std::stack are representations of sequence, which is the same abstraction as fields' variable-size array. That is, if you declare the field to be abstract fieldname[], the message compiler will only generate pure virtual functions and you can implement the underlying data storage using standard container classes. You can also write additional C++ methods that delegate to the container object's push_back(), push(), pop(), etc. methods.

Consider the following message declaration:

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

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

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

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

    // helper method
    void unsupported() {throw cRuntimeError("unsupported method called");}

  public:
    ...
    // foo methods
    virtual int getFoo(unsigned int k) {return foo[k];}
    virtual void setFoo(unsigned int k, int x) {foo[k]=x;}
    virtual void addFoo(int x) {foo.push_back(x);}
    virtual void setFooArraySize(unsigned int size) {foo.resize(size);}
    virtual unsigned int getFooArraySize() const {return foo.size();}

    // bar methods
    virtual int getBar(unsigned int k) {...}
    virtual void setBar(unsigned int k, int x) {unsupported();}
    virtual void barPush(int x) {bar.push(x);}
    virtual void barPop() {bar.pop();}
    virtual int barTop() {return bar.top();}
    virtual void setBarArraySize(unsigned int size) {unsupported();}
    virtual unsigned int getBarArraySize() const {return bar.size();}
};

Register_Class(FooPacket);

Some additional boilerplate code is needed so that the class conforms to conventions, and duplication and copying works 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;
        bar = other.bar;
        return *this;
    }
    virtual FooPacket *dup() {
        return new FooPacket(*this);
    }

Some additional notes:

  1. setFooArraySize(), setBarArraySize() are redundant.
  2. getBar(int k) cannot be implemented in a straightforward way (std::stack does not support accessing elements by index). It could still be implemented in a less efficient way using STL iterators, and efficiency does not seem to be major problem because only Tkenv is going to invoke this function.
  3. setBar(int k, int x) could not be implemented, but this is not particularly a problem. The exception will materialize in a Tkenv error dialog when you try to change the field value.

6.9 Namespaces

It is possible to place the generated classes into a C++ namespace, and also to use types from other namespaces.

6.9.1 Declaring a Namespace

To place the generated types into a namespace, add a namespace declaration near the top of the message file:

namespace inet;

If you are fond of hierarchical (nested) namespaces, you can declare one with a straightforward syntax, using double colons in the namespace declaration. There is no need for multiple nested namespace declarations as in C++:

namespace org::omnetpp::inet::ieee80211;

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

namespace org { namespace omnetpp { namespace inet { namespace ieee80211 {
...
}}}}

Conceptually, the namespace extends from the place of the namespace declaration to the end of the message file. (A message file may contain only one namespace declaration.) In other words, it does matter whether you put something above the namespace declaration line or below it:

  1. The contents of cplusplus blocks above the namespace declaration will be placed outside (i.e. above) the namespace block in the generated C++ header; blocks below the namespace declaration will placed inside the C++ namespace block.
  2. Type announcements are interpreted differently depending on whether they occur above or below the namespace declaration (this will be detailed later).
  3. Types defined with the message syntax are placed into the namespace of the message file; thus, definitions must always be after the namespace declaration. Type definitions above the namespace line will be rejected with an error message.

6.9.2 C++ Blocks and Namespace

As described above, the contents of a cplusplus block will be copied above or into the C++ namespace block in the generated header depending on whether it occurs above or below the namespace declaration in the message file.

The placement of cplusplus blocks relative to the namespace declaration is important because you don't want #include directives to be placed inside the C++ namespace block. That would cause the declarations in the header file to be interpreted as being part of the namespace, which they are not. Includes should always be put into cplusplus blocks above the namespace declaration. This is so important that I repeat it:

As for typedefs and other C++ code, you need to place them above or below the namespace declaration based on whether you want them to be in the C++ namespace or not.

6.9.3 Type Announcements and Namespace

The type announcement syntax allows one to specify the namespace of the type as well, so the following lines are syntactically correct:

packet foo::FooPacket;
packet nes::ted::name::space::BarPacket;
packet ::BazPacket;

Announced type names are interpreted in the following way:

  1. If the type name contains a double colon (::), it is interpreted as being fully qualified with an absolute namespace.
  2. If the name is just an identifier (no double colon), the interpretation depends on whether it is above or below the namespace declaration. If it is above, the name is interpreted as a global type; otherwise it is interpreted as part of the package file's namespace.

This also means that if you want to announce a global type, you either have to put the announcement above the namespace declaration, or prefix the type with “::” to declare that it is not part of a namespace.

When the announced types are used later (as field type, base class, etc.), they can be referred to just with their simple names (without namespace); or alternatively with their fully qualified names. When a message compiler encounters type name as field type or base class, it interprets the type name in the following way:

  1. If the type name contains a double colon (::), it is interpreted as being fully qualified with an absolute namespace.
  2. If the name is just an identifier (no double colon), and the message file's namespace contains that name, it is chosen; otherwise:
  3. It is looked up among all announced types in all namespaces (including the global namespace), and there must be exactly one match. That is, if the same name exists in multiple namespaces, it may only be referenced with fully qualified name.

The following code illustrates the above rules:

cplusplus {{
// includes go above the namespace line
#include <vector>
#include "IPAddress.h"
}}

// the IPAddress type is in the global namespace
class noncobject IPAddress;

namespace foo;  // namespace begins with this line

// we could also have announced IPAddress here as "::IPAddress":
//class noncobject ::IPAddress;

cplusplus {{
// we want IPAddressVector to be part of the namespace
typedef std::vector<IPAddress> IPAddressVector;
}}

// type will be understood as foo::IPAddressVector
class noncobject IPAddressVector;

packet FooPacket {
    IPAddress source;
    IPAddressVector neighbors;
};

Another example that uses a PacketData class and a NetworkPacket type from a net namespace:

// NetworkPacket.msg
namespace net;
class PacketData { }
packet NetworkPacket { }

// FooPacket.msg
cplusplus {{
#include "NetworkPacket_m.h"
}}
class net::PacketData;
packet net::NetworkPacket;

namespace foo;

packet FooPacket extends NetworkPacket
{
    PacketData data;
}

6.10 Descriptor Classes

For each generated class and struct, the message compiler generates an associated descriptor class. The descriptor class carries “reflection” information about the new class, and makes it possible to inspect message contents in Tkenv.

The descriptor class encapsulates virtually all information that the original message definition contains, and exposes it via member functions. It has methods for enumerating fields (getFieldCount(), getFieldName(), getFieldTypeString(), etc.), for getting and setting a field's value in an instance of the class (getFieldAsString(), setFieldAsString()), for exploring the class hierarchy (getBaseClassDescriptor(), etc.), for accessing class and field properties, and for similar tasks. When you inspect a message or packet in the simulation, Tkenv can uses the associated descriptor class to extract and display the field values.

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 description class for the class at all.

It is also possible to use (or abuse) the message compiler for generating a descriptor class for an existing class. (This can be useful for making your class inspectable in Tkenv.) 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(true). This will tell the message compiler that it should not generate an actual class (as it already exists), only a descriptor class.

6.11 Summary

This section summarizes the possibilities offered by message definitions.

Base functionality:

The following data types are supported for fields:

Further features:

Generated code (all generated methods are virtual, although this is not written out in the following table):

Field declaration

Generated code
primitive types
double field;
double getField();
void setField(double d);
string type
string field;
const char *getField();
void setField(const char *);
fixed-size arrays
double field[4];
double getField(unsigned k);
void setField(unsigned k, double d);
unsigned getFieldArraySize();

variable-size arrays
double field[];
void setFieldArraySize(unsigned n);
unsigned getFieldArraySize();
double getField(unsigned k);
void setField(unsigned k, double d);
customized class
class Foo {
  @customize(true);
class Foo_Base { ... };
and you have to write:
class Foo : public Foo_Base {
   ...
};
abstract fields
abstract double field;
double getField() = 0;
void setField(double d) = 0;



[Prev] [Next] [TOC] [Chapters]