Embedding the Simulation Kernel

Architecture

OMNeT++ has a modular architecture. The following diagram illustrates the high-level architecture of OMNeT++ simulations:

../_images/embed-architecture.svg

The architecture of OMNeT++ simulations

The blocks represent the following components:

  • Sim is the simulation kernel and class library. Sim is a library linked to simulation programs.

  • Envir is another library that contains all code that is common to all the user interfaces. main() also resides in the Envir library. Envir presents itself towards Sim and the executing model as an instance of the cEnvir facade class. Some aspects of the Envir library like result recording can be customized using plugin interfaces. Embedding OMNeT++ into applications usually involves writing a custom cEnvir subclass (see sections Implementing a New User Interface and Embedding the OMNeT++ Simulation Kernel.)

  • Cmdenv, Qtenv are Envir-based libraries that contain specific user interface implementations. A simulation program is linked with one or more of them; in the latter case, one of the UI libraries is chosen and instantiated either explicitly or automatically when the program starts.

  • The Model Component Library includes simple module definitions and their C++ implementations, compound module types, channels, networks, message types, and everything belonging to models that have been linked to the simulation program. A simulation program can run any model that contains all of the required linked components.

  • The Executing Model is the model that is set up for simulation. This model contains objects (modules, channels, and so on) that are all instances of the components in the model component library.

The arrows in the figure describe how components interact with each other:

  • Executing Model :math:`Leftrightarrow` Sim. The simulation kernel manages the future events and activates modules in the executing model as events occur. The modules of the executing model are stored in an instance of the class cSimulation. In turn, the executing model calls functions in the simulation kernel and uses classes in the Sim library.

  • Sim :math:`Leftrightarrow` Model Component Library. The simulation kernel instantiates simple modules and other components when the simulation model is set up at the beginning of the simulation run. In addition, it refers to the component library when dynamic module creation is used. The mechanisms for registering and looking up components in the model component library are implemented as part of Sim.

  • Executing Model :math:`Leftrightarrow` Envir. The Envir presents itself as a facade object towards the executing model. Model code directly accesses Envir e.g. for logging (EV<<).

  • Sim :math:`Leftrightarrow` Envir. Envir is in full command of what happens in the simulation program. Envir contains the main() function where execution begins. Envir determines which models should be set up for simulation, and instructs Sim to do so. Envir contains the main simulation loop (determine-next-event, execute-event sequence) and invokes the simulation kernel for the necessary functionality (event scheduling and event execution are implemented in Sim). Envir catches and handles errors and exceptions that occur in the simulation kernel or in the library classes during execution. Envir presents a single facade object toward Sim – no Envir internals are visible to Sim or the executing model. During simulation model setup, Envir supplies module parameter values for Sim when Sim asks for them. Sim writes output vectors via Envir, so one can redefine the output vector storing mechanism by changing Envir. Sim and its classes use Envir to print debug information.

  • Envir :math:`Leftrightarrow` Cmdenv/Qtenv. Cmdenv, and Qtenv are concrete user interface implementations. When a simulation program is started, the main() function (which is part of Envir) determines the appropriate user interface class, creates an instance and runs it. Sim’s or the model’s calls on Envir are delegated to the user interface.

Embedding the OMNeT++ Simulation Kernel

This section discusses the issues of embedding the simulation kernel or a simulation model into a larger application. We assume that you do not just want to change one or two aspects of the simulator (such as event scheduling or result recording) or create a new user interface similar to Cmdenv or Qtenv – if so, see chapter Customizing and Extending OMNeT++.

For the following section, we assume that you will write the embedding program from scratch, that is, starting from a main() function.

The main() Function

The minimalistic program described below initializes the simulation library and runs two simulations. In later sections, we will review the details of the code and discuss how to improve it.

#include <omnetpp.h>
using namespace omnetpp;

int main(int argc, char *argv[])
{
    // the following line MUST be at the top of main()
    cStaticFlag dummy;

    // initializations
    CodeFragments::executeAll(CodeFragments::STARTUP);
    SimTime::setScaleExp(-12);

    // load NED files
    cSimulation::loadNedSourceFolder("./foodir");
    cSimulation::loadNedSourceFolder("./bardir");
    cSimulation::doneLoadingNedFiles();

    // run two simulations
    simulate("FooNetwork", 1000);
    simulate("BarNetwork", 2000);

    // deallocate registration lists, loaded NED files, etc.
    CodeFragment::executeAll(CodeFragment::SHUTDOWN);
    return 0;
}

The first few lines of the code initialize the simulation library. The purpose of cStaticFlag is to set a global variable to true for the duration of the main() function to help the simulation library handle exceptions correctly in extreme cases. CodeFragment::executeAll(CodeFragment::STARTUP) performs various startup tasks, such as building registration tables out of the Define_Module(), Register_Class(), and similar entries throughout the code. SimTime::setScaleExp(-12) sets the simulation time resolution to picoseconds; other values can be used as well, but it is mandatory to choose one.

Note

The simulation time exponent cannot be changed at a later stage since it is a global variable, and the values of the existing simtime_t instances would change.

The code then loads the NED files from the foodir and bardir subdirectories of the working directory (as if the NED path was ./foodir;./bardir), and runs two simulations.

The simulate() Function

A minimalistic version of the simulate() function is shown below. To shorten the code, the exception handling code has been omitted (try/catch blocks) apart from the event loop. However, every line is marked with “E!” where various problems with the simulation model can occur and can be thrown as exceptions.

void simulate(const char *networkName, simtime_t limit)
{
    // look up network type
    cModuleType *networkType = cModuleType::find(networkName);
    if (networkType == nullptr) {
        printf("No such network: %s\n", networkName);
        return;
    }

    // create a simulation manager and an environment for the simulation
    cEnvir *env = new CustomSimulationEnv(argc, argv, new EmptyConfig());
    cSimulation *sim = new cSimulation("simulation", env);
    cSimulation::setActiveSimulation(sim);

    // set up network and prepare for running it
    sim->setupNetwork(networkType); //E!
    sim->setSimulationTimeLimit(limit);

    // prepare for running it
    sim->callInitialize();

    // run the simulation
    bool ok = true;
    try {
        while (true) {
            cEvent *event = sim->takeNextEvent();
            if (!event)
                break;
            sim->executeEvent(event);
        }
    }
    catch (cTerminationException& e) {
        printf("Finished: %s\n", e.what());
    }
    catch (std::exception& e) {
        ok = false;
        printf("ERROR: %s\n", e.what());
    }

    if (ok)
        sim->callFinish(); //E!

    sim->deleteNetwork(); //E!

    cSimulation::setActiveSimulation(nullptr);
    delete sim; // deletes env as well
}

The function accepts a network type name (which must be fully qualified with a package name) and a simulation time limit.

In the first few lines, the code looks up the network among the available module types and prints an error message if it is not found.

Then it proceeds to create and activate a simulation manager object (cSimulation). The simulation manager requires another object, called the environment object. The environment object is used by the simulation manager to read the configuration. In addition, the simulation results are also written via the environment object.

The environment object (CustomSimulationEnv in the above code) must be provided by the programmer; this is described in detail in a later section.

Note

In versions 4.x and earlier, the simulation manager and the environment object could be accessed as simulation and ev (which were global variables in 3.x and macros in 4.x). In 5.x they can be accessed with the getSimulation() and getEnvir() functions, which are basically aliases to cSimulation::getActiveSimulation() and cSimulation::getActiveSimulation()->getEnvir().

The network is then set up in the simulation manager. The sim->setupNetwork() method creates the system module and recursively all modules and their interconnections; module parameters are also read from the configuration (where required) and assigned. If there is an error (for example, module type not found), an exception will be thrown. The exception object is some kind of std::exception, usually a cRuntimeError.

If the network setup is successful, sim->callInitialize() is invoked next to run the initialization code of modules and channels in the network. An exception is thrown if something goes wrong in any of the initialize() methods.

The next lines run the simulation by calling sim->takeNextEvent() and sim->executeEvent() in a loop. The loop is exited when an exception occurs. The exception may indicate a runtime error or a normal termination condition such as when there are no more events or the simulation time limit has been reached. (The latter are represented by cTerminationException.)

If the simulation has completed successfully (ok == true), the code goes on to call the finish() methods of modules and channels. Then, regardless of whether there was an error, cleanup takes place by calling sim->deleteNetwork().

Finally, the simulation manager object is deallocated, but the active simulation manager is not allowed to be deleted; therefore, it is deactivated using setActiveSimulation(nullptr).

Providing an Environment Object

The environment object needs to be subclassed from the cEnvir class, but since it has many pure virtual methods, it is easier to begin by subclassing cNullEnvir. cNullEnvir defines all pure virtual methods with either an empty body or with a body that throws an "unsupported method called" exception. You can redefine methods to be more sophisticated later on as you progress with the development.

You must redefine the readParameter() method. This enables module parameters to obtain their values. For debugging purposes, you can also redefine sputn() where module log messages are written to. cNullEnvir only provides one random number generator, so if your simulation model uses more than one, you also need to redefine the getNumRNGs() and getRNG(k) methods. To print or store simulation records, redefine recordScalar(), recordStatistic(), and/or the output vector related methods. Other cEnvir methods are invoked from the simulation kernel to inform the environment about messages being sent, events scheduled and cancelled, modules created, and so on.

The following example shows a minimalistic environment class that is enough to get started:

class CustomSimulationEnv : public cNullEnvir
{
  public:
    // constructor
    CustomSimulationEnv(int ac, char **av, cConfiguration *c) :
        cNullEnvir(ac, av, c) {}

    // model parameters: accept defaults
    virtual void readParameter(cPar *par) {
        if (par->containsValue())
            par->acceptDefault();
        else
            throw cRuntimeError("no value for %s", par->getFullPath().c_str());
    }

    // send module log messages to stdout
    virtual void sputn(const char *s, int n) {
        (void) ::fwrite(s,1,n,stdout);
    }
};

Providing a Configuration Object

The configuration object needs to subclass from cConfiguration. cConfiguration also has several methods, but the typed ones (getAsBool(), getAsInt(), etc.) have default implementations that delegate to the much fewer string-based methods (getConfigValue(), etc.).

It is fairly straightforward to implement a configuration class that emulates an empty ini file:

class EmptyConfig : public cConfiguration
{
  protected:
    class NullKeyValue : public KeyValue {
      public:
        virtual const char *getKey() const {return nullptr;}
        virtual const char *getValue() const {return nullptr;}
        virtual const char *getBaseDirectory() const {return nullptr;}
    };
    NullKeyValue nullKeyValue;

  protected:
    virtual const char *substituteVariables(const char *value) {return value;}

  public:
    virtual const char *getConfigValue(const char *key) const
        {return nullptr;}
    virtual const KeyValue& getConfigEntry(const char *key) const
        {return nullKeyValue;}
    virtual const char *getPerObjectConfigValue(const char *objectFullPath,
        const char *keySuffix) const {return nullptr;}
    virtual const KeyValue& getPerObjectConfigEntry(const char *objectFullPath,
        const char *keySuffix) const {return nullKeyValue;}
};

Loading NED Files

NED files can be loaded with any of the following static methods of cSimulation: loadNedSourceFolder(), loadNedFile(), and loadNedText(). The first method loads an entire subdirectory tree, the second method loads a single NED file, and the third method takes a literal string containing NED code and parses it.

Note

One use of loadNedText() is to parse NED sources previously converted to C++ string constants and linked into the executable. This enables creating executables that are self-contained and do not require NED files to be distributed with them.

The above functions can also be mixed, but after the last call, doneLoadingNedFiles() must be invoked (it checks for unresolved NED types).

Loading NED files has a global effect; therefore, they cannot be unloaded.

How to Eliminate NED Files

It is possible to get rid of NED files altogether. This would also remove the dependency on the oppnedxml library and the code in sim/netbuilder, although at the cost of additional coding.

Note

When the only purpose is to get rid of NED files as an external dependency of the program, it is simpler to use loadNedText() on NED files converted to C++ string constants instead.

The trick is to write cModuleType and cChannelType objects for simple module, compound module, and channel types and register them manually. For example, cModuleType has pure virtual methods called createModuleObject(), addParametersAndGatesTo(module), setupGateVectors(module), buildInside(module), which you need to implement. The body of the buildInside() method would be similar to C++ files generated by nedtool of OMNeT++ 3.x.

Assigning Module Parameters

As mentioned earlier, modules obtain values for their input parameters by calling the readParameter() method of the environment object (cEnvir).

Note

readParameter() is only called for parameters that have not been set to a fixed (i.e., non-default) value in the NED files.

The readParameter() method should be written in a manner that enables it to assign the parameter. When doing so, it can recognize the parameter from its name (par->getName()), from its full path (par->getFullPath()), from the owner module’s class (par->getOwner()->getClassName()) or NED type name (((cComponent *)par->getOwner())->getNedTypeName()). Then it can set the parameter using one of the typed setter methods (setBoolValue(), setLongValue(), etc.), or set it to an expression provided in string form (parse() method). It can also accept the default value if it exists (acceptDefault()).

The following code is a straightforward example that answers parameter value requests from a pre-filled table.

class CustomSimulationEnv : public cNullEnvir
{
  protected:
    // parameter (fullpath,value) pairs, needs to be pre-filled
    std::map<std::string,std::string> paramValues;
  public:
    ...
    virtual void readParameter(cPar *par) {
        if (paramValues.find(par->getFullPath()) != paramValues.end())
            par->parse(paramValues[par->getFullPath()]);
        else if (par->containsValue())
            par->acceptDefault();
        else
            throw cRuntimeError("no value for %s", par->getFullPath().c_str());
    }
};

Extracting Statistics from the Model

There are several ways you can extract statistics from the simulation.

C++ Calls into the Model

Modules in the simulation are C++ objects. If you add the appropriate public getter methods to the module classes, you can call them from the main program to obtain statistics. Modules may be looked up with the getModuleByPath() method of cSimulation, then cast to the specific module type via check_and_cast<>() so that the getter methods can be invoked.

cModule *mod = getSimulation()->getModuleByPath("Network.client[2].app");
WebApp *appMod = check_and_cast<WebApp *>(mod);
int numRequestsSent = appMod->getNumRequestsSent();
double avgReplyTime = appMod->getAvgReplyTime();
...

The drawback of this approach is that getters need to be added manually to all affected module classes, which might not be practical, especially if modules come from external projects.

cEnvir Callbacks

A more general way is to catch recordScalar() method calls in the simulation model. The cModule’s recordScalar() method delegates to the similar function in cEnvir. You may define the latter function so that it stores all recorded scalars (for example, in an std::map), where the main program can find them later. Values from output vectors can be captured in a similar manner.

An example implementation:

class CustomSimulationEnv : public cNullEnvir
{
  private:
    std::map<std::string, double> results;
  public:
    virtual void recordScalar(cComponent *component, const char *name,
                              double value, opp_string_map *attributes=nullptr)
    {
       results[component->getFullPath() + "." + name] = value;
    }

    const std::map<std::string, double>& getResults() {return results;}
};

...

const std::map<std::string, double>& results = env->getResults();
int numRequestsSent = results["Network.client[2].app.numRequestsSent"];
double avgReplyTime = results["Network.client[2].app.avgReplyTime"];

A drawback of this approach is that compile-time checking of statistics names is lost, but the advantages are that any simulation model can now be used without changes, and that capturing additional statistics does not require code modification in the main program.

The Simulation Loop

To run the simulation, the takeNextEvent() and executeEvent() methods of cSimulation must be called in a loop:

cSimulation *sim = getSimulation();
while (sim->getSimTime() < limit) {
    cEvent *event = sim->takeNextEvent();
    sim->executeEvent(event);
}

Depending on the concrete scheduler class, the takeNextEvent() may return nullptr in certain cases. The default cSequentialScheduler never returns nullptr.

The execution may terminate in various ways. Runtime errors cause a cRuntimeError (or another kind of std::exception) to be thrown. cTerminationException is thrown on normal termination conditions, such as when the simulation runs out of events to process.

You may customize the loop to exit on other termination conditions as well, such as on a simulation time limit (see above), on a CPU time limit, or when results reach a required accuracy. It is relatively straightforward to build in progress reporting and interactivity (start/stop).

Animation can be hooked up to the appropriate callback methods of cEnvir: beginSend(), sendHop(), endSend(), and others.

Multiple, Coexisting Simulations

It is possible for several instances of cSimulation to coexist, and also to set up and simulate a network in each instance. However, this requires frequent use of cSimulation::setActiveSimulation(). Before invoking any cSimulation method or module method, the corresponding cSimulation instance needs to be designated as the active simulation manager.

Every cSimulation instance should have its own associated environment object (cEnvir). Environment objects may not be shared among several cSimulation instances. The cSimulation’s destructor also removes the associated cEnvir instance.

cSimulation instances may be reused from one simulation to another, but it is also possible to create a new instance for each simulation run.

Note

It is not possible to run different simulations concurrently from different threads due to the use of global variables which are not easy to eliminate, such as the active simulation manager pointer and the active environment object pointer. Static buffers and objects (like string pools) are also used for efficiency reasons in some places inside the simulation kernel.

Installing a Custom Scheduler

The default event scheduler is cSequentialScheduler. To replace it with a different scheduler (e.g., cRealTimeScheduler or your own scheduler class), add a setScheduler() call into main():

cScheduler *scheduler = new CustomScheduler();
getSimulation()->setScheduler(scheduler);

It is usually not a good idea to change schedulers in the middle of a simulation; therefore, setScheduler() may only be called when no network is set up.

Multi-Threaded Programs

The OMNeT++ simulation kernel is not reentrant; therefore, it must be protected against concurrent access.