Customizing and Extending OMNeT++

Overview

OMNeT++ is an open system, and several details of its operation can be customized and extended by writing C++ code. Some extension interfaces have already been covered in other chapters:

This chapter will begin by introducing some infrastructure features that are useful for extensions:

  • Config options. This facility lets other extension classes define their own configuration options.

  • Simulation lifecycle listeners allow extensions to get notified when a network is set up, simulation is started, paused or resumed, the simulation ended successfully or with an error, and so on.

  • cEvent lets extensions schedule actions for certain simulation times. This is especially useful for custom event schedulers that we’ll cover later in this chapter.

Then we will continue with the descriptions of the following extension interfaces:

  • cRNG lets one add new random number generator algorithms.

  • cScheduler is an interface for event schedulers. This extension interface allows for implementing real-time, hardware-in-the-loop, distributed and distributed parallel simulation.

  • cFutureEventSet. This extension interface allows one to replace the data structure used for storing future events during simulation, i.e. the FES. This may make sense for specialized workloads.

  • cFingerprintCalculator. This extension interface allows one to replace or extend the fingerprint computation algorithm.

  • cIOutputScalarManager. This extension interface allows one to create additional means of saving scalar results, for example database or CSV output.

  • cIOutputVectorManager. This extension interface allows one to create additional means of saving vector results, for example database or CSV output.

  • cIEventlogManager. This extension interface allows one to customize event log recording.

  • cISnapshotManager. It provides an output stream to which snapshots are written.

  • cConfigurationEx. Configuration provider extension. This extension interface lets one replace omnetpp.ini with some other implementation, for example a database.

  • User interfaces. When existing runtime user interfaces (Cmdenv, Qtenv) don’t suffice, one can create a new one, reusing the infrastructure provided by the common base of the three.

Many extension interfaces follow a common pattern: one needs to implement a given interface class (e.g. cRNG for random number generators), let OMNeT++ know about it by registering the class with the Register_Class() macro, and finally activate it by the appropriate configuration option (e.g. rng-class=MyRNG). The interface classes (cRNG, cScheduler, etc.) are documented in the API Reference.

Note

A common error is that OMNeT++ cannot find the class at runtime. When that happens, make sure the executable actually contains the code of the class. When linking with a library, over-optimizing linkers (especially on Unix) tend to leave out code which seems to be unreferenced by other parts of the program.

The following sections elaborate on the various extension interfaces.

Adding a New Configuration Option

Registration

New configuration options need to be declared using one of the appropriate registration macros. These macros are:

Register_GlobalConfigOption(ID, NAME, TYPE, DEFAULTVALUE, DESCRIPTION)
Register_PerRunConfigOption(ID, NAME, TYPE, DEFAULTVALUE, DESCRIPTION)
Register_GlobalConfigOptionU(ID, NAME, UNIT, DEFAULTVALUE, DESCRIPTION)
Register_PerRunConfigOptionU(ID, NAME, UNIT, DEFAULTVALUE, DESCRIPTION)
Register_PerObjectConfigOption(ID, NAME, KIND, TYPE, DEFAULTVALUE, DESCRIPTION)
Register_PerObjectConfigOptionU(ID, NAME, KIND, UNIT, DEFAULTVALUE, DESCRIPTION)

Configuration options come in three flavors, as indicated by the macro names:

  • Global options affect all configurations (i.e., they are only accepted in the [General] section but not in [Config <name>] sections).

  • Per-Run options can be specified in any section (i.e., both in [General] and in [Config <name>] sections). They affect the configuration they occur in.

  • Per-Object options can be specified in any section (i.e., both in [General] and in [Config <name>] sections). They are specific to an object or group of objects. Their names must always contain a hyphen (-) character so that they can be distinguished from module/channel parameter assignments when they occur in ini files.

The macro arguments are as follows:

  • ID is a C++ identifier that becomes the name of a global variable, a pointer to a cConfigOption object that the macro creates. It allows you to refer to the configuration option, e.g., when querying its value using the member functions of cConfiguration.

  • NAME is the name of the option (a string).

  • KIND applies to per-object configuration options and clarifies what kind of objects the option applies to. Its value must be one of: KIND_COMPONENT (module or channel), KIND_CHANNEL, KIND_MODULE (simple or compound module), KIND_SIMPLE_MODULE, KIND_PARAMETER (module or channel parameter), KIND_STATISTIC (statistic declared in NED via @statistic), KIND_SCALAR (output scalar), KIND_VECTOR (output vector), KIND_UNSPECIFIED_TYPE (only used for the typename option), KIND_OTHER (anything else).

  • TYPE is the data type of the config option; it must be one of: CFG_BOOL, CFG_INT, CFG_DOUBLE, CFG_STRING, CFG_FILENAME, CFG_FILENAMES, CFG_PATH, CFG_CUSTOM. The most significant difference between filesystem-related types (filename, filenames, path) and plain strings is that relative filenames and paths are automatically converted to absolute when the configuration is read, with the base directory being the location of the ini file from which the configuration entry was read.

  • UNIT is a string that names the measurement unit in which the option’s value is to be interpreted; it implies type CFG_DOUBLE.

  • DEFAULTVALUE is the default value in textual form (string); this should be nullptr if the option has no default value.

  • DESCRIPTION is an arbitrarily long string that describes the purpose and operation of the option. It will be used in help texts, etc.

For example, the debug-on-errors option is declared in the following way:

Register_GlobalConfigOption(CFGID_DEBUG_ON_ERRORS, "debug-on-errors",
    CFG_BOOL, "false", "When enabled, runtime errors will cause...");

The macro will register the option and also declare the CFGID_DEBUG_ON_ERRORS variable as a pointer to a cConfigOption. The variable can be used later as a “handle” when reading the option’s value from the configuration database.

Reading the Value

The configuration is accessible via the getConfig() method of cEnvir. It returns a pointer to the configuration object (cConfiguration):

cConfiguration *config = getEnvir()->getConfig();

cConfiguration provides several methods for querying the configuration.

Note

The configuration object provides a flattened view of the ini file. Sections that inherit from each other are merged. Configuration options provided on the command line in the form --option=value are added first to the object. This ensures that the command line options take precedence over the values specified in the INI file.

const char *getAsCustom(cConfigOption *entry, const char *fallbackValue=nullptr);
bool getAsBool(cConfigOption *entry, bool fallbackValue=false);
long getAsInt(cConfigOption *entry, long fallbackValue=0);
double getAsDouble(cConfigOption *entry, double fallbackValue=0);
std::string getAsString(cConfigOption *entry, const char *fallbackValue="");
std::string getAsFilename(cConfigOption *entry);
std::vector<std::string> getAsFilenames(cConfigOption *entry);
std::string getAsPath(cConfigOption *entry);

The fallbackValue is returned if the value is not specified in the configuration and there is no default value.

bool debug = getEnvir()->getConfig()->getAsBool(CFGID_PARSIM_DEBUG);

Simulation Lifecycle Listeners

cISimulationLifecycleListener is a callback interface for receiving notifications at various stages of simulations: setting up, running, tearing down, etc. Extension classes, such as custom event schedulers, often need this functionality for performing initialization and various other tasks.

Listeners of the type cISimulationLifecycleListener need to be added to cEnvir with its addLifecycleListener() method and removed with removeLifecycleListener().

cISimulationLifecycleListener *listener = ...;
getEnvir()->addLifecycleListener(listener);
// and finally:
getEnvir()->removeLifecycleListener(listener);

To implement a simulation lifecycle listener, subclass from cISimulationLifecycleListener and override its lifecycleEvent() method. It has the following signature:

virtual void lifecycleEvent(SimulationLifecycleEventType eventType, cObject *details) = 0;

Event types include:

  • LF_ON_STARTUP

  • LF_PRE_NETWORK_SETUP, LF_POST_NETWORK_SETUP

  • LF_PRE_NETWORK_INITIALIZE, LF_POST_NETWORK_INITIALIZE

  • LF_ON_SIMULATION_START

  • LF_ON_SIMULATION_PAUSE, LF_ON_SIMULATION_RESUME

  • LF_ON_SIMULATION_SUCCESS, LF_ON_SIMULATION_ERROR

  • LF_PRE_NETWORK_FINISH, LF_POST_NETWORK_FINISH

  • LF_ON_RUN_END

  • LF_PRE_NETWORK_DELETE, LF_POST_NETWORK_DELETE

  • LF_ON_SHUTDOWN

The details argument is currently nullptr; future OMNeT++ versions may pass extra information in it. Notifications always refer to the active simulation in case there are more (see cSimulation’s getActiveSimulation()).

Simulation lifecycle listeners are mainly intended for use by classes that extend the simulator’s functionality, such as custom event schedulers and output vector/scalar managers. The lifecycle of such an extension object is managed by OMNeT++, so one can use their constructor to create and add the listener object to cEnvir, and the destructor to remove and delete it. The code is further simplified if the extension object itself implements cISimulationLifecycleListener:

class CustomScheduler : public cScheduler, public cISimulationLifecycleListener
{
  public:
    CustomScheduler() { getEnvir()->addLifecycleListener(this); }
    ~CustomScheduler() { getEnvir()->removeLifecycleListener(this); }
    //...
};

cEvent

cEvent represents an event in the discrete event simulator. When events are scheduled, they are inserted into the future events set (FES). During the simulation, events are removed from the FES and executed one by one in timestamp order. The cEvent is executed by invoking its execute() member function. execute() should be overridden in subclasses to carry out the actions associated with the event.

Note

cMessage is also a subclass of cEvent. Its execute() method calls the handleMessage() method of the message’s destination module or switches to the coroutine of its activity() method.

execute() has the following signature:

virtual void execute() = 0;

Raw (non-message) event objects are an internal mechanism of the OMNeT++ simulation kernel and should not be used in programming simulation models. However, they can be very useful when implementing custom event schedulers. For example, in co-simulation, events that occur in the other simulator may be represented with a cEvent in OMNeT++. The simulation time limit is also implemented with a custom cEvent.

Defining a New Random Number Generator

This interface allows adding new RNG implementations (see section Random Number Generators) to OMNeT++. The motivation might be to achieve integration with external software (for example, something like Akaroa) or to replicate exactly the trajectory of a simulation ported from another simulation framework that uses a different RNG.

The new RNG C++ class must implement the cRNG interface and can be activated with the rng-class configuration option.

Defining a New Event Scheduler

This extension interface allows you to replace the event scheduler class with a custom one, which is essential for implementing many features, including cosimulation, real-time simulation, network or device emulation, and distributed simulation.

The role of the event scheduler is to always return the next event to be processed by the simulator. The default implementation returns the first event in the future events list. Other variants include:

  • For real-time simulation, this scheduler is replaced with one augmented with wait calls (e.g. usleep()) that synchronize the simulation time to the system clock. There are several options for what should happen if the simulation time has already fallen behind: one may re-adjust the reference time, leave it unchanged in the hope of catching up later, or stop with an error message.

  • For emulation, the real-time scheduler is augmented with code that captures packets from real network devices and inserts them into the simulation. INET Framework, the main protocol simulation package for OMNeT++, contains an emulation scheduler. It uses the pcap library to capture packets and raw sockets to send packets to a real network device. Emulation in INET also involves header serializer classes that convert between protocol headers and their C++ object representations used within the simulation.

  • For parallel simulation (see chapter Parallel Distributed Simulation), the scheduler is modified to listen for messages arriving from other logical processes (LPs) and inserts them into the simulation. The scheduler also blocks the simulation when it is not safe to execute the next event due to a potential causality violation, until clearance arrives from other LPs to continue in the form of a null message.

  • OMNeT++ supports distributed simulation using HLA (IEEE 1516) [1] as well. The scheduler plays the role of the HLA Federate Ambassador, is responsible for exchanging messages (interactions, change notifications, etc.) with other federates, and performs time regulation.

  • OMNeT++ also supports mixing SystemC (IEEE 1666-2005) modules with OMNeT++ modules in the simulation. When this feature is enabled, there are two future event lists in the simulation: OMNeT++’s and SystemC’s. A special scheduler takes care to consume events from both lists in increasing timestamp order. This method of performing mixed simulations is orders of magnitude faster and also more flexible than letting the two simulators execute in separate processes and communicate over a pipe or socket connection.

The scheduler C++ class must implement the cScheduler interface and can be activated with the scheduler-class configuration option.

Simulation lifetime listeners and the cEvent class can be extremely useful when implementing certain types of event schedulers.

To see examples of scheduler classes, check the cSequentialScheduler and cRealTimeScheduler classes in the simulation kernel, cSocketRTScheduler which is part of the Sockets sample simulation, or cParsimSynchronizer and its subclasses that are part of the parallel simulation support of OMNeT++.

Defining a New FES Data Structure

This extension interface allows one to replace the data structure used for storing future events during simulation, i.e., the FES. Replacing the FES may be suitable for specialized workloads or for the purpose of performance comparison of various FES algorithms. (The default, binary heap-based FES implementation is a good choice for general workloads.)

The FES C++ class must implement the cFutureEventSet interface and can be activated with the futureeventset-class configuration option.

Defining a New Fingerprint Algorithm

This extension interface allows one to replace or extend the fingerprint computational algorithm (see section Fingerprint Tests).

The computational class for fingerprint must implement the cFingerprintCalculator interface, and can be activated with the fingerprintcalculator-class configuration option.

Defining a New Output Scalar Manager

An output scalar manager handles the recording of scalar and histogram output data. The default output scalar manager is cFileOutputScalarManager, which saves data into .sca files. This extension interface allows one to create additional means of saving scalar and histogram results, such as database or CSV output.

The new class must implement cIOutputScalarManager and can be activated with the outputscalarmanager-class configuration option.

Defining a New Output Vector Manager

An output vector manager handles the recording of output vectors produced by objects such as cOutVector. The default output vector manager is cIndexedFileOutputVectorManager, which saves data into .vec files indexed in separate .vci files. This extension interface allows one to create additional means of saving vector results, such as database or CSV output.

The new class must implement the cIOutputVectorManager interface and can be activated with the outputvectormanager-class configuration option.

Defining a New Eventlog Manager

An eventlog manager handles the recording of simulation history into an event log (see Eventlog). The default eventlog manager is EventlogFileManager, which records into a file and also allows for some filtering. By replacing the default eventlog manager class, one can introduce additional filtering, record into a different file format, or store it in different storage (e.g., a database or a remote visualizer).

The new class must implement the cIEventlogManager interface and can be activated with the eventlogmanager-class configuration option.

Defining a New Snapshot Manager

A snapshot manager provides an output stream to which snapshots are written (see Section Snapshots). The default snapshot manager is cFileSnapshotManager.

The new class must implement the cISnapshotManager interface and can be activated with the snapshotmanager-class configuration option.

Defining a New Configuration Provider

Overview

The configuration provider extension allows one to replace ini files with some other storage implementation, such as a database. The configuration provider C++ class must implement the cConfigurationEx interface and can be activated using the configuration-class configuration option.

The cConfigurationEx interface abstracts the inifile-based data model to some extent. It assumes that the configuration data consists of several named configurations. Before every simulation run, one of the named configurations is activated, and from then on, all queries into the configuration operate on the active named configuration only.

In practice, you will probably use the SectionBasedConfiguration class (in src/envir) or subclass it because it already implements a lot of functionality that you would otherwise have to implement.

SectionBasedConfiguration does not assume ini files or any other specific storage format. Instead, it accepts an object that implements the cConfigurationReader interface to provide the data in its raw form. The default implementation of cConfigurationReader is InifileReader.

The Startup Sequence

From the perspective of the configuration extension, the startup sequence looks as follows (see src/envir/startup.cc in the source code):

  1. First, ini files specified on the command line are read into a boot-time configuration object. The boot-time configuration is always a SectionBasedConfiguration with InifileReader.

  2. Shared libraries are loaded (see the -l command-line option and the load-libs configuration option). This allows configuration classes to come from shared libraries.

  3. The configuration-class configuration option is examined. If it is present, a configuration object of the given class is instantiated and replaces the boot-time configuration. The new configuration object is initialized from the boot-time configuration, allowing it to read parameters (e.g., database connection parameters, XML file name, etc.) from it. Then the boot-time configuration object is deallocated.

  4. The load-libs option from the new configuration object is processed.

  5. Then everything proceeds as normal, using the new configuration object.

Providing a Custom Configuration Class

To replace the configuration object with a custom implementation, one needs to subclass cConfigurationEx, register the new class:

#include "cconfiguration.h"

class CustomConfiguration : public cConfigurationEx
{
   ...
};

Register_Class(CustomConfiguration);

and then activate it in the boot-time configuration:

[General]
configuration-class = CustomConfiguration

Providing a Custom Reader for SectionBasedConfiguration

As mentioned earlier, writing a configuration class from scratch can be a lot of work, and it may be more practical to reuse SectionBasedConfiguration with a different configuration reader class. This can be done using the sectionbasedconfig-configreader-class configuration option, which is interpreted by SectionBasedConfiguration. Specify the following in the boot-time ini file:

[General]
configuration-class = SectionBasedConfiguration
sectionbasedconfig-configreader-class = <new-reader-class>

The configuration reader class should look like this:

#include "cconfigreader.h"

class DatabaseConfigurationReader : public cConfigurationReader
{
   ...
};

Register_Class(DatabaseConfigurationReader);

Implementing a New User Interface

It is possible to extend OMNeT++ with a new user interface. The new user interface will have the same privileges as Cmdenv and Qtenv. This means that it can be activated by starting the simulation executable with the -u <name> command-line or the user-interface configuration option. It can also be set as the default user interface and define new command-line options and configuration options.

User interfaces must implement (i.e. subclass from) cRunnableEnvir and must be registered to OMNeT++ with the Register_OmnetApp() macro. In most cases, you will want to subclass EnvirBase instead of cRunnableEnvir because EnvirBase already implements many functionalities that you would otherwise have to implement.

Note

If you want something completely different from what EnvirBase provides, such as embedding the simulation kernel into another application, then you should refer to section Embedding the OMNeT++ Simulation Kernel, not this one.

An example user interface:

#include "envirbase.h"

class FooEnv : public EnvirBase
{
    ...
};

Register_OmnetApp("FooEnv", FooEnv, 30, "an experimental user interface");

The envirbase.h header comes from the src/envir directory, so it is necessary to add it to the include path (-I).

The arguments to Register_OmnetApp() include the user interface name (to be used with the -u and user-interface options), the C++ class that implements it, a weight for default user interface selection (if -u is missing, the user interface with the highest weight will be activated), and a description string (for help and other purposes).

The C++ class should implement all methods left as pure virtual in EnvirBase, and possibly other methods if you want to customize their behavior. One method that you will certainly want to re-implement is run(), as this is where your user interface will be executed. Once this method exits, the simulation program will also exit.

Note

A good starting point for implementing your own user interface is Cmdenv. You can simply copy and modify its source code to quickly get started.