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

17 Customizing and Extending OMNeT++

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

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

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.

The following sections elaborate on the various extension interfaces.

17.2 Adding a New Configuration Option

17.2.1 Registration

New configuration options need to be declared with 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)

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

The macro arguments are as follows:

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 pointer to a cConfigOption. The variable can be used later as a “handle” when reading the option value from the configuration database.

17.2.2 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.

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);

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);

17.3 Simulation Lifetime 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 initalization 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 type is one of the following. Their names are fairly self-describing, but the API documentation contains more precise information.

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

Simulation lifecycle listeners are mainly intended for use by classes that extend the simulator's functionality, for example 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); }
    //...
};

17.4 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. A cEvent is executed by invoking its execute() member function. execute() should be overridden in subclasses to carry out the actions associated with the event.

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 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++. Simulation time limit is also implemented with a custom cEvent.

17.5 Defining a New Random Number Generator

This interface lets one add new RNG implementations (see section [7.3]) to OMNeT++. The motivation might be achieving integration with external software (for example something like Akaroa), or exactly replicating 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.

17.6 Defining a New Event Scheduler

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

The job 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:

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++.

17.7 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 make sense 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.

17.8 Defining a New Fingerprint Algorithm

This extension interface allows one to replace or extend the fingerprint computation algorithm (see section [15.4]).

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

17.9 Defining a New Output Scalar Manager

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

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

17.10 Defining a New Output Vector Manager

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

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

17.11 Defining a New Eventlog Manager

An eventlog manager handles the recording of simulation history into an event log (see [13]). The default eventlog manager is EventlogFileManager, which records into 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 to different storage (e.g. to a database or a remote vizualizer).

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

17.12 Defining a New Snapshot Manager

A snapshot manager provides an output stream to which snapshots are written (see section [7.10.5]). The default snapshot manager is cFileSnapshotManager.

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

17.13 Defining a New Configuration Provider

17.13.1 Overview

The configuration provider extension lets one replace ini files with some other storage implementation, for example a database. The configuration provider C++ class must implement the cConfigurationEx interface, and can be activated with the configuration-class configuration option.

The cConfigurationEx interface abstracts the inifile-based data model to some degree. 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.

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

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

17.13.2 The Startup Sequence

From the configuration extension's point of view, the startup sequence looks like the following (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, so that it can 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 goes on as normally, using the new configuration object.

17.13.3 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

17.13.4 Providing a Custom Reader for SectionBasedConfiguration

As said already, 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 with sectionbasedconfig-configreader-class config 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);

17.14 Implementing a New User Interface

It is possible to extend OMNeT++ with a new user interface. The new user interface will have fully equal rights to Cmdenv, Tkenv and Qtenv; that is, it can be activated by starting the simulation executable with the -u <name> command-line or the user-interface configuration option, it can be made the default user interface, it can define new command-line options and configuration options, and so on.

User interfaces must implement (i.e. subclass from) cRunnableEnvir, and must be registered to OMNeT++ with the Register_OmnetApp() macro. In practice, you will almost always want to subclass EnvirBase instead of cRunnableEnvir, because EnvirBase already implements lots of functionality that otherwise you'd have to.

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 (for use 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 largest weight will be activated), and a description string (for help and other purposes).

The C++ class should implement all methods left pure virtual in EnvirBase, and possibly others if you want to customize their behavior. One method that you will surely want to reimplement is run() -- this is where your user interface runs. When this method exits, the simulation program exits.



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