Simulation Manual
Simulation Manual
Simulation Manual
Version 6.x
Copyright ©1992-2021, András Varga and OpenSim Ltd.
OMNeT++ Simulation Manual –
Chapters
Contents v
1 Introduction 1
2 Overview 3
4 Simple Modules 55
13 Eventlog 355
15 Testing 369
iii
18 Embedding the Simulation Kernel 405
References 583
Index 586
OMNeT++ Simulation Manual –
Contents
Contents v
1 Introduction 1
1.1 What Is OMNeT++? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Organization of This Manual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2 Overview 3
2.1 Modeling Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.1.1 Hierarchical Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.2 Module Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.3 Messages, Gates, Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.4 Modeling of Packet Transmissions . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.5 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.6 Topology Description Method . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Programming the Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3 Using OMNeT++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3.1 Building and Running Simulations . . . . . . . . . . . . . . . . . . . . . . . 6
2.3.2 What Is in the Distribution . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
v
3.5 Channels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.6 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.6.1 Assigning a Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.6.2 Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.6.3 Parameter References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.6.4 Volatile Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.6.5 Mutable Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.6.6 Units . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.6.7 XML Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.6.8 Object Parameters and Structured Data . . . . . . . . . . . . . . . . . . . . 31
3.6.9 Passing a Formula as Parameter . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.7 Gates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.8 Submodules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.9 Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.9.1 Channel Specification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.9.2 Reconnecting Gates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.9.3 Channel Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.10 Multiple Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.10.1Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.10.2Connection Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.11 Parametric Submodule and Connection Types . . . . . . . . . . . . . . . . . . . . . 43
3.11.1Parametric Submodule Types . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.11.2Conditional Parametric Submodules . . . . . . . . . . . . . . . . . . . . . . 45
3.11.3Parametric Connection Types . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.12 Metadata Annotations (Properties) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.12.1Property Indices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.12.2Data Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.12.3Overriding and Extending Property Values . . . . . . . . . . . . . . . . . . . 49
3.12.4Known Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.13 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.14 Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.14.1Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.14.2Name Resolution, Imports . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.14.3Name Resolution With "like" . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.14.4The Default Package . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4 Simple Modules 55
4.1 Simulation Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.1.1 Discrete Event Simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.1.2 The Event Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.1.3 Events and Event Execution Order in OMNeT++ . . . . . . . . . . . . . . . 56
4.1.4 Simulation Time . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.1.5 FES Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.2 Components, Simple Modules, Channels . . . . . . . . . . . . . . . . . . . . . . . . 58
4.3 Defining Simple Module Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.3.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.3.2 Constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.3.3 Initialization and Finalization . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.4 Adding Functionality to cSimpleModule . . . . . . . . . . . . . . . . . . . . . . . . 64
4.4.1 handleMessage() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
4.4.2 activity() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
4.4.3 Use Modules Instead of Global Variables . . . . . . . . . . . . . . . . . . . . 73
4.4.4 Reusing Module Code via Subclassing . . . . . . . . . . . . . . . . . . . . . 74
4.5 Accessing Module Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.5.1 Reading the Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.5.2 Volatile versus Non-Volatile Parameters . . . . . . . . . . . . . . . . . . . . . 75
4.5.3 Object Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.5.4 JSON-Style Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.5.5 Changing a Parameter’s Value . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.5.6 Further cPar Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.5.7 Reacting to Parameter Changes . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.6 Accessing Gates and Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.6.1 Gate Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.6.2 Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.6.3 The Connection’s Channel . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.7 Sending and Receiving Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.7.1 Self-Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.7.2 Sending Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4.7.3 Broadcasts and Retransmissions . . . . . . . . . . . . . . . . . . . . . . . . 88
4.7.4 Delayed Sending . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
4.7.5 Direct Message Sending . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.7.6 Packet Transmissions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
4.7.7 Transmission Updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
4.7.8 Receiving Packets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
4.7.9 Receiving Messages with activity() . . . . . . . . . . . . . . . . . . . . . . . . 96
4.8 Channels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.8.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.8.2 The Channel API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.8.3 Channel Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
4.9 Stopping the Simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.9.1 Normal Termination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.9.2 Raising Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.10 Finite State Machines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.10.1Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.11 Navigating the Module Hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
4.11.1Module Vectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
4.11.2Component IDs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
4.11.3Walking Up and Down the Module Hierarchy . . . . . . . . . . . . . . . . . 107
4.11.4Finding Modules by Path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
4.11.5Iterating over Submodules . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
4.11.6Navigating Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
4.12 Direct Method Calls Between Modules . . . . . . . . . . . . . . . . . . . . . . . . . 108
4.13 Dynamic Module Creation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
4.13.1When To Use . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
4.13.2Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
4.13.3Creating Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
4.13.4Deleting Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.13.5The preDelete() method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.13.6Component Weak Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.13.7Module Deletion and finish() . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
4.13.8Creating Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
4.13.9Removing Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
4.14 Signals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
4.14.1Design Considerations and Rationale . . . . . . . . . . . . . . . . . . . . . . 116
4.14.2The Signals Mechanism . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
4.14.3Listening to Model Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
4.15 Signal-Based Statistics Recording . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
4.15.1Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
4.15.2Declaring Statistics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
4.15.3Demultiplexing Results with the Demux Filter . . . . . . . . . . . . . . . . . 128
4.15.4Statistics Recording for Dynamically Registered Signals . . . . . . . . . . . 129
4.15.5Adding Result Filters and Recorders Programmatically . . . . . . . . . . . . 130
4.15.6Emitting Signals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
4.15.7Writing Result Filters and Recorders . . . . . . . . . . . . . . . . . . . . . . 132
13 Eventlog 355
13.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
13.2 Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
13.2.1File Name . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
13.2.2Recording Intervals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
13.2.3Recording Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
13.2.4Recording Message Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
13.3 Eventlog Tool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
13.3.1Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
13.3.2Echo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
15 Testing 369
15.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
15.1.1Verification, Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
15.1.2Unit Testing, Regression Testing . . . . . . . . . . . . . . . . . . . . . . . . . 369
15.2 The opp_test Tool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
15.2.1Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
15.2.2Terminology . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
15.2.3Test File Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
15.2.4Test Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
15.2.5Test Code Generation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
15.2.6PASS Criteria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
15.2.7Extra Processing Steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
15.2.8Error . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
15.2.9Expected Failure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
15.2.10
Skipped . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
15.2.11
opp_test Synopsis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
15.2.12
Writing the Control Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
15.3 Smoke Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
15.4 Fingerprint Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
15.4.1Fingerprint Computation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
15.4.2Fingerprint Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
15.5 Unit Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
15.6 Module Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
15.7 Statistical Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
15.7.1Validation Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
15.7.2Statistical Regression Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
15.7.3Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
References 583
Index 586
OMNeT++ Simulation Manual – Introduction
Chapter 1
Introduction
• Protocol modeling.
• In general, modeling and simulation of any system where the discrete event approach
is suitable, and can be conveniently mapped into entities communicating by exchanging
messages.
OMNeT++ itself is not a simulator of anything concrete, but rather provides infrastructure
and tools for writing simulations. One of the fundamental ingredients of this infrastructure
is a component architecture for simulation models. Models are assembled from reusable
components termed modules. Well-written modules are truly reusable and can be combined
in various ways, like LEGO blocks.
Modules can be connected with each other via gates (other systems would call them ports) and
combined to form compound modules. The depth of module nesting is not limited. Modules
communicate through message passing, where messages may carry arbitrary data structures.
Modules can pass messages along predefined paths via gates and connections or directly to
their destination. The latter is useful for wireless simulations, for example. Modules may
have parameters that can be used to customize module behavior and/or to parameterize
the model’s topology. Modules at the lowest level of the module hierarchy are called simple
modules and encapsulate model behavior. Simple modules are programmed in C++ and make
use of the simulation library.
1
OMNeT++ Simulation Manual – Introduction
OMNeT++ simulations can be run under various user interfaces. Graphical, animating user
interfaces are highly useful for demonstration and debugging purposes, and command-line
user interfaces are best for batch execution.
The simulator as well as user interfaces and tools are highly portable. They are tested on the
most common operating systems (Linux, macOS, Windows) and they can be compiled out of
the box or after trivial modifications on most Unix-like operating systems.
OMNeT++ also supports parallel distributed simulation. OMNeT++ can use several mecha-
nisms for communication between partitions of a parallel distributed simulation, for example,
MPI or named pipes. The parallel simulation algorithm can easily be extended, or new ones
can be plugged in. Models do not need any special instrumentation to be run in parallel – it
is just a matter of configuration. OMNeT++ can even be used for classroom presentation of
parallel simulation algorithms because simulations can be run in parallel even under the GUI
that provides detailed feedback on what is going on.
OMNEST is the commercially supported version of OMNeT++. OMNeT++ is free only for aca-
demic and non-profit use; for commercial purposes, one needs to obtain OMNEST licenses
from Simulcraft Inc.
• Chapters 8 and 14 explain how to customize the network graphics and how to write NED
source code comments from which documentation can be generated.
• Chapters 9, 10, 11, and ?? deal with practical issues like building and running simula-
tions and analyzing results, and describe the tools OMNeT++ provides to support these
tasks.
2
OMNeT++ Simulation Manual – Overview
Chapter 2
Overview
Network
Simple modules
Compound module
Modules communicate with messages that can contain arbitrary data, in addition to the usual
attributes such as a timestamp. Simple modules typically send messages through gates, but
it is also possible to send them directly to their destination modules. Gates are the input and
output interfaces of modules: messages are sent through output gates and arrive through
input gates. An input gate and output gate can be linked by a connection. Connections are
created within a single level of module hierarchy; within a compound module, the gates of
3
OMNeT++ Simulation Manual – Overview
two submodules, or a gate of one submodule and a gate of the compound module can be
connected. Connections spanning hierarchy levels are not permitted, as they would hinder
model reuse. Because of the hierarchical structure of the model, messages typically travel
through a chain of connections, starting and arriving in simple modules. Compound modules
act like "cardboard boxes" in the model, transparently relaying messages between their inner
realm and the outside world. Parameters such as propagation delay, data rate, and bit error
rate can be assigned to connections. One can also define connection types with specific
properties (referred to as channels) and reuse them in several places. Modules can have
parameters. Parameters are used mainly to pass configuration data to simple modules, and
to help define the model’s topology. Parameters can hold string, numeric, or boolean values.
Because parameters are represented as objects in the program, parameters – in addition to
holding constants – may also act as sources of random numbers, with the actual distributions
provided by the model configuration. They may interactively prompt the user for a value, and
they may also hold expressions referencing other parameters. Compound modules may pass
parameters or expressions of parameters to their submodules.
OMNeT++ provides efficient tools for the user to describe the structure of the actual system.
Some of the main features are as follows:
Both simple and compound modules are instances of module types. In describing the model,
the user defines module types; instances of these module types serve as components for
more complex module types. Finally, the user creates the system module as an instance of a
previously defined module type; all modules in the network are instantiated as submodules
and sub-submodules of the system module.
When a module type is used as a building block, it makes no difference whether it is a simple
or compound module. This allows the user to split a simple module into several simple
4
OMNeT++ Simulation Manual – Overview
2.1.5 Parameters
Modules can have parameters. Parameters can be assigned in either the NED files or the
configuration file omnetpp.ini.
Parameters can be used to customize simple module behavior and to parameterize the model’s
topology.
5
OMNeT++ Simulation Manual – Overview
Parameters can hold string, numeric, or boolean values or can contain XML data trees. Nu-
meric values include expressions using other parameters and calling C functions, random
variables from different distributions, and values input interactively by the user.
Numeric-valued parameters can be used to construct topologies in a flexible way. Within a
compound module, parameters can define the number of submodules, number of gates, and
the way the internal connections are made.
The user defines the structure of the model in NED language descriptions (Network Descrip-
tion). The NED language will be discussed in detail in chapter 3.
• message, packet
The classes are also specially instrumented, allowing one to traverse objects of a running
simulation and display information about them such as name, class name, state variables, or
contents. This feature makes it possible to create a simulation GUI where all internals of the
simulation are visible.
This section provides insights into working with OMNeT++ in practice. Issues such as model
files and compiling and running simulations are discussed.
An OMNeT++ model consists of the following parts:
6
OMNeT++ Simulation Manual – Overview
• NED language topology description(s) (.ned files) that describe the module structure with
parameters, gates, etc. NED files can be written using any text editor, but the OMNeT++
IDE provides excellent support for two-way graphical and text editing.
• Message definitions (.msg files) that let one define message types and add data fields to
them. OMNeT++ will translate message definitions into full-fledged C++ classes.
• Simple module sources. They are C++ files, with .h/.cc suffix.
• Simulation kernel. This contains the code that manages the simulation and the simula-
tion class library. It is written in C++, compiled into a shared or static library.
• User interfaces. OMNeT++ user interfaces are used in simulation execution, to facilitate
debugging, demonstration, or batch execution of simulations. They are written in C++,
compiled into libraries.
Simulation programs are built from the above components. First, .msg files are translated into
C++ code using the opp_msgc. program. Then all C++ sources are compiled and linked with
the simulation kernel and a user interface library to form a simulation executable or shared
library. NED files are loaded dynamically in their original text forms when the simulation
program starts.
User Interfaces
The primary purpose of user interfaces is to make the internals of the model visible to the user,
to control the simulation execution, and possibly allow the user to intervene by changing
variables/objects inside the model. This is very important in the development/debugging
phase of the simulation project. Equally important, a hands-on experience allows the user to
get a feel of the model’s behavior. The graphical user interface can also be used to demonstrate
a model’s operation.
The same simulation model can be executed with various user interfaces, with no change in
the model files themselves. The user would typically test and debug the simulation with a
powerful graphical user interface, and finally run it with a simple, fast user interface that
supports batch execution.
7
OMNeT++ Simulation Manual – Overview
Component Libraries
Module types can be stored in files separate from the place of their actual use, enabling the
user to group existing module types and create component libraries.
A simulation executable can store several independent models that use the same set of simple
modules. The user can specify in the configuration file which model is to be run. This allows
one to build one large executable that contains several simulation models, and distribute it as
a standalone simulation tool. The flexibility of the topology description language also supports
this approach.
8
OMNeT++ Simulation Manual – Overview
...
test/ Regression test suite
core/ tests for the simulation library
anim/ tests for graphics and animation
dist/ tests for the built-in distributions
makemake/ tests for opp_makemake
...
The Windows version of OMNeT++ contains a redistribution of the MinGW gcc compiler, to-
gether with a copy of MSYS that provides Unix tools commonly used in Makefiles. The MSYS
directory also contains various 3rd party open-source libraries needed to compile and run
OMNeT++.
9
OMNeT++ Simulation Manual – Overview
10
OMNeT++ Simulation Manual – The NED Language
Chapter 3
Component-Based. Simple modules and compound modules are inherently reusable, which
not only reduces code copying, but more importantly, allows component libraries like the
INET Framework to exist.
Interfaces. Module and channel interfaces can be used as placeholders instead of specific
module or channel types. The concrete module or channel type is determined at network
setup time using a parameter. Concrete module types must “implement” the interface
they substitute. For example, a compound module type called MobileHost may contain
a mobility submodule of type IMobility, where IMobility is a module interface. The
actual type of mobility can be chosen from the module types that implement IMobility
(such as RandomWalkMobility, TurtleMobility, etc.).
Inheritance. Modules and channels can be subclassed, with derived modules and channels
being able to add new parameters, gates, and (in the case of compound modules) sub-
modules and connections. Existing parameters can be set to specific values, and the
gate size of a gate vector can also be set. This allows, for example, taking a Gener-
icTcpClientApp module and deriving a FileTransferApp from it by setting certain
parameters to fixed values.
Packages. The NED language features a Java-like package structure to reduce the risk of
name clashes between different models. Additionally, a NEDPATH (similar to Java’s
CLASSPATH) has been introduced to facilitate the specification of dependencies among
simulation models.
11
OMNeT++ Simulation Manual – The NED Language
Inner types. Channel types and module types used locally within a compound module can
be defined within the compound module itself to minimize namespace pollution.
Metadata annotations. Module or channel types, parameters, gates, and submodules can be
annotated with properties. Metadata is not used directly by the simulation kernel, but
it can provide additional information to various tools, the runtime environment, or even
other modules in the model. For example, metadata annotations can specify a module’s
graphical representation (such as an icon) or the prompt string and measurement unit
(such as milliwatt) of a parameter.
The NED language has an abstract syntax tree representation that can be serialized to XML.
NED files can be converted to XML and back without any data loss, including comments.
This makes it easier to programmatically manipulate NED files. For example, information can
be extracted, refactored, and transformed, NED can be generated from data stored in other
systems like SQL databases, and so on.
NOTE: This chapter will gradually explain the NED language through examples. A more
formal and concise treatment can be found in Appendix B.
First, we define the network and then, in the next sections, we continue to define the network
nodes.
Let the network topology be as shown in Figure 3.1.
The corresponding NED description would be as follows:
//
// A network
//
network Network
{
submodules:
node1: Node;
node2: Node;
node3: Node;
...
connections:
node1.port++ <--> {datarate=100Mbps;} <--> node2.port++;
node2.port++ <--> {datarate=100Mbps;} <--> node4.port++;
node4.port++ <--> {datarate=100Mbps;} <--> node6.port++;
...
}
12
OMNeT++ Simulation Manual – The NED Language
The above code defines a network type named Network. Note that the NED language uses the
customary curly brace syntax and // to denote comments.
NOTE: Comments in NED not only enhance the readability of the source code, but also
appear at various places (tooltips, content assist, etc) in the OMNeT++ IDE and become
part of the documentation extracted from the NED files. The NED documentation system,
similar to JavaDoc or Doxygen, will be described in Chapter 14.
The network contains several nodes named node1, node2, etc. from the NED module type
Node. We will define Node in the following sections.
The second half of the declaration specifies how the nodes are connected. The double arrow
represents a bidirectional connection. The connection points of modules are called gates, and
the notation port++ adds a new gate to the port[] gate vector. Gates and connections will
be discussed in more detail in sections 3.7 and 3.9. The nodes are connected with a channel
that has a data rate of 100Mbps.
NOTE: In many other systems, the equivalent of OMNeT++ gates are called ports. We
have chosen to retain the term gate to avoid confusion with other uses of the word port:
router port, TCP port, I/O port, etc.
The above code would be placed in a file named Net6.ned. It is conventional to put each NED
definition in its own file and name the file accordingly, but it is not mandatory.
Any number of networks can be defined in the NED files, and for each simulation, the user
needs to specify which network to set up. The usual way to specify the network is to include
the network option in the configuration (usually the omnetpp.ini file):
[General]
network = Network
13
OMNeT++ Simulation Manual – The NED Language
It is inconvenient to repeat the data rate for every connection. Fortunately, NED provides a
convenient solution: it allows the creation of a new channel type that encapsulates the data
rate setting. This channel type can be defined inside the network so that it does not clutter
the global namespace.
The improved network would look like this:
//
// A Network
//
network Network
{
types:
channel C extends ned.DatarateChannel {
datarate = 100Mbps;
}
submodules:
node1: Node;
node2: Node;
node3: Node;
...
connections:
node1.port++ <--> C <--> node2.port++;
node2.port++ <--> C <--> node4.port++;
node4.port++ <--> C <--> node6.port++;
...
}
Later sections will cover the concepts used (inner types, channels, the DatarateChannel
built-in type, inheritance) in detail.
Simple modules are the basic building blocks for other (compound) modules, denoted by
the simple keyword. All active behavior in the model is encapsulated in simple modules.
Behavior is defined by a C++ class; NED files only declare the externally visible interface of
the module (gates, parameters).
In our example, we could define Node as a simple module. However, its functionality is quite
complex (such as traffic generation, routing, etc.), so it is better to implement it with several
smaller simple module types. We will assemble these modules into a compound module. We
will have one simple module for traffic generation (App), one for routing (Routing), and one
for queueing up packets to be sent out (Queue). For brevity, we omit the bodies of the latter
two in the following code.
simple App
{
parameters:
int destAddress;
...
@display("i=block/browser");
gates:
14
OMNeT++ Simulation Manual – The NED Language
input in;
output out;
}
simple Routing
{
...
}
simple Queue
{
...
}
According to convention, the above simple module declarations go into App.ned, Routing.ned,
and Queue.ned files.
NOTE: Note that module type names (App, Routing, Queue) begin with a capital letter,
while parameter and gate names begin with lowercase. This is the recommended naming
convention. Capitalization matters because the language is case-sensitive.
Let’s consider the first simple module type declaration. App has a parameter called destAd-
dress (with others omitted for now) and two gates named out and in for sending and receiving
application packets.
The argument of @display() is called a display string, which defines the rendering of the
module in graphical environments. In @display("i=..."), "i=..." defines the default icon.
In general, attributes starting with @ like @display are called properties in NED. They are
used to annotate various objects with metadata. Properties can be attached to files, mod-
ules, parameters, gates, connections, and other objects, and parameter values have a flexible
syntax.
Now we can assemble App, Routing, and Queue into the compound module Node. A com-
pound module can be thought of as a “cardboard box” that groups other modules into a larger
unit, which can further be used as a building block for other modules. Networks are also a
kind of compound module.
module Node
{
parameters:
int address;
@display("i=misc/node_vs,gold");
gates:
inout port[];
submodules:
app: App;
routing: Routing;
queue[sizeof(port)]: Queue;
connections:
routing.localOut --> app.in;
15
OMNeT++ Simulation Manual – The NED Language
Compound modules, like simple modules, may have parameters and gates. Our Node module
contains an address parameter and a gate vector named port of unspecified size. The actual
gate vector size will be determined implicitly by the number of neighbors when we create a
network from nodes of this type. The type of port[] is inout, which allows bidirectional
connections.
The modules that make up the compound module are listed under submodules. Our Node
compound module type has an app and a routing submodule, plus a queue[] submodule
vector that contains one Queue module for each port, as specified by [sizeof(port)]. (Re-
ferring to [sizeof(port)] is allowed because the network is built in a top-down order, and
the node is already created and connected at the network level when its submodule structure
is built out.)
In the connections section, the submodules are connected to each other and to the parent
module. Single arrows are used to connect input and output gates, while double arrows con-
nect inout gates. A for loop is utilized to connect the routing module to each queue module
and to connect the outgoing/incoming link (line gate) of each queue to the corresponding
port of the enclosing module.
We have created the NED definitions for this example, but how are they used by OMNeT++?
When the simulation program is started, it loads the NED files. The program should already
include the C++ classes that implement the required simple modules, App, Routing, and
Queue. The C++ code for these modules is either part of the executable or loaded from a shared
library. The simulation program also loads the configuration (omnetpp.ini) and determines
from it that the simulation model to be run is the Network network. Then, the network is
16
OMNeT++ Simulation Manual – The NED Language
***
In the following sections, we will delve deeper into the elements of the NED language and
examine them in greater detail.
Both the parameters and gates sections are optional, that is, they can be left out if there are
no parameters or gates. In addition, the parameters keyword itself is optional too; it can be
left out even if there are parameters or properties.
Note that the NED definition doesn’t contain any code to define the operation of the module:
that part is expressed in C++. By default, OMNeT++ looks for C++ classes of the same name
as the NED type (so here, Queue).
One can explicitly specify the C++ class with the @class property. Classes with namespace
qualifiers are also accepted, as shown in the following example that uses the mylib::Queue
class:
simple Queue
{
parameters:
int capacity;
@class(mylib::Queue);
@display("i=block/queue");
gates:
input in;
output out;
}
17
OMNeT++ Simulation Manual – The NED Language
If there are several modules whose C++ implementation classes are in the same namespace,
a better alternative to @class is the @namespace property. The C++ namespace given with
@namespace will be prepended to the normal class name. In the following example, the C++
classes will be mylib::App, mylib::Router and mylib::Queue:
@namespace(mylib);
simple App {
...
}
simple Router {
...
}
simple Queue {
...
}
The @namespace property may not only be specified at the file level as in the above example,
but for packages as well. When placed in a file called package.ned, the namespace will apply
to all components in that package and below.
The implementation C++ classes need to be subclassed from the cSimpleModule library class;
chapter 4 of this manual describes in detail how to write them.
Simple modules can be extended (or specialized) via subclassing. The motivation for subclass-
ing can be to set some open parameters or gate sizes to a fixed value (see 3.6 and 3.7), or to
replace the C++ class with a different one. Now, by default, the derived NED module type will
inherit the C++ class from its base, so it is important to remember that you need to write out
@class if you want it to use the new class.
The following example shows how to specialize a module by setting a parameter to a fixed
value (and leaving the C++ class unchanged):
simple Queue
{
int capacity;
...
}
In the next example, the author wrote a PriorityQueue C++ class, and wants to have a
corresponding NED type, derived from Queue. However, it does not work as expected:
simple PriorityQueue extends Queue // wrong! still uses the Queue C++ class
{
}
The correct solution is to add a @class property to override the inherited C++ class:
simple PriorityQueue extends Queue
18
OMNeT++ Simulation Manual – The NED Language
{
@class(PriorityQueue);
}
A compound module groups other modules into a larger unit. A compound module may have
gates and parameters like a simple module, but no active behavior is associated with it.1
NOTE: When there is a temptation to add code to a compound module, then encapsulate
the code into a simple module, and add it as a submodule.
A compound module declaration may contain several sections, all of them optional:
module Host
{
types:
...
parameters:
...
gates:
...
submodules:
...
connections:
...
}
Modules contained in a compound module are called submodules, and they are listed in the
submodules section. One can create arrays of submodules (i.e. submodule vectors), and the
submodule type may come from a parameter.
Connections are listed under the connections section of the declaration. One can create
connections using simple programming constructs (loop, conditional). Connection behavior
can be defined by associating a channel with the connection; the channel type may also come
from a parameter.
Module and channel types only used locally can be defined in the types section as inner
types, so that they do not pollute the namespace.
Compound modules may be extended via subclassing. Inheritance may add new submodules
and new connections as well, not only parameters and gates. Also, one may refer to inherited
submodules, inherited types, etc. What is not possible is to "de-inherit" or modify submodules
or connections. 2
1 Although the C++ class for a compound module can be overridden with the @class property, this is a feature that
should probably never be used. Encapsulate the code into a simple module, and add it as a submodule.
2 With one exception: Since OMNeT++ version 5.6, reconnecting existing gates is possible using the reconnect
19
OMNeT++ Simulation Manual – The NED Language
In the following example, we show how to assemble common protocols into a “stub” for wire-
less hosts, and add user agents via subclassing.3
module WirelessHostBase
{
gates:
input radioIn;
submodules:
tcp: TCP;
ip: IP;
wlan: Ieee80211;
connections:
tcp.ipOut --> ip.tcpIn;
tcp.ipIn <-- ip.tcpOut;
ip.nicOut++ --> wlan.ipIn;
ip.nicIn++ <-- wlan.ipOut;
wlan.radioIn <-- radioIn;
}
The WirelessHost compound module can further be extended, for example with an Ethernet
port:
module DesktopHost extends WirelessHost
{
gates:
inout ethg;
submodules:
eth: EthernetNic;
connections:
ip.nicOut++ --> eth.ipIn;
ip.nicIn++ <-- eth.ipOut;
eth.phy <--> ethg;
}
3.5 Channels
Channels encapsulate parameters and behavior associated with connections. Channels are
like simple modules, in the sense that there are C++ classes behind them. The rules for
finding the C++ class for a NED channel type are the same as with simple modules: the
3 Module types, gate names, etc. used in the examples are fictional, not based on an actual OMNeT++-based model
framework
20
OMNeT++ Simulation Manual – The NED Language
default class name is the NED type name unless there is a @class property (@namespace is
also recognized), and the C++ class is inherited when the channel is subclassed.
Thus, the following channel type would expect a CustomChannel C++ class to be present:
channel CustomChannel // requires a CustomChannel C++ class
{
}
The practical difference compared to modules is that one rarely needs to write a custom
channel C++ class because there are predefined channel types that one can subclass from,
inheriting their C++ code. The predefined types are: ned.IdealChannel, ned.DelayChannel,
and ned.DatarateChannel. (“ned” is the package name; one can get rid of it by importing the
types with the import ned.* directive. Packages and imports are described in section 3.14.)
IdealChannel has no parameters and lets all messages through without delay or any side
effect. A connection without a channel object and a connection with an IdealChannel behave
in the same way. Still, IdealChannel has its uses, for example, when a channel object is
required so that it can carry a new property or parameter that is going to be read by other
parts of the simulation model.
DelayChannel has two parameters:
• delay is a double parameter that represents the propagation delay of the message.
Values need to be specified together with a time unit (s, ms, us, etc.)
• disabled is a Boolean parameter that defaults to false; when set to true, the channel
object will drop all messages.
• datarate is a double parameter that represents the data rate of the channel. Values
need to be specified in bits per second or its multiples as a unit (bps, kbps, Mbps, Gbps,
etc.) Zero is treated specially and results in zero transmission duration, i.e. it stands
for infinite bandwidth. Zero is also the default. Data rate is used for calculating the
transmission duration of packets.
• ber and per stand for Bit Error Rate and Packet Error Rate and allow basic error mod-
eling. They expect a double in the [0, 1] range. When the channel decides (based on
random numbers) that an error occurred during the transmission of a packet, it sets an
error flag in the packet object. The receiver module is expected to check the flag and
discard the packet as corrupted if it is set. The default ber and per are zero.
NOTE: There is no channel parameter that specifies whether the channel delivers the
message object to the destination module at the end or at the start of the reception; that
is decided by the C++ code of the target simple module. See the setDeliverOnRecep-
tionStart() method of cGate.
The following example shows how to create a new channel type by specializing Datarate-
Channel:
channel Ethernet100 extends ned.DatarateChannel
{
datarate = 100Mbps;
delay = 100us;
21
OMNeT++ Simulation Manual – The NED Language
ber = 1e-10;
}
NOTE: The three built-in channel types are also used for connections where the channel
type is not explicitly specified.
One may add parameters and properties to channels via subclassing and may modify existing
ones. In the following example, we introduce distance-based calculation of the propagation
delay:
channel DatarateChannel2 extends ned.DatarateChannel
{
double distance @unit(m);
delay = this.distance / 200000km * 1s;
}
Parameters are primarily intended to be read by the underlying C++ class, but new parameters
may also be added as annotations to be used by other parts of the model. For example, a cost
parameter may be used for routing decisions in the routing module, as shown in the example
below. The example also shows annotation using properties (@backbone).
channel Backbone extends ned.DatarateChannel
{
@backbone;
double cost = default(1);
}
3.6 Parameters
Parameters are variables that belong to a module. Parameters can be used in building the
topology (number of nodes, etc), and to supply input to C++ code that implements simple
modules and channels.
Parameters can be of type double, int, bool, string, xml, and object; they can also be
declared volatile. For the numeric types, a unit of measurement can also be specified
(@unit property).
Parameters can get their value from NED files or from the configuration (omnetpp.ini). A
default value can also be given (default(...)), which is used if the parameter is not otherwise
assigned.
The following example shows a simple module that has five parameters, three of which have
default values:
simple App
{
parameters:
string protocol; // protocol to use: "UDP" / "IP" / "ICMP" / ...
int destAddress; // destination address
volatile double sendInterval @unit(s) = default(exponential(1s));
// time between generating packets
volatile int packetLength @unit(byte) = default(100B);
22
OMNeT++ Simulation Manual – The NED Language
Parameters may get their values in several ways: from NED code, from the configuration
(omnetpp.ini), or even interactively from the user. NED lets one assign parameters at several
places: in subclasses via inheritance; in submodule and connection definitions where the
NED type is instantiated; and in networks and compound modules that directly or indirectly
contain the corresponding submodule or connection.
For instance, one could specialize the above App module type via inheritance with the following
definition:
simple PingApp extends App
{
parameters:
protocol = "ICMP/ECHO"
sendInterval = default(1s);
packetLength = default(64byte);
}
This definition sets the protocol parameter to a fixed value ("ICMP/ECHO"), and changes the
default values of the sendInterval and packetLength parameters. protocol is now locked
down in PingApp, and its value cannot be modified via further subclassing or other ways.
sendInterval and packetLength are still unassigned here, and only their default values
have been overwritten.
Now, let us see the definition of a Host compound module that uses PingApp as submodule:
module Host
{
submodules:
ping : PingApp {
packetLength = 128B; // always ping with 128-byte packets
}
...
}
This definition sets the packetLength parameter to a fixed value. It is now hardcoded that
Hosts send 128-byte ping packets; this setting cannot be changed from NED or the configu-
ration.
It is not only possible to set a parameter from the compound module that contains the sub-
module, but also from modules higher up in the module tree. A network that employs several
Host modules could be defined like this:
network Network
{
23
OMNeT++ Simulation Manual – The NED Language
submodules:
host[100]: Host {
ping.timeToLive = default(3);
ping.destAddress = default(0);
}
...
}
Parameter assignment can also be placed into the parameters block of the parent compound
module, which provides additional flexibility. The following definition sets up the hosts so
that half of them ping host #50, and the other half ping host #0:
network Network
{
parameters:
host[*].ping.timeToLive = default(3);
host[0..49].ping.destAddress = default(50);
host[50..].ping.destAddress = default(0);
submodules:
host[100]: Host;
...
}
Note the use of asterisk to match any index, and .. to match index ranges.
If there were a number of individual hosts instead of a submodule vector, the network defini-
tion could look like this:
network Network
{
parameters:
host*.ping.timeToLive = default(3);
host{0..49}.ping.destAddress = default(50);
host{50..}.ping.destAddress = default(0);
submodules:
host0: Host;
host1: Host;
host2: Host;
...
host99: Host;
}
An asterisk matches any substring not containing a dot, and a .. within a pair of curly braces
matches a natural number embedded in a string.
In most assignments we have seen above, the left hand side of the equal sign contained a dot
and often a wildcard as well (asterisk or numeric range); we call these assignments pattern
assignments or deep assignments.
There is one more wildcard that can be used in pattern assignments, and this is the double
asterisk; it matches any sequence of characters including dots, so it can match multiple path
elements. An example:
network Network
24
OMNeT++ Simulation Manual – The NED Language
{
parameters:
**.timeToLive = default(3);
**.destAddress = default(0);
submodules:
host0: Host;
host1: Host;
...
}
Note that some assignments in the above examples changed default values, while others set
parameters to fixed values. Parameters that received no fixed value in the NED files can be
assigned from the configuration (omnetpp.ini).
IMPORTANT: A non-default value assigned from NED cannot be overwritten later in NED
or from ini files; it becomes “hardcoded” as far as ini files and NED usage are concerned.
A parameter can be assigned in the configuration using a similar syntax as NED pattern
assignments (actually, it would be more historically accurate to say it the other way round,
that NED pattern assignments use a similar syntax to ini files):
Network.host[*].ping.sendInterval = 500ms # for the host[100] example
Network.host*.ping.sendInterval = 500ms # for the host0,host1,... example
**.sendInterval = 500ms
One often uses the double asterisk to save typing. One can write
**.ping.sendInterval = 500ms
Or if one is certain that only ping modules have sendInterval parameters, the following will
suffice:
**.sendInterval = 500ms
If there is no assignment for a parameter in NED or in the ini file, the default value (given
with =default(...) in NED) will be applied implicitly. If there is no default value, the user
will be asked, provided the simulation program is allowed to do that; otherwise there will be
an error. (Interactive mode is typically disabled for batch executions where it would do more
harm than good.)
It is also possible to explicitly apply the default (this can sometimes be useful):
**.sendInterval = default
Finally, one can explicitly ask the simulator to prompt the user interactively for the value
(again, provided that interactivity is enabled; otherwise this will result in an error):
**.sendInterval = ask
25
OMNeT++ Simulation Manual – The NED Language
NOTE: How can one decide whether to assign a parameter from NED or from an ini
file? The advantage of ini files is that they allow a cleaner separation of the model and
experiments. NED files (together with C++ code) are considered to be part of the model
and to be more or less constant. Ini files, on the other hand, are for experimenting with
the model by running it several times with different parameters. Thus, parameters that
are expected to change (or make sense to be changed) during experimentation should be
put into ini files.
3.6.2 Expressions
Parameter values may be given with expressions. NED language expressions have a C-like
syntax, with additions like quantities (numbers with measurement units, e.g., 100Gbps) and
JSON constructs. Compared to C, there are some variations on operator names: binary
and logical XOR are # and ##, while ˆ has been reassigned to power-of instead. The +
operator does string concatenation as well as numeric addition. There are two extra operators:
<=> (“spaceship”) and =∼ (string match). The JSON constructs are the array and the object
syntaxes, which will be covered in section 3.6.8. Keyword constants include true, false,
nan (floating-point Not-a-Number), inf (infinity), null and its synonym nullptr, and also
undefined which represents the missing value.
The spaceship operator <=> compares its two arguments and returns the result (“less”, “equal”,
“greater” and “not applicable”) in the form of a negative, zero, positive or nan double number,
respectively.
2 <=> 2 // --> 0
10 <=> 5 // --> 1
2 <=> nan // --> nan
The string match operator =∼ is used as string =∼ pattern, and returns a boolean that indi-
cates whether if the second argument (the pattern) matches the first one (the string). Pattern
syntax and rules are similar to those used in omnetpp.ini files: case sensitive, full-string
match, where an asterisk * matches zero or more of any character except dot, and a double
asterisk ** matches zero or more characters (including dot), and other notations also exist to
express embedded numbers and square-bracketed numeric indices within a numeric range.
"foo" =~ "f*" // --> true
"foo" =~ "b*" // --> false
"foo" =~ "F*" // --> false
"foo.bar.baz" =~ "*.baz" // --> false
"foo.bar.baz" =~ "**.baz" // --> true
"foo[15]" =~ "foo[5..20]" // --> true
"foo15" =~ "foo{5..20}" // --> true
Expressions may refer to module parameters, gate vector and module vector sizes (using the
sizeof operator), existence of a submodule or submodule vector (exists operator), and the
index of the current module in a submodule vector (index).
The special operator expr() can be used to pass a formula into a module as a parameter
(3.6.9).
Expressions may also utilize various numeric, string, stochastic, and miscellaneous other
functions (fabs(), uniform(), lognormal(), etc.).
NOTE: The list of NED functions can be found in Appendix D. The user can also extend
NED with new functions.
26
OMNeT++ Simulation Manual – The NED Language
Expressions may refer to parameters of the compound module being defined, parameters of
the current module, and parameters of already defined submodules, with the syntax submod-
ule.parametername (or submodule[index].parametername).
Unqualified parameter names refer to a parameter of the compound module, wherever it oc-
curs within the compound module definition. For example, all foo references in the following
example refer to the network’s foo parameter.
network Network
{
parameters:
double foo;
double bar = foo;
submodules:
node[10]: Node {
baz = foo;
}
...
}
Use the this qualifier to refer to another parameter of the same submodule.
submodules:
node: Node {
datarate = this.amount / this.duration;
}
From OMNeT++ 5.7 onwards, there is also a parent qualifier with the obvious meaning.
NOTE: The interpretation of names which are not qualified with either this or parent
and occur within submodule/channel blocks is going to change in OMNeT++ 6.0: An
unqualified name foo is going to refer to the parameter of the submodule itself, i.e., will
be interpreted as this.foo. To create NED files which are compatible with both versions,
make those parameter references explicit by using the parent qualifier: parent.foo. A
similar rule applies to the arguments of sizeof and exists.
Volatile parameters are those marked with the volatile modifier keyword. Normally, expres-
sions assigned to parameters are evaluated once, and the resulting values are stored in the
parameters. In contrast, a volatile parameter holds the expression itself, and it is evaluated
every time the parameter is read. Therefore, if the expression contains a stochastic or chang-
ing component, such as normal(0,1) (a random value from the unit normal distribution) or
simTime() (the current simulation time), reading the parameter may yield a different value
every time.
NOTE: Technically, non-volatile parameters may also contain stochastic values. How-
ever, the result of that would be that the simulation use a constant value throughout,
chosen randomly at the beginning of the simulation. This is akin to running a randomly
selected simulation rather than performing a Monte-Carlo simulation, hence, it is rarely
desirable.
27
OMNeT++ Simulation Manual – The NED Language
If a parameter is marked volatile, the C++ code that implements the corresponding module
is expected to re-read the parameter every time a new value is needed, as opposed to reading
it once and caching the value in a variable.
To demonstrate the use of volatile, suppose we have a Queue simple module that has a
volatile double parameter named serviceTime.
simple Queue
{
parameters:
volatile double serviceTime;
}
Because of the volatile modifier, the C++ code underlying the queue module is supposed
to read the serviceTime parameter for every job serviced. Thus, if a stochastic value like
uniform(0.5s, 1.5s) is assigned to the parameter, the expression will be evaluated every
time, and every job will likely have a different, random service time.
As another example, here’s how one can have a time-varying parameter by exploiting the
simTime() NED function:
**.serviceTime = simTime()<1000s ? 1s : 2s # queue that slows down after 1000s
A parameter is marked as mutable by adding the @mutable property to it. Mutable parameters
can be set to a different value during runtime, whereas normal, i.e., non-mutable parameters
cannot be changed after their initial assignment (attempts to do so will result in an error being
raised).
Parameter mutability addresses the fact that although it would be technically possible to allow
changing the value of any parameter to a different value during runtime, it only really makes
sense to do so if the change actually takes effect. Otherwise, users doing the change could be
mislead.
For example, if a module is implemented in C++ in a way that it only reads a parameter once
and then uses the cached value throughout, it would be misleading to allow changing the
parameter’s value during simulation. For a parameter to rightfully be marked as @mutable,
module’s implementation has to be explicitly prepared to handle runtime parameter changes
(see section 4.5.7).
As a practical example, a drop-tail queue module could have a maxLength parameter which
controls the maximum number of elements the queue can hold. If it was allowed to set
the maxLength parameter to a different value at runtime but the module would continue to
operate according to the initially configured value throughout the entire simulation, that could
falsify simulation results.
simple Queue
{
parameters:
int maxLength @mutable; // @mutable indicates that Queue's
// implementation is prepared for handling
// runtime changes in the value of the
// maximum queue length.
...
}
28
OMNeT++ Simulation Manual – The NED Language
In a model framework that contains a large number of modules with many parameters, the
presence or absence of @mutable allows the user to know which are the parameters whose
runtime changes are properly handled by their modules. This is an important input for deter-
mining what kinds of experiments can be done with the model.
HINT: Note that although volatile and @mutable are two different things, parameters
marked volatile may often be marked @mutable as well.
3.6.6 Units
One can declare a parameter to have an associated unit of measurement by adding the @unit
property. An example:
simple App
{
parameters:
volatile double sendInterval @unit(s) = default(exponential(350ms));
volatile int packetLength @unit(byte) = default(4KiB);
...
}
The @unit(s) and @unit(byte) declarations specify the measurement unit for the param-
eter. Values assigned to parameters must have the same or compatible unit, i.e., @unit(s)
accepts milliseconds, nanoseconds, minutes, hours, etc., and @unit(byte) accepts kilobytes,
megabytes, etc., as well.
NOTE: The list of units accepted by OMNeT++ is listed in the Appendix, see A.5.11.
Unknown units (bogomips, etc.) can also be used, but there are no conversions for them,
i.e., decimal prefixes will not be recognized.
The OMNeT++ runtime does a full and rigorous unit check on parameters to ensure "unit
safety" of models. Constants should always include the measurement unit.
The @unit property of a parameter cannot be added or overridden in subclasses or in sub-
module declarations.
OMNeT++ supports two explicit ways of passing structured data to a module using parame-
ters: XML parameters and object parameters with JSON-style structured data. This section
describes the former, and the next one the latter.
XML parameters are declared with the keyword xml. When using XML parameters, OMNeT++
will read the XML document for you, validate it against its DTD (if it contains one), and present
the contents in a DOM-like object tree. It is also possible to assign a part (i.e., a subtree) of
29
OMNeT++ Simulation Manual – The NED Language
the document to the parameter; the subset can be selected using an XPath-subset notation.
OMNeT++ caches the content of the document, so it is loaded only once even if it is referenced
by multiple parameters.
Values for an XML parameter can be produced using the xmldoc() and the xml() functions.
xmldoc() accepts a filename as an argument, while xml() parses its string argument as XML
content. Of course, one can assign xml parameters both from NED and from omnetpp.ini.
The following example declares an xml parameter and assigns the contents of an XML file to
it. The file name is understood as being relative to the working directory.
simple TrafGen {
parameters:
xml profile;
gates:
output out;
}
module Node {
submodules:
trafGen1 : TrafGen {
profile = xmldoc("data.xml");
}
...
}
xmldoc() also lets one select an element within an XML document. In case a simulation
model contains numerous modules that need XML input, this feature allows the user to get
rid of many small XML files by aggregating them into a single XML file. For example, the
following XML file contains two profiles identified with the IDs gen1 and gen2:
<?xml>
<root>
<profile id="gen1">
<param>1</param>
<param>3</param>
</profile>
<profile id="gen2">
<param>9</param>
</profile>
</root>
And one can assign each profile to a corresponding submodule using an XPath-like expres-
sion:
module Node {
submodules:
trafGen1 : TrafGen {
profile = xmldoc("all.xml", "/root/profile[@id='gen1']");
}
trafGen2 : TrafGen {
profile = xmldoc("all.xml", "/root/profile[@id='gen2']");
}
}
30
OMNeT++ Simulation Manual – The NED Language
The following example shows how to specify XML content using a string literal with the xml()
function. This is especially useful for specifying a default value.
simple TrafGen {
parameters:
xml profile = xml("<root/>"); // empty document as default
...
}
The xml() function, like xmldoc(), also supports an optional second XPath parameter for
selecting a subtree.
Object parameters are declared with the keyword object. The values of object parameters are
C++ objects, which can hold arbitrary data and can be constructed in various ways in NED.
Although object parameters were introduced in OMNeT++ only in version 6.0, they are now
the preferred way of passing structured data to modules.
There are two basic constructs in NED for creating objects: the array and the object syntax.
The array syntax is a pair of square brackets that encloses the list of comma-separated array
elements: [ value1, value2, ... ]. The object (a.k.a. dictionary) syntax uses curly braces around
key-value pairs, with the separators being colon and comma: { key1 : value1, key2 : value2,
... }. These constructs can be composed, so an array may contain objects and further arrays
as elements, and similarly, an object may contain arrays and further objects as values, and
so on. This allows describing complex data structures, with a JSON-like notation.
The notation is only JSON-like, as the syntax rules are more relaxed than in JSON. All valid
JSON is accepted, but also more. The main difference is that in JSON, values in arrays and
objects may only be constants or null, while OMNeT++ allows NED expressions as values:
quantities, nan/inf, parameter references, functions, arithmetic operations, etc., are all ac-
cepted. Also, unlike strict JSON, NED allows quotation marks around object keys to be left
out, as long as the key complies with the identifier syntax.
Another extension is that for objects, the desired C++ class may be specified in front of the
open curly brace: classname { key1 : value1, ... }. The object will be created and filled in using
OMNeT++’s reflection features. This allows internal data structures of modules to be filled out
directly, eliminating most of the “parsing” code which is otherwise necessary. More about this
feature will be written in the chapter about C++ programming (section 4.5.3).
Object parameters with JSON-style values obsolete several workarounds that were used in
pre-6.0 OMNeT++ versions for passing structured data to modules, such as using strings
to specify numeric arrays or using text files of ad-hoc syntax as configuration or data files.
JSON-style values are also more convenient than XML input.
After this introduction, let’s see some examples! We begin with a list of completely made-up
object parameter assignments to show the syntax and possibilities:
simple Example {
parameters:
object array1 = []; // empty array
object array2 = [2, 5, 3, -1]; // array of integers
object array3 = [ 3, 24.5mW, "Hello", false, true ]; // misc array
object array4 = [ nan, inf, inf s, null, nullptr ]; // special values
object object1 = {}; // empty object
31
OMNeT++ Simulation Manual – The NED Language
// default values
object default1 = default([]); // empty array by default
object default2 = default({}); // empty object by default
object default3 = default([1,2,3]); // some array by default
object default4 = default(nullptr); // null pointer by default
}
The following, more practical example demonstrates how one could describe an IPv4 routing
table. Each route is represented as an object, and the table itself is represented as an array
of routes.
object routes = [
{ dest: "10.0.0.0", netmask: "255.255.0.0", interf: "eth0", metric:10 },
{ dest: "10.1.0.0", netmask: "255.255.0.0", interf: "eth1", metric:20 },
{ dest: "*", interf: "eth2" },
];
The next example shows the use of the extended object syntax for specifying a "template" for
the packets that a traffic source module should generate. Note the stochastic expression for
the byteLength field, and that the parameter is declared as volatile. Every time the module
needs to send a packet, its C++ code should read the packetToSend parameter, which will
cause the expression to be evaluated and a new packet of random length to be created that
the module can send.
simple TrafficSource {
parameters:
volatile object packetToSend = default(cPacket {
name: "data",
kind: 10,
byteLength: intuniform(64,4096)
});
volatile double sendInterval @unit(s) = default(exponential(100ms));
}
Another traffic source module that supports a predetermined schedule of what to send at
which points in time could have the following parameter to describe the schedule:
object sendSchedule = [
{ time: 1s, pk: cPacket { name: "pk1", byteLength: 64 } },
{ time: 2s, pk: cPacket { name: "pk2", byteLength: 76 } },
{ time: 3s, pk: cPacket { name: "pk3", byteLength: 32 } },
32
OMNeT++ Simulation Manual – The NED Language
];
In the next example, we want to pass a trail given with its waypoints to a module. The module
will get the data in an instance of a Trail C++ class expressly created for this purpose.
This means that the module will get the trail data in a ready-to-use form just by reading the
parameter, without having to do any parsing or additional processing.
We use a message file (chapter 5) to define the classes; the C++ classes will be automatically
generated by OMNeT++ from it.
// file: Trail.msg
struct Point {
double x;
double y;
}
Values for object parameters may also be placed in ini files, just like values for other parameter
types. In ini files, indented lines are treated as continuations of the previous line, so the above
example doesn’t need trailing backslashes when moved to omnetpp.ini:
**.trail = Trail {
waypoints: [
{ x: 1, y : 5 },
{ x: 4, y : 6 },
{ x: 3, y : 8 },
{ x: 5, y : 3 }
]
}
The special operator expr() allows one to pass a formula into a module as a parameter.
expr() takes an expression as an argument, which syntactically must correspond to the
general syntax of NED expressions. However, it is not a normal NED expression: it will not be
interpreted and evaluated as one. Instead, it will be encapsulated into, and returned as, an
object, and typically assigned to a module parameter.
The module may access the object via the parameter and may evaluate the expression encap-
sulated in it any number of times during simulation. While doing so, the module’s code can
33
OMNeT++ Simulation Manual – The NED Language
freely determine how various identifiers and other syntactical elements in the expression are
interpreted.
Let us see a practical example. In the model of a wireless network, one of the tasks is to
compute the path loss suffered by each wirelessly transmitted frame as part of the procedure
to determine whether the frame could be successfully received by the receiver node. There are
several formulas for computing the path loss (free space, two-ray ground reflection, etc.), and
it depends on multiple factors which one to use. If the model author wants to leave it open for
their users to specify the formula they want to use, they might define the model like so:
simple RadioMedium {
parameters:
object pathLoss; // =expr(...): formula to compute path loss
...
}
The pathLoss parameter expects the formula to be given with expr(). The formula is ex-
pected to contain two variables, distance and frequency, which stand for the distance be-
tween the transmitter and the receiver and the packet transmission frequency, respectively.
The module would evaluate the expression for each frame, binding values that correspond to
the current frame to those variables.
Given the above, free space path loss would be specified to the module with the following
formula (assuming isotropic antennas with the same polarization, etc.):
The next example is borrowed from the INET Framework, which extensively uses expr() for
specifying packet filter conditions. A few examples:
expr(hasBitError)
expr(name == 'P1')
expr(name =~ 'P*')
expr(totalLength == 128B)
expr(ipv4.destAddress.str() == '10.0.0.1' && udp.destPort == 42)
The interesting part is that the packet itself does not appear explicitly in the expressions.
Instead, identifiers like hasBitError and name are interpreted as attributes of the packet, as
if the user had written e.g. pk.hasBitError and pk.name. Similarly, ipv4 and udp stand for
the IPv4 and UDP headers of the packet. The last line also shows that the interpretation of
member accesses and method calls is also in the hands of the module’s code.
The details of implementing expr() support in modules will be described as part of the sim-
ulation library, in section 7.8.
3.7 Gates
Gates are the connection points of modules. OMNeT++ has three types of gates: input, output,
and inout, the latter being essentially an input and an output gate glued together.
A gate, whether input or output, can only be connected to one other gate. (For compound
module gates, this means one connection “outside” and one “inside”.) It is possible, though
generally not recommended, to connect the input and output sides of an inout gate separately
(see section 3.9).
34
OMNeT++ Simulation Manual – The NED Language
One can create single gates and gate vectors. The size of a gate vector can be given inside
square brackets in the declaration, but it is also possible to leave it open by just writing a pair
of empty brackets ("[]").
When the gate vector size is left open, one can still specify it later when subclassing the
module or when using the module for a submodule in a compound module. However, it does
not need to be specified because one can create connections with the gate++ operator that
automatically expands the gate vector.
The gate size can be queried from various NED expressions with the sizeof() operator.
NED normally requires that all gates be connected. To relax this requirement, one can anno-
tate selected gates with the @loose property, which turns off the connectivity check for that
gate. Also, input gates that solely exist so that the module can receive messages via send-
Direct() (see 4.7.5) should be annotated with @directIn. It is also possible to turn off the
connectivity check for all gates within a compound module by specifying the allowuncon-
nected keyword in the module’s connections section.
Let us see some examples.
In the following example, the Classifier module has one input for receiving jobs, which it
will send to one of the outputs. The number of outputs is determined by a module parameter:
simple Classifier {
parameters:
int numCategories;
gates:
input in;
output out[numCategories];
}
The following Sink module also has its in[] gate defined as a vector, so that it can be con-
nected to several modules:
simple Sink {
gates:
input in[];
}
The following lines define a node for building a square grid. Gates around the edges of the
grid are expected to remain unconnected; hence, the @loose annotation:
simple GridNode {
gates:
inout neighbour[4] @loose;
}
WirelessNode below is expected to receive messages (radio transmissions) via direct sending,
so its radioIn gate is marked with @directIn.
simple WirelessNode {
gates:
input radioIn @directIn;
}
In the following example, we define TreeNode as having gates to connect any number of
children, then subclass it to get a BinaryTreeNode to set the gate size to two:
35
OMNeT++ Simulation Manual – The NED Language
simple TreeNode {
gates:
inout parent;
inout children[];
}
An example for setting the gate vector size in a submodule, using the same TreeNode module
type as above:
module BinaryTree {
submodules:
nodes[31]: TreeNode {
gates:
children[2];
}
connections:
...
}
3.8 Submodules
Modules that compose a compound module are called its submodules. A submodule has a
name, and it is an instance of a compound or simple module type. In the NED definition of
a submodule, this module type is usually given statically, but it is also possible to specify
the type with a string expression. (The latter feature, parametric submodule types, will be
discussed in section 3.11.1.)
NED also supports submodule arrays (vectors) and conditional submodules. Submodule vec-
tor size, unlike gate vector size, must always be specified and cannot be left open as with
gates.
It is possible to add new submodules to an existing compound module via subclassing; this
has been described in section 3.4.
The basic syntax of submodules is shown below:
module Node
{
submodules:
routing: Routing; // a submodule
queue[sizeof(port)]: Queue; // submodule vector
...
}
As seen in previous code examples, a submodule may also have a curly brace block as a body,
where one can assign parameters, set the size of gate vectors, and add/modify properties like
the display string (@display). It is not possible to add new parameters and gates.
36
OMNeT++ Simulation Manual – The NED Language
Display strings specified here will be merged with the display string from the type to get the
effective display string. The merge algorithm is described in chapter 8.
module Node
{
gates:
inout port[];
submodules:
routing: Routing {
parameters: // this keyword is optional
routingTable = "routingtable.txt"; // assign parameter
gates:
in[sizeof(port)]; // set gate vector size
out[sizeof(port)];
}
queue[sizeof(port)]: Queue {
@display("t=queue id $id"); // modify display string
id = 1000+index; // use submodule index to generate different IDs
}
connections:
...
}
is the same as
queue: Queue {
}
A submodule or submodule vector can be conditional. The if keyword and the condition itself
go after the submodule type, as shown in the example below:
module Host
{
parameters:
bool withTCP = default(true);
submodules:
tcp : TCP if withTCP;
...
}
Note that with submodule vectors, setting a zero vector size can be used as an alternative to
the if condition.
3.9 Connections
Connections are defined in the connections section of compound modules. Connections
cannot span across hierarchy levels; one can connect two submodule gates, a submodule
gate and the "inside" of the parent (compound) module’s gates, or two gates of the parent
37
OMNeT++ Simulation Manual – The NED Language
module (though this is rarely useful), but it is not possible to connect to any gate outside the
parent module, or inside compound submodules.
Input and output gates are connected with a normal arrow, and inout gates with a double-
headed arrow “<-->”. To connect the two gates with a channel, use two arrows and put
the channel specification in between. The same syntax is used to add properties such as
@display to the connection.
Some examples have already been shown in the NED Quickstart section (3.2); let’s see some
more.
It has been mentioned that an inout gate is basically an input and an output gate glued
together. These sub-gates can also be addressed (and connected) individually if needed, as
port$i and port$o (or for vector gates, as port$i[k] and port$o[k]).
Gates are specified as modulespec.gatespec (to connect a submodule), or as gatespec (to con-
nect the compound module). modulespec is either a submodule name (for scalar submodules),
or a submodule name plus an index in square brackets (for submodule vectors). For scalar
gates, gatespec is the gate name; for gate vectors it is either the gate name plus an index in
square brackets, or gatename++.
The gatename++ notation causes the first unconnected gate index to be used. If all gates of
the given gate vector are connected, the behavior is different for submodules and for the en-
closing compound module. For submodules, the gate vector expands by one. For a compound
module, after the last gate is connected, ++ will stop with an error.
NOTE: Why is it not possible to expand a gate vector of the compound module? The
model structure is built in top-down order, so new gates would be left unconnected on
the outside, as there is no way in NED to "go back" and connect them afterwards.
When the ++ operator is used with $i or $o (e.g. g$i++ or g$o++, see later), it will actually
add a gate pair (input+output) to maintain equal gate sizes for the two directions.
When using built-in channel types, the type name can be omitted; it will be inferred from the
parameter names.
a.g++ <--> {delay=10ms;} <--> b.g++;
a.g++ <--> {delay=10ms; ber=1e-8;} <--> b.g++;
a.g++ <--> {@display("ls=red");} <--> b.g++;
38
OMNeT++ Simulation Manual – The NED Language
Naturally, if other parameter names are assigned in a connection without an explicit channel
type, it will be an error (with “ned.DelayChannel has no such parameter” or similar message).
Connection parameters, similarly to submodule parameters, can also be assigned using pat-
tern assignments, although the channel names to be matched with patterns are a little more
complicated and less convenient to use. A channel can be identified with the name of its
source gate plus the channel name; the channel name is currently always channel. It is
illustrated by the following example:
module Queueing
{
parameters:
source.out.channel.delay = 10ms;
queue.out.channel.delay = 20ms;
submodules:
source: Source;
queue: Queue;
sink: Sink;
connections:
source.out --> ned.DelayChannel --> queue.in;
queue.out --> ned.DelayChannel <--> sink.in;
Using bidirectional connections is a bit trickier, because both directions must be covered
separately:
network Network
{
parameters:
hostA.g$o[0].channel.datarate = 100Mbps; // the A -> B connection
hostB.g$o[0].channel.datarate = 100Mbps; // the B -> A connection
hostA.g$o[1].channel.datarate = 1Gbps; // the A -> C connection
hostC.g$o[0].channel.datarate = 1Gbps; // the C -> A connection
submodules:
hostA: Host;
hostB: Host;
hostC: Host;
connections:
hostA.g++ <--> ned.DatarateChannel <--> hostB.g++;
hostA.g++ <--> ned.DatarateChannel <--> hostC.g++;
Also, with the ++ syntax it is not always easy to figure out which gate indices map to the
connections one needs to configure. If connection objects could be given names to override
the default name “channel”, that would make it easier to identify connections in patterns.
This feature is described in the next section.
Normally, it is an error for NED connection to refer to a gate which is already connected. This
behavior can be overridden with the @reconnect property. A syntax example:
a.out --> {@reconnect;} --> b.in;
When a connection with the @reconnect property is encountered by the network builder, it
first checks whether any of the involved gates are connected. If they are, it will unconnect
39
OMNeT++ Simulation Manual – The NED Language
The default name given to channel objects is "channel". Since OMNeT++ 4.3, it is possible
to specify the name explicitly and also to override the default name per channel type. The
purpose of custom channel names is to make addressing easier when channel parameters are
assigned from ini files.
The syntax for naming a channel in a connection is similar to submodule syntax: name: type.
Since both name and type are optional, the colon must be there after name even if type is
missing, in order to remove the ambiguity.
Examples:
r1.pppg++ <--> eth1: EthernetChannel <--> r2.pppg++;
a.out --> foo: {delay=1ms;} --> b.in;
a.out --> bar: --> b.in;
In the absence of an explicit name, the channel name comes from the @defaultname property
of the channel type if that exists.
channel Eth10G extends ned.DatarateChannel like IEth {
@defaultname(eth10G);
}
There’s a catch with @defaultname though: if the channel type is specified with a **.channel-
name.liketype= line in an ini file, then the channel type’s @defaultname cannot be used as
channelname in that configuration line because the channel type would only be known as
a result of using that very configuration line. To illustrate the problem, consider the above
Eth10G channel and a compound module containing the following connection:
r1.pppg++ <--> <> like IEth <--> r2.pppg++;
40
OMNeT++ Simulation Manual – The NED Language
The anomaly can be avoided by using an explicit channel name in the connection, not using
@defaultname, or by specifying the type via a module parameter (e.g. writing <param> like
... instead of <> like ...).
3.10.1 Examples
Chain
Binary Tree
module BinaryTree {
parameters:
41
OMNeT++ Simulation Manual – The NED Language
int height;
submodules:
node[2^height-1]: BinaryTreeNode;
connections allowunconnected:
for i=0..2^(height-1)-2 {
node[i].left <--> node[2*i+1].parent;
node[i].right <--> node[2*i+2].parent;
}
}
Note that not every gate of the modules will be connected. By default, an unconnected gate
produces a run-time error message when the simulation is started, but this error message is
turned off here with the allowunconnected modifier. Consequently, it is the simple modules’
responsibility not to send on an unconnected gate.
Random Graph
Conditional connections can be used to generate random topologies, for example. The follow-
ing code generates a random subgraph of a full graph:
module RandomGraph {
parameters:
int count;
double connectedness; // 0.0<x<1.0
submodules:
node[count]: Node {
gates:
in[count];
out[count];
}
connections allowunconnected:
for i=0..count-1, for j=0..count-1 {
node[i].out[j] --> node[j].in[i]
if i!=j && uniform(0,1)<connectedness;
}
}
Note the use of the allowunconnected modifier here as well, to turn off error messages
produced by the network setup code for unconnected gates.
Several approaches can be used to create complex topologies with a regular structure; three
of them are described below.
This pattern takes a subset of the connections of a full graph. A condition is used to “carve
out” the necessary interconnection from the full graph:
for i=0..N-1, for j=0..N-1 {
42
OMNeT++ Simulation Manual – The NED Language
The RandomGraph compound module (presented earlier) is an example of this pattern, but
the pattern can generate any graph where an appropriate condition(i, j) can be formulated.
For example, when generating a tree structure, the condition would determine whether node
j is a child of node i or vice versa.
Though this pattern is very general, its usage can be prohibitive if the number of nodes N
is high and the graph is sparse (having much fewer than N 2 connections). The following two
patterns do not suffer from this drawback.
The pattern loops through all nodes and creates the necessary connections for each one. It
can be generalized as follows:
for i=0..Nnodes, for j=0..Nconns(i)-1 {
node[i].out[j] --> node[rightNodeIndex(i,j)].in[j];
}
The Hypercube compound module (to be presented later) is a clear example of this approach.
The BinaryTree can also be regarded as an example of this pattern, with the inner j loop being
unrolled.
The applicability of this pattern depends on how easily the rightN odeIndex(i, j) function can
be determined.
This pattern can be used if the lef tN odeIndex(i) and rightN odeIndex(i) mapping functions can
be adequately formulated.
The Chain module is an example of this approach where the mapping functions are extremely
simple: lef tN odeIndex(i) = i and rightN odeIndex(i) = i + 1. This pattern can also be used to
create a random subset of a full graph with a fixed number of connections.
In the case of irregular structures where none of the above patterns can be employed, one can
resort to listing all connections, as one would do in most existing simulators.
A submodule type can be specified with a module parameter of type string, or in general,
with any string-typed expression. The syntax uses the like keyword.
Let us begin with an example:
43
OMNeT++ Simulation Manual – The NED Language
network Net6
{
parameters:
string nodeType;
submodules:
node[6]: <nodeType> like INode {
address = index;
}
connections:
...
}
This code creates a submodule vector whose module type will come from the nodeType pa-
rameter. For example, if nodeType is set to "SensorNode", then the module vector will consist
of sensor nodes, provided such module type exists and it qualifies. What this means is that
the INode must be an existing module interface, which the SensorNode module type must
implement (more about this later).
As already mentioned, one can write an expression between the angle brackets. The expres-
sion may use the parameters of the parent module and previously defined submodules, and
it must yield a string value. For example, the following code is also valid:
network Net6
{
parameters:
string nodeTypePrefix;
int variant;
submodules:
node[6]: <nodeTypePrefix + "Node" + string(variant)> like INode {
...
}
The syntax “<nodeType> like INode” has an issue when used with submodule vectors: it
does not allow specifying different types for different indices. The following syntax is better
suited for submodule vectors:
44
OMNeT++ Simulation Manual – The NED Language
The expression between the angle brackets may be left out altogether, leaving a pair of empty
angle brackets, <>:
module Node
{
submodules:
nic: <> like INic; // type name expression left unspecified
...
}
Now the submodule type name is expected to be defined via typename pattern assignments.
Typename pattern assignments look like pattern assignments for the submodule’s parame-
ters, except that the parameter name is replaced by the typename keyword. Typename pattern
assignments may also be written in the configuration file. In a network that uses the above
Node NED type, typename pattern assignments would look like this:
network Network
{
parameters:
node[*].nic.typename = "Ieee80211g";
submodules:
node: Node[100];
}
A default value may also be specified between the angle brackets; it will be used if there is no
typename assignment for the module:
module Node
{
submodules:
nic: <default("Ieee80211b")> like INic;
...
}
There must be exactly one module type that goes by the simple name Ieee80211b and also
implements the module interface INic, otherwise, an error message will be issued. (The
imports in Node’s NED file play no role in the type resolution.) If there are two or more such
types, one can remove the ambiguity by specifying the fully qualified module type name, i.e.,
one that also includes the package name:
module Node
{
submodules:
nic: <default("acme.wireless.Ieee80211b")> like INic; // made-up name
...
}
When creating reusable compound modules, it is often useful to be able to make a parametric
submodule optional. One solution is to let the user define the submodule type with a string
parameter and not create the module when the parameter is set to the empty string. Like this:
45
OMNeT++ Simulation Manual – The NED Language
module Node
{
parameters:
string tcpType = default("Tcp");
submodules:
tcp: <tcpType> like ITcp if tcpType != "";
}
However, this pattern, when used extensively, can lead to a large number of string parameters.
Luckily, it is also possible to achieve the same effect with typename, without using extra
parameters:
module Node
{
submodules:
tcp: <default("Tcp")> like ITcp if typename != "";
}
The typename operator in a submodule’s if condition evaluates to the would-be type of the
submodule. By using the typename != "" condition, we can let the user eliminate the tcp
submodule by setting its typename to the empty string. For example, in a network that uses
the above NED type, typename pattern assignments could look like this:
network Network
{
parameters:
node1.tcp.typename = "TcpExt"; // let node1 use a custom TCP
node2.tcp.typename = ""; // no TCP in node2
submodules:
node1: Node;
node2: Node;
}
Note that this trick does not work with submodule vectors. The reason is that the condition
applies to the vector as a whole, while the type is per-element.
It is often also useful to be able to check, e.g., in the connections section, whether a conditional
submodule has been created or not. This can be done with the exists() operator. An
example:
module Node
{
...
connections:
ip.tcpOut --> tcp.ipIn if exists(ip) && exists(tcp);
}
Limitation: exists() may only be used after the submodule’s occurrence in the compound
module.
Parametric connection types work similarly to parametric submodule types, and the syntax is
similar as well. A basic example that uses a parameter of the parent module:
46
OMNeT++ Simulation Manual – The NED Language
The expression may use loop variables, parameters of the parent module, and parameters of
submodules (e.g., host[2].channelType).
The type expression may also be absent, and then the type is expected to be specified using
typename pattern assignments:
a.g++ <--> <> like IMyChannel <--> b.g++;
a.g++ <--> <> like IMyChannel {@display("ls=red");} <--> b.g++;
module Example
{
parameters:
@node; // module property
@display("i=device/pc"); // module property
int a @unit(s) = default(1); // parameter property
gates:
output out @loose @labels(pk); // gate properties
submodules:
src: Source {
parameters:
@display("p=150,100"); // submodule property
count @prompt("Enter count:"); // adding a property to a parameter
47
OMNeT++ Simulation Manual – The NED Language
gates:
out[] @loose;// adding a property to a gate
}
...
connections:
src.out++ --> { @display("ls=green,2"); } --> sink1.in; // connection prop.
src.out++ --> Channel { @display("ls=green,2"); } --> sink2.in;
}
Sometimes it is useful to have multiple properties with the same name, for example for declar-
ing multiple statistics produced by a simple module. Property indices make this possible.
A property index is an identifier or a number in square brackets after the property name, such
as eed and jitter in the following example:
simple App {
@statistic[eed](title="end-to-end delay of received packets";unit=s);
@statistic[jitter](title="jitter of received packets");
}
This example declares two statistics as @statistic properties, @statistic[eed] and @statis-
tic[jitter]. Property values within the parentheses are used to supply additional informa-
tion, like a more descriptive name (title="...") or a unit (unit=s). Property indices can be
conveniently accessed from the C++ API as well; for example, it is possible to ask what indices
exist for the "statistic" property, and it will return a list containing "eed" and "jitter").
In the @statistic example, the index was textual and meaningful, but neither is actually
required. The following dummy example shows the use of numeric indices which may be
ignored altogether by the code that interprets the properties:
simple Dummy {
@foo[1](what="apples";amount=2);
@foo[2](what="oranges";amount=5);
}
Note that without the index, the lines would actually define the same @foo property and would
overwrite each other’s values.
Indices also make it possible to override entries via inheritance:
simple DummyExt extends Dummy {
@foo[2](what="grapefruits"); // 5 grapefruits instead of 5 oranges
}
Properties may contain data given in parentheses; the data model is quite flexible. To begin
with, properties may contain no value or a single value:
@node;
@node(); // same as @node
@class(FtpApp2);
48
OMNeT++ Simulation Manual – The NED Language
The above examples are special cases of the general data model. According to the data model,
properties contain key-value list pairs separated by semicolons. Items in the value list are
separated by commas. Wherever key is missing, values go on the value list of the default key,
the empty string.
Value items may contain words, numbers, string constants, and some other characters, but
not arbitrary strings. Whenever the syntax does not permit some value, it should be enclosed
in quotes. This quoting does not affect the value because the parser automatically drops one
layer of quotes; thus, @class(TCP) and @class("TCP") are exactly the same. If the quotes
themselves need to be part of the value, an extra layer of quotes and escaping are the solution:
@foo("\"some string\"").
There are also some conventions. One can use properties to tag NED elements; for example,
a @host property could be used to mark all module types that represent various hosts. This
property could be recognized, e.g. by editing tools, by topology discovery code inside the
simulation model, etc.
The convention for such a “marker” property is that any extra data in it (i.e., within paren-
theses) is ignored, except a single word false, which has the special meaning of “turning off”
the property. Thus, any simulation model or tool that interprets properties should handle all
the following forms as equivalent to @host: @host(), @host(true), @host(anything-but-
false), @host(a=1;b=2); and @host(false) should be interpreted as the lack of the @host
tag.
Properties defined on a module or channel type may be updated both by subclassing and
when using type as a submodule or connection channel. One can add new properties and
also modify existing ones.
When modifying a property, the new property is merged with the old one. The rules of merging
are fairly simple. New keys simply get added. If a key already exists in the old property, items
in its value list overwrite items on the same position in the old property. A single hyphen (−)
as a value list item serves as an “antivalue”; it removes the item at the corresponding position.
Some examples:
base @prop
new @prop(a)
result @prop(a)
base @prop(a,b,c)
new @prop(,-)
result @prop(a,,c)
49
OMNeT++ Simulation Manual – The NED Language
base @prop(foo=a,b)
new @prop(foo=A,,c;bar=1,2)
result @prop(foo=A,b,c;bar=1,2)
NOTE: The above merge rules are part of NED, but the code that interprets properties
may have special rules for certain properties. For example, the @unit property of pa-
rameters is not allowed to be overridden, and @display is merged with special although
similar rules (see Chapter 8).
Here is a list of known NED properties in OMNeT++, grouped by the place of their usage.
Note that simulation models, such as the INET Framework, may define and use additional
properties for their purposes.
File / package level properties:
Parameter properties:
• @unit(<string>): Specifies the measurement unit for a parameter, e.g., "s" for seconds.
See 3.6.6.
50
OMNeT++ Simulation Manual – The NED Language
Gate properties:
• @directIn: Marks an input gate for receiving direct messages, bypassing the standard
message passing mechanism. See 4.7.5, A.4.11.
• @loose: Declares that the gate is not required to be connected in the connections section
of the compound module. See A.4.11.
• @labels(<strings>): Assigns a set of labels to the gate, which are used for matching
gates to be connected in the graphical editor.
3.13 Inheritance
Inheritance support in the NED language is only briefly described here because several details
and examples have already been presented in previous sections.
In NED, a type may only extend (extends keyword) an element of the same component type:
a simple module may extend a simple module, a channel may extend a channel, a module
interface may extend a module interface, and so on. However, there is one irregularity: a
compound module may extend a simple module (and inherit its C++ class), but the reverse is
not true.
Single inheritance is supported for modules and channels, and multiple inheritance is sup-
ported for module interfaces and channel interfaces. A network is a shorthand for a compound
module with the @isNetwork property set, so the same rules apply to it as to compound mod-
ules.
However, a simple or compound module type may implement (like keyword) several module
interfaces, and similarly, a channel type may implement several channel interfaces.
IMPORTANT: When extending a simple module type both in NED and in C++, the @class
property must be used to specify the new C++ class. Otherwise, the new module type will
inherit the C++ class of the base!
Inheritance may:
• add new properties, parameters, gates, inner types, submodules, and connections, as
long as the names do not conflict with inherited names
• modify inherited properties and properties of inherited parameters and gates
• not modify inherited submodules, connections, and inner types
For details and examples, refer to the corresponding sections of this chapter (simple mod-
ules 3.3, compound modules 3.4, channels 3.5, parameters 3.6, gates 3.7, submodules 3.8,
connections 3.9, module interfaces and channel interfaces 3.11.1).
51
OMNeT++ Simulation Manual – The NED Language
3.14 Packages
Having all NED files in a single directory is fine for small simulation projects. When a project
grows, however, it sooner or later becomes necessary to introduce a directory structure and
sort the NED files into them. NED natively supports directory trees with NED files and calls
directories packages. Packages are also useful for reducing name conflicts because names
can be qualified with the package name.
NOTE: NED packages are based on the Java package concept with minor enhancements.
If you are familiar with Java, you’ll find little surprise in this section.
3.14.1 Overview
When a simulation is run, one must tell the simulation kernel the directory which is the root of
the package tree; let’s call it NED source folder. The simulation kernel will traverse the whole
directory tree and load all NED files from every directory. One can have several NED directory
trees, and their roots (the NED source folders) should be given to the simulation kernel in
the NED path variable. The NED path can be specified in several ways: as an environment
variable (NEDPATH), as a configuration option (ned-path), or as a command-line option to the
simulation runtime (-n). NEDPATH is described in detail in Chapter 11.
Directories in a NED source tree correspond to packages. If NED files are in the <root>/a/b/c
directory (where <root> is listed in NED path), then the package name is a.b.c. The package
name has to be explicitly declared at the top of the NED files as well, like this:
package a.b.c;
The package name that follows from the directory name and the declared package must
match; it is an error if they don’t. (The only exception is the root package.ned file, as de-
scribed below.)
By convention, package names are all lowercase and begin with either the project name
(myproject) or the reversed domain name plus the project name (org.example.myproject).
The latter convention would cause the directory tree to begin with a few levels of empty direc-
tories, but this can be eliminated with a top-level package.ned.
NED files called package.ned have a special role, as they are meant to represent the whole
package. For example, comments in package.ned are treated as documentation of the pack-
age. Also, a @namespace property in a package.ned file affects all NED files in that directory
and all directories below.
The top-level package.ned file can be used to designate the root package, which is useful for
eliminating a few levels of empty directories resulting from the package naming convention.
For example, given a project where all NED types are under the org.acme.foosim package,
one can eliminate the empty directory levels org, acme, and foosim by creating a pack-
age.ned file in the source root directory with the package declaration org.example.myproject.
This will cause a directory foo under the root to be interpreted as package org.example.myproject.foo
and NED files in them must contain that as the package declaration. Only the root pack-
age.ned can define the package, package.ned files in subdirectories must follow it.
Let’s look at the INET Framework as an example, which contains hundreds of NED files in
several dozen packages. The directory structure looks like this:
INET/
src/
52
OMNeT++ Simulation Manual – The NED Language
base/
transport/
tcp/
udp/
...
networklayer/
linklayer/
...
examples/
adhoc/
ethernet/
...
The src and examples subdirectories are denoted as NED source folders, so NEDPATH is the
following (provided INET was unpacked in /home/joe):
/home/joe/INET/src;/home/joe/INET/examples
Both src and examples contain package.ned files to define the root package:
// INET/src/package.ned:
package inet;
// INET/examples/package.ned:
package inet.examples;
We already mentioned that packages can be used to distinguish similarly named NED types.
The name that includes the package name (a.b.c.Queue for a Queue module in the a.b.c
package) is called a fully qualified name; without the package name (Queue) it is called a
simple name.
Simple names alone are not enough to unambiguously identify a type. Here is how one can
refer to an existing type:
1. By fully qualified name. This is often cumbersome though, as names tend to be too long;
2. Import the type, then the simple name will be enough;
3. If the type is in the same package, then it doesn’t need to be imported; it can be referred
to by simple name
Types can be imported with the import keyword by either the fully qualified name or by a
wildcard pattern. In wildcard patterns, one asterisk ("*") stands for “any character sequence
not containing a period”, and two asterisks ("**") mean “any character sequence which may
contain a period”.
So, any of the following lines can be used to import a type called inet.protocols.net-
worklayer.ip.RoutingTable:
53
OMNeT++ Simulation Manual – The NED Language
import inet.protocols.networklayer.ip.RoutingTable;
import inet.protocols.networklayer.ip.*;
import inet.protocols.networklayer.ip.Ro*Ta*;
import inet.protocols.*.ip.*;
import inet.**.RoutingTable;
If an import explicitly names a type with its exact fully qualified name, then that type must
exist; otherwise, it is an error. Imports containing wildcards are more permissive; it is allowed
for them not to match any existing NED type (although that might generate a warning).
Inner types may not be referred to outside their enclosing types, so they cannot be imported
either.
The situation is a little different for submodule and connection channel specifications using
the like keyword, when the type name comes from a string-valued expression (see Section
3.11.1 about submodule and channel types as parameters). Imports are not much use here:
at the time of writing the NED file, it is not yet known what NED types will be suitable for
being “plugged in” there, so they cannot be imported in advance.
There is no problem with fully qualified names, but simple names need to be resolved differ-
ently. What NED does is this: it determines which interface the module or channel type must
implement (i.e. ... like INode), and then collects the types that have the given simple
name AND implement the given interface. There must be exactly one such type, which is then
used. If there is none or there are more than one, it will be reported as an error.
Let us see the following example:
module MobileHost
{
parameters:
string mobilityType;
submodules:
mobility: <mobilityType> like IMobility;
...
}
and suppose that the following modules implement the IMobility module interface: inet.mo-
bility.RandomWalk, inet.adhoc.RandomWalk, inet.mobility.MassMobility. Also, sup-
pose that there is a type called inet.examples.adhoc.MassMobility, but it does not imple-
ment the interface.
So if mobilityType="MassMobility", then inet.mobility.MassMobility will be selected;
the other MassMobility doesn’t interfere. However, if mobilityType="RandomWalk", then it
is an error because there are two matching RandomWalk types. Both RandomWalk’s can still be
used, but one must explicitly choose one of them by providing a package name: mobility-
Type="inet.adhoc.RandomWalk".
It is not mandatory to make use of packages: if all NED files are in a single directory listed on
the NEDPATH, then package declarations (and imports) can be omitted. Those files are said
to be in the default package.
54
OMNeT++ Simulation Manual – Simple Modules
Chapter 4
Simple Modules
Simple modules are the active components in the model. Simple modules are programmed in
C++, using the OMNeT++ class library. The following sections contain a brief introduction to
discrete event simulation in general, explain how its concepts are implemented in OMNeT++,
and give an overview and practical advice on how to design and code simple modules.
A discrete event system is a system where state changes (events) happen at discrete instances
in time, and events take zero time to happen. It is assumed that nothing (i.e. nothing inter-
esting) happens between two consecutive events, that is, no state change takes place in the
system between the events. This is in contrast to continuous systems where state changes
are continuous. Systems that can be viewed as discrete event systems can be modeled using
discrete event simulation, DES.
For example, computer networks are usually viewed as discrete event systems. Some of the
events are:
This implies that between two events such as start of a packet transmission and end of a
packet transmission, nothing interesting happens. That is, the packet’s state remains being
transmitted. Note that the definition of “interesting” events and states always depends on the
intent and purposes of the modeler. If we were interested in the transmission of individual bits,
we would have included something like start of bit transmission and end of bit transmission
among our events.
55
OMNeT++ Simulation Manual – Simple Modules
The time when events occur is often called event timestamp; with OMNeT++ we use the term
arrival time (because in the class library, the word “timestamp” is reserved for a user-settable
attribute in the event class). Time within the model is often called simulation time, model time
, or virtual time, as opposed to real time or CPU time, which refer to how long the simulation
program has been running and how much CPU time it has consumed.
Discrete event simulation maintains the set of future events in a data structure often called
FES (Future Event Set) or FEL (Future Event List). Such simulators usually work according
to the following pseudocode:
The initialization step usually builds the data structures representing the simulation model,
calls any user-defined initialization code, and inserts initial events into the FES to ensure that
the simulation can start. Initialization strategies can differ considerably from one simulator
to another.
The subsequent loop consumes events from the FES and processes them. Events are pro-
cessed in strict timestamp order to maintain causality, that is, to ensure that no current
event may have an effect on earlier events.
Processing an event involves calls to user-supplied code. For example, using the computer
network simulation example, processing a “timeout expired” event may consist of re-sending
a copy of the network packet, updating the retry count, scheduling another “timeout” event,
and so on. The user code may also remove events from the FES, for example, when canceling
timeouts.
The simulation stops when there are no events left (this rarely happens in practice) or when
it isn’t necessary for the simulation to run further because the model time or the CPU time
has reached a given limit, or because the statistics have reached the desired accuracy. At
this time, before the program exits, the user will typically want to record statistics into output
files.
OMNeT++ uses messages to represent events.1 Messages are represented by instances of the
cMessage class and its subclasses. Messages are sent from one module to another – this
1 For all practical purposes. Note that there is a class called cEvent that cMessage subclasses from, but it is only
56
OMNeT++ Simulation Manual – Simple Modules
means that the place where the “event will occur” is the message’s destination module, and
the model time when the event occurs is the arrival time of the message. Events like “timeout
expired” are implemented by the module sending a message to itself.
Events are consumed from the FES in arrival time order, to maintain causality. More precisely,
given two messages, the following rules apply:
1. The message with the earlier arrival time is executed first. If arrival times are equal,
2. the one with the higher scheduling priority (smaller numeric value) is executed first. If
priorities are the same,
3. the one scheduled/sent earlier is executed first.
The current simulation time can be obtained with the simTime() function.
Simulation time in OMNeT++ is represented by the C++ type simtime_t, which is by default a
typedef to the SimTime class. SimTime class stores simulation time in a 64-bit integer, using
decimal fixed-point representation. The resolution is controlled by the scale exponent global
configuration variable; that is, SimTime instances have the same resolution. The exponent
can be chosen between -18 (attosecond resolution) and 0 (seconds). Some exponents with the
ranges they provide are shown in the following table.
Note that although simulation time cannot be negative, it is still useful to be able to represent
negative numbers because they often arise during the evaluation of arithmetic expressions.
There is no implicit conversion from SimTime to double, mostly because it would conflict with
overloaded arithmetic operations of SimTime; use the dbl() method of SimTime or the SIM-
TIME_DBL() macro to convert. To reduce the need for dbl(), several functions and methods
have overloaded variants that directly accept SimTime, for example, fabs(), fmod(), div(),
ceil(), floor(), uniform(), exponential(), and normal().
Other useful methods of SimTime include str(), which returns the value as a string; parse(),
which converts a string to SimTime; raw(), which returns the underlying 64-bit integer;
getScaleExp(), which returns the global scale exponent; isZero(), which tests whether the
simulation time is 0; and getMaxTime(), which returns the maximum simulation time that
can be represented at the current scale exponent. Zero and the maximum simulation time
are also accessible via the SIMTIME_ZERO and SIMTIME_MAX macros.
// 340 microseconds in the future, truncated to the millisecond boundary
simtime_t timeout = (simTime() + SimTime(340, SIMTIME_US)).trunc(SIMTIME_MS);
57
OMNeT++ Simulation Manual – Simple Modules
NOTE: Converting a SimTime to double may lose precision because double only has a
52-bit mantissa. Earlier versions of OMNeT++ used double for the simulation time, but
that caused problems in long simulations that relied on fine-grained timing, for example,
MAC protocols. Other problems were the accumulation of rounding errors, and non-
associativity (often (x + y) + z 6= x + (y + z), see [Gol91]) which meant that two double
simulation times could not be reliably compared for equality.
The implementation of the FES is a crucial factor in the performance of a discrete event
simulator. In OMNeT++, the FES is replaceable, and the default FES implementation uses
binary heap as the data structure. Binary heap is generally considered to be the best FES
algorithm for discrete event simulation as it provides a good, balanced performance for most
workloads. (Exotic data structures like skiplist may perform better than heap in some cases.)
• initialize(). This method is invoked after OMNeT++ has set up the network (i.e.,
created modules and connected them according to the definitions) and provides a place
for initialization code.
• finish() is called when the simulation has terminated successfully, and it is recom-
mended to use it for recording summary statistics.
initialize() and finish(), together with initialize()’s variants for multi-stage initial-
ization, will be covered in detail in section 4.3.3.
58
OMNeT++ Simulation Manual – Simple Modules
cObject
...
cComponent
cModule cChannel
In OMNeT++, events occur inside simple modules. Simple modules encapsulate C++ code that
generates events and reacts to events, implementing the behavior of the module.
To define the dynamic behavior of a simple module, one of the following member functions
needs to be overridden:
Modules written with activity() and handleMessage() can be freely mixed within a simu-
lation model. Generally, handleMessage() should be preferred to activity(), due to scal-
ability and other practical reasons. The two functions will be described in detail in sections
4.4.1 and 4.4.2, including their advantages and disadvantages.
The behavior of channels can also be modified by redefining member functions. However, the
channel API is slightly more complicated than that of simple modules, so we’ll describe it in a
later section (4.8).
Last, let us mention refreshDisplay(), which is related to updating the visual appearance
of the simulation when run under a graphical user interface. refreshDisplay() is covered
in the chapter that deals with simulation visualization (8.2).
NOTE: refreshDisplay() has been added in OMNeT++ 5.0. Until then, visualization-
related tasks were usually implemented as part of handleMessage(). refreshDis-
play() provides a far superior and more efficient solution.
2 Cooperatively scheduled thread, explained later.
59
OMNeT++ Simulation Manual – Simple Modules
4.3.1 Overview
As mentioned before, a simple module is nothing more than a C++ class which needs to be
subclassed from cSimpleModule, with one or more virtual member functions redefined to
define its behavior.
The class needs to be registered with OMNeT++ via the Define_Module() macro. The De-
fine_Module() line should always be placed in .cc or .cpp files and not in the header file
(.h), because the compiler generates code from it.
The following HelloModule is one of the simplest simple modules that can be written. (We
could have omitted the initialize() method as well to make it even smaller, but then
how would it say Hello?) Note the use of cSimpleModule as the base class, and the De-
fine_Module() line.
// file: HelloModule.cc
#include <omnetpp.h>
using namespace omnetpp;
void HelloModule::initialize()
{
EV << "Hello World!\n";
}
In order to refer to this simple module type in NED files, an associated NED declaration is
also needed, which might look like this:
// file: HelloModule.ned
simple HelloModule
{
gates:
input in;
}
60
OMNeT++ Simulation Manual – Simple Modules
4.3.2 Constructor
Simple modules are never directly instantiated by the user, but rather by the simulation
kernel. This means that arbitrary constructors cannot be used: the signature must be what
is expected by the simulation kernel. Luckily, this contract is very simple: the constructor
must be public and must take no arguments:
public:
HelloModule(); // constructor takes no arguments
The first version should be used with handleMessage() simple modules, and the second one
with activity() modules. (With the latter, the activity() method of the module class runs
as a coroutine that needs a separate CPU stack, usually of 16..32K. This will be discussed in
detail later.) Passing zero stack size to the latter constructor also selects handleMessage().
Therefore, the following constructor definitions are all correct and select handleMessage() to
be used with the module:
HelloModule::HelloModule() {...}
HelloModule::HelloModule() : cSimpleModule() {...}
It is also correct to omit the constructor altogether, because the compiler-generated one is
suitable too.
The following constructor definition selects activity() to be used with the module, with 16K
of coroutine stack:
HelloModule::HelloModule() : cSimpleModule(16384) {...}
Basic Usage
The initialize() and finish() methods are declared as part of cComponent and provide
the user with the opportunity to run code at the beginning and successful termination of the
simulation.
The reason initialize() exists is that simulation-related code cannot usually be placed in
the simple module’s constructor, because the simulation model is still being set up when the
constructor runs, and many required objects are not yet available. In contrast, initialize()
is called just before the simulation starts executing, when everything else has already been
set up.
finish() is used for recording statistics and is only called when the simulation has termi-
nated normally. It does not get called when the simulation stops with an error message. The
destructor always gets called at the end, regardless of how the simulation stopped, but at that
time it is reasonable to assume that the simulation model has already been partly destroyed.
Based on the above considerations, the following conventions exist for these four methods:
61
OMNeT++ Simulation Manual – Simple Modules
Constructor:
Set pointer members of the module class to nullptr; postpone all other initialization
tasks to initialize().
initialize():
Perform all initialization tasks: read module parameters, initialize class variables, allo-
cate dynamic data structures with new, and allocate and initialize self-messages (timers)
if needed.
finish():
Record statistics. Do not delete anything or cancel timers – all cleanup must be done
in the destructor.
Destructor:
Delete everything that was allocated by new and is still held by the module class. When
deleting self-messages (timers), use the cancelAndDelete(msg) function! It is usually
incorrect to simply delete a self-message from the destructor, because it might be in the
scheduled events list. The cancelAndDelete(msg) function first checks for that and
cancels the message before deletion if necessary.
OMNeT++ prints the list of unreleased objects at the end of the simulation. When a simulation
model displays "undisposed object ..." messages, it indicates that the corresponding module
destructors need to be fixed. As a temporary measure, these messages can be hidden by
setting print-undisposed=false in the configuration.
NOTE: The perform-gc configuration option has been removed in OMNeT++ 4.0. Auto-
matic garbage collection cannot be reliably implemented due to the limitations of the C++
language.
Invocation Order
The initialize() functions of the modules are invoked before the first event is processed,
but after the initial events (starter messages) have been placed into the FES by the simulation
kernel.
Both simple and compound modules have initialize() functions. The initialize() func-
tion of a compound module runs before that of its submodules.
The finish() functions are called when the event loop has terminated, but only if it termi-
nated normally.
NOTE: finish() is not called if the simulation has terminated with a runtime error.
The calling order for finish() is the reverse of the order of initialize(): first the submod-
ules, then the encompassing compound module. 3
The following pseudocode summarizes this:
3 To provide an initialize() function for a compound module, cModule needs to be subclassed, and the new
class needs to be used for the compound module by adding the @class(<classname>) property to the NED declara-
tion.
62
OMNeT++ Simulation Manual – Simple Modules
callInitialize()
{
call to user-defined initialize() function
if (module is compound)
for (each submodule)
do callInitialize() on the submodule
}
callFinish()
{
if (module is compound)
for (each submodule)
do callFinish() on the submodule
call to user-defined finish() function
}
Keep in mind that finish() is not always called, so it is not a suitable place for cleanup
code that should run every time the module is deleted. finish() is only appropriate for
writing statistics, result post-processing, and other operations that are intended to run only
on successful completion. Cleanup code should be placed in the destructor.
Multi-Stage Initialization
The initialization of modules is orchestrated in stages. It starts with the call to initialize(0)
for every module, initiating the first setup stage. Once this is completed across all modules,
the system proceeds to the next steps, initialize(1), initialize(2), and so on, effectively
allowing modules to undergo additional configuration in a controlled, sequential order.
To effectively manage this sequential setup, each module must declare how many initial stages
it requires by overriding the numInitStages() function. For instance, if a module needs
two phases of setup, this function should return 2. Subsequently, the module must also
tailor the C++ initialize(int stage) function to specify the operations that occur at each
stage, such as handling specific setups at stage=0 and stage=1. This organized approach to
initialization ensures that each module is systematically readied according to its operational
63
OMNeT++ Simulation Manual – Simple Modules
4.4.1 handleMessage()
The idea is that at each event (message arrival), we simply call a user-defined function. This
function, handleMessage(cMessage *msg), is a virtual member function of cSimpleModule
which does nothing by default – the user has to redefine it in subclasses and add the message
processing code.
The handleMessage() function will be called for every message that arrives at the module.
The function should process the message and return immediately after that. The simula-
tion time is potentially different in each call. No simulation time elapses within a call to
handleMessage().
The event loop inside the simulator handles both activity() and handleMessage() simple
modules, and it corresponds to the following pseudocode:
Modules with handleMessage() are NOT started automatically: the simulation kernel creates
starter messages only for modules with activity(). This means that you have to schedule
self-messages from the initialize() function if you want a handleMessage() simple mod-
ule to start working “by itself”, without first receiving a message from other modules.
4 Note the const in the numInitStages() declaration. If you forget it, a different function is created instead of
redefining the existing one in the base class, so the existing function remains in effect and returns 1.
64
OMNeT++ Simulation Manual – Simple Modules
To use the handleMessage() mechanism in a simple module, you must specify zero stack
size for the module. This is important because this tells OMNeT++ that you want to use
handleMessage(), not activity().
Message/event related functions you can use in handleMessage():
The receive() and wait() functions cannot be used in handleMessage() because they are
coroutine-based by nature, as explained in the section about activity().
You have to add data members to the module class for every piece of information you want to
preserve. This information cannot be stored in local variables of handleMessage() because
they are destroyed when the function returns. Also, they cannot be stored in static variables
in the function (or the class) because they would be shared between all instances of the class.
Data members to be added to the module class will typically include things like:
• other variables which belong to the state of the module: retry counts, packet queues,
etc.
• values retrieved/computed once and then stored: values of module parameters, gate
indices, routing information, etc.
• pointers of message objects created once and then reused for timers, timeouts, etc.
These variables are often initialized from the initialize() method because the information
needed to obtain the initial value (e.g. module parameters) may not yet be available at the
time the module constructor runs.
Another task to be done in initialize() is to schedule initial event(s) which trigger the first
call(s) to handleMessage(). After the first call, handleMessage() must take care to schedule
further events for itself so that the “chain” is not broken. Scheduling events is not necessary
if your module only has to react to messages coming from other modules.
finish() is normally used to record statistics information accumulated in data members of
the class at the end of the simulation.
Application Area
1. When you expect the module to be used in large simulations involving several thou-
sand modules. In such cases, the module stacks required by activity() would simply
consume too much memory.
65
OMNeT++ Simulation Manual – Simple Modules
2. For modules that maintain little or no state information, such as packet sinks, han-
dleMessage() is more convenient to program.
3. Other good candidates are modules with a large state space and many arbitrary state
transition possibilities (i.e. where there are many possible subsequent states for any
state). Such algorithms are difficult to program with activity() and better suited for
handleMessage() (see rule of thumb below). This is the case for most communication
protocols.
// ...
The code for simple packet generators and sinks programmed with handleMessage() might
be as simple as the following pseudocode:
66
OMNeT++ Simulation Manual – Simple Modules
PacketGenerator::handleMessage(msg)
{
create and send out a new packet;
schedule msg again to trigger next call to handleMessage;
}
PacketSink::handleMessage(msg)
{
delete msg;
}
Note that PacketGenerator will need to redefine initialize() to create m and schedule the
first event.
The following simple module generates packets with exponential inter-arrival time. (Some
details in the source haven’t been discussed yet, but the code is probably understandable
nevertheless.)
class Generator : public cSimpleModule
{
public:
Generator() : cSimpleModule() {}
protected:
virtual void initialize();
virtual void handleMessage(cMessage *msg);
};
Define_Module(Generator);
void Generator::initialize()
{
// schedule first sending
scheduleAt(simTime(), new cMessage);
}
A bit more realistic example is to rewrite our Generator to create packet bursts, each consist-
ing of burstLength packets.
We add some data members to the class:
• burstLength will store the parameter that specifies how many packets a burst must
contain,
67
OMNeT++ Simulation Manual – Simple Modules
• burstCounter will count how many packets are left to be sent in the current burst.
The code:
class BurstyGenerator : public cSimpleModule
{
protected:
int burstLength;
int burstCounter;
Define_Module(BurstyGenerator);
void BurstyGenerator::initialize()
{
// init parameters and state variables
burstLength = par("burstLength");
burstCounter = burstLength;
// schedule first packet of first burst
scheduleAt(simTime(), new cMessage);
}
Pros:
Cons:
68
OMNeT++ Simulation Manual – Simple Modules
4.4.2 activity()
Process-Style Description
With activity(), a simple module can be coded much like an operating system process or
thread. One can wait for an incoming message (event) at any point in the code, suspend the
execution for some time (model time!), etc. When the activity() function exits, the module
is terminated (the simulation can continue if there are other modules that can run).
The most important functions that can be used in activity() are (they will be discussed in
detail later):
• end() – to finish execution of this module (same as exiting the activity() function)
The activity() function normally contains an infinite loop, with at least a wait() or re-
ceive() call in its body.
Application Area
In general, you should prefer handleMessage() to activity(). The main problem with
activity() is that it does not scale because every module needs a separate coroutine stack.
It has also been observed that activity() does not encourage good programming style, and
stack switching can confuse many debuggers.
There is one scenario where activity()’s process-style description is convenient: when the
process has many states, but transitions are very limited, i.e., from any state the process can
only go to one or two other states. For example, this is the case when programming a network
application that uses a single network connection. The pseudocode of the application, which
talks to a transport layer protocol, might look like this:
activity()
{
while(true)
{
open the connection by sending OPEN command to the transport layer
receive the reply from the transport layer
if (the open is not successful)
69
OMNeT++ Simulation Manual – Simple Modules
{
wait(some time)
continue // loop back to while()
}
70
OMNeT++ Simulation Manual – Simple Modules
activity() runs as a coroutine. Coroutines are similar to threads, but are scheduled non-
preemptively (this is also called cooperative multitasking). One can switch from one corou-
tine to another by a transferTo(otherCoroutine) call, causing the first coroutine to be
suspended and the second one to run. Later, when the second coroutine performs a trans-
ferTo(firstCoroutine) call to the first one, the execution of the first coroutine resumes
from the point of the transferTo(otherCoroutine) call. The full state of the coroutine, in-
cluding local variables, is preserved while the thread of execution is in other coroutines. This
implies that each coroutine has its own CPU stack, and transferTo() involves switching
from one CPU stack to another.
Coroutines are at the heart of OMNeT++, and the simulation programmer doesn’t ever need to
call transferTo() or other functions in the coroutine library, nor does the programmer need
to care about the coroutine library implementation. It is important to understand, however,
how the event loop works with coroutines.
When using coroutines, the event loop looks like this (simplified):
That is, when a module has an event, the simulation kernel transfers control to the module’s
coroutine. It is expected that when the module “decides it has finished processing the event”,
it will transfer control back to the simulation kernel by a transferTo(main) call. Initially,
simple modules using activity() are booted by events (starter messages) inserted into the
FES by the simulation kernel before the start of the simulation.
How does the coroutine know it has “finished processing the event”? The answer: when it
requests another event. The functions that request events from the simulation kernel are re-
ceive() and wait(), so their implementations contain a transferTo(main) call somewhere.
Their pseudocode, as implemented in OMNeT++, is:
receive()
{
transferTo(main)
retrieve the current event
return the event // remember: events = messages
}
wait()
{
create the event e
schedule it at (current simulation time + wait interval)
transferTo(main)
71
OMNeT++ Simulation Manual – Simple Modules
Thus, the receive() and wait() calls are special points in the activity() function because
they are where
Starter Messages
Modules written with activity() need starter messages to “boot”. These starter messages
are inserted into the FES automatically by OMNeT++ at the beginning of the simulation, even
before the initialize() functions are called.
The simulation programmer needs to define the CPU stack size for coroutines. This cannot be
automated.
16 or 32 kbytes is usually a good choice, but more space may be needed if the module uses
recursive functions or has many/large local variables. OMNeT++ has a built-in mechanism
that usually detects if the module stack is too small and overflows. OMNeT++ can also report
how much stack space a module actually uses at runtime.
Because local variables of activity() are preserved across events, you can store everything
(state information, packet buffers, etc.) in them. Local variables can be initialized at the top
of the activity() function, so there isn’t much need to use initialize().
You do need finish() if you want to write statistics at the end of the simulation. Because
finish() cannot access the local variables of activity(), you have to put the variables and
objects containing the statistics into the module class. You still don’t need initialize()
because class members can also be initialized at the top of activity().
A typical setup looks like this in pseudocode:
class MySimpleModule...
{
...
variables for statistics collection
activity();
finish();
};
72
OMNeT++ Simulation Manual – Simple Modules
MySimpleModule::activity()
{
declare local variables and initialize them
initialize statistics collection variables
while(true)
{
...
}
}
MySimpleModule::finish()
{
record statistics into file
}
Pros:
Cons:
• limited scalability: coroutine stacks can unacceptably increase the memory require-
ments of the simulation program if there are many activity()-based simple modules;
• run-time overhead: switching between coroutines is slower than a simple function call
• does not encourage good programming style: as module complexity grows, activity()
tends to become a large, monolithic function.
In most cases, cons outweigh pros, and it is a better idea to use handleMessage() instead.
If possible, avoid using global variables, including static class members. They are prone to
causing several problems. First, they are not reset to their initial values (to zero) when you
rebuild the simulation in Qtenv or start another run in Cmdenv. This may produce surprising
results. Second, they prevent you from parallelizing the simulation. When using parallel
simulation, each partition of the model runs in a separate process, having its own copies of
global variables. This is usually not what you want.
The solution is to encapsulate the variables into simple modules as private or protected data
members and expose them via public methods. Other modules can then call these public
methods to get or set the values. Calling methods of other modules will be discussed in
section 4.12. Examples of such modules are InterfaceTable and RoutingTable in the INET
Framework.
73
OMNeT++ Simulation Manual – Simple Modules
The code of simple modules can be reused via subclassing and redefining virtual member
functions. For example:
class TransportProtocolExt : public TransportProtocol
{
protected:
virtual void recalculateTimeout();
};
Define_Module(TransportProtocolExt);
void TransportProtocolExt::recalculateTimeout()
{
//...
}
NOTE: Note the @class() property, which tells OMNeT++ to use the TransportPro-
tocolExt C++ class for the module type! It is needed because NED inheritance is NED
inheritance only, so without @class() the TransportProtocolExt NED type would in-
herit the C++ class from its base NED type.
The value in a cPar object can be read with methods that correspond to the parameter’s NED
type: boolValue(), intValue(), doubleValue(), stringValue()/stdstringValue(), ob-
jectValue(), xmlValue(). There are also overloaded typecast operators for the correspond-
ing types (bool, integer types including int and long, double, const char *, cObject*,
and cXMLElement*).
long numJobs = par("numJobs").intValue();
double processingDelay = par("processingDelay"); // using operator double()
Note that cPar has two methods for returning a string value: stringValue(), which returns
const char *, and stdstringValue(), which returns std::string. For volatile parame-
ters, only stdstringValue() may be used, but otherwise the two are interchangeable.
74
OMNeT++ Simulation Manual – Simple Modules
If you use the par("foo") parameter in expressions (such as 4*par("foo")+2), the C++
compiler may be unable to decide between overloaded operators and report ambiguity. This
issue can be resolved by adding an explicit cast such as (double)par("foo"), or using the
doubleValue() or intValue() methods.
Volatile parameters in OMNeT++ are designed to provide dynamic values that are recalculated
every time they are accessed. This feature is particularly useful for simulations requiring
variability and unpredictability in parameter values.
Parameters can be declared volatile by marking them with the volatile keyword in the NED
file. When a parameter is marked as volatile, that indicates that reading the parameter’s
value will cause a re-evaluation of the NED expression, which, due to possible calls to the
random number generator, may yield a different value each time. Consequently, within the
model code, it is essential to re-fetch the parameter’s value each time it is required during
simulation. In other words, simply reading the parameter once in the initialize() function
and storing that value for subsequent use is incorrect.
Volatile parameters are often used to allow stochastic input, such as random packet gener-
ation intervals specified e.g. as exponential(1.0) (numbers drawn from the exponential
distribution with mean 1.0).
Note that non-volatile NED parameters behave differently: reading their values multiple times
is guaranteed to yield the same value every time. For non-volatile parameters, the NED ex-
pression is evaluated only once and the result is stored, so all reads will yield the same value.
When a non-volatile parameter is assigned an expression like exponential(1.0), multiple
reads will yield the same randomly chosen value.
The typical usage for non-volatile parameters is to read them in the initialize() method of
the module class and store the values in class variables for easy access later:
class Source : public cSimpleModule
{
protected:
long numJobs;
virtual void initialize();
...
};
void Source::initialize()
{
numJobs = par("numJobs");
...
}
volatile parameters need to be re-read every time the value is needed. For example, a
parameter that represents a random packet generation interval may be used like this:
void Source::handleMessage(cMessage *msg)
{
...
scheduleAt(simTime() + par("interval").doubleValue(), timerMsg);
...
}
75
OMNeT++ Simulation Manual – Simple Modules
This code looks up the parameter by name every time. This lookup can be avoided by storing
the parameter object’s pointer in a class variable, resulting in the following code:
class Source : public cSimpleModule
{
protected:
cPar *intervalp;
virtual void initialize();
virtual void handleMessage(cMessage *msg);
...
};
void Source::initialize()
{
intervalp = &par("interval");
...
}
Parameters declared with the type object in NED can be accessed with the objectValue()
method of cPar. It returns a pointer of the type cObject*, which then must be cast to the
appropriate type using check_and_cast() or dynamic_cast().
For example, if a module has a parameter declared as follows:
object packetToSend;
Then one can access this object parameter in C++ with the following line:
cPacket *packet = check_and_cast<cPacket*>(par("packetToSend").objectValue());
Object parameters allow for JSON-style parameters and many interesting use cases. These
use cases, along with real-life examples, were presented in the NED chapter, section 3.6.8.
In OMNeT++, JSON-style parameters introduced in version 6.0 offer a flexible way to pass
structured data to simulation modules. The NED expression syntax was extended with JSON-
like list and map (dictionary) syntaxes, which allows the user to express data structures as
JSON.
These data structures appear in C++ as object trees, with lists represented by the cValueAr-
ray class, and dictionaries represented by the cValueMap class. Values inside cValueArray
76
OMNeT++ Simulation Manual – Simple Modules
and cValueMap are stored in cValue instances. When a single value is assigned to an object
parameter, it is represented as a cValue wrapped in a cValueHolder. 5
To query and process JSON-style parameters in your module’s C++ code, particularly within
the initialize() method, you would retrieve the object using the par() and objectValue()
methods, cast them to the appropriate type, then use the methods provided by the above
classes to access the structured data.
Let us see an example. Consider a module that needs to process a routing table defined as
a JSON-style parameter. The routing table is an array of route objects, each specifying dest,
netmask, interf, and metric.
In the module’s NED file, we define the parameter as follows:
object routes;
In the module’s initialize() method, you can process this parameter as follows:
#include <omnetpp.h>
#include <vector>
#include <map>
#include <string>
77
OMNeT++ Simulation Manual – Simple Modules
This example demonstrates how to access a JSON-style parameter (routes), iterate over its
elements (routes), and extract and use the data in the simulation module’s logic. The use
of cValueMap and cValueArray classes makes handling structured data straightforward,
resembling the process of working with JSON in high-level programming languages.
Note that volatile object parameters yield a new object instance every time the parameter is
accessed.
Parameter values can be changed from the program during execution. This is rarely needed
but may be useful for some scenarios.
NOTE: The parameter’s type cannot be changed at runtime – it must remain the type
declared in the NED file. It is also not possible to add or remove module parameters at
runtime.
The methods to set the parameter value are setBoolValue(), setLongValue(), setString-
Value(), setDoubleValue(), setObjectValue(), setXMLValue(). There are also over-
loaded assignment operators for various types including bool, int, long, double, const
char *, cObject*, and cXMLElement*.
To allow a module to be notified about parameter changes, override its handleParameter-
Change() method, see 4.5.7.
The parameter’s name and type are returned by the getName() and getType() methods.
The latter returns a value from an enum that can be converted to a readable string with the
getTypeName() static method. The enum values are BOOL, DOUBLE, INT, STRING, OBJECT,
and XML, and since the enum is an inner type, they usually have to be qualified with cPar::.
isVolatile() returns whether the parameter was declared volatile in the NED file. isNu-
meric() returns true if the parameter type is double or long.
The str() method returns the parameter’s value in a string form. If the parameter contains
an expression, then the string representation of the expression is returned.
An example usage of the above methods:
int n = getNumParams();
for (int i = 0; i < n; i++)
{
cPar& p = par(i);
EV << "parameter: " << p.getName() << "\n";
EV << " type:" << cPar::getTypeName(p.getType()) << "\n";
EV << " contains:" << p.str() << "\n";
78
OMNeT++ Simulation Manual – Simple Modules
The NED properties of a parameter can be accessed with the getProperties() method
that returns a pointer to the cProperties object that stores the properties of this param-
eter. Specifically, getUnit() returns the unit of measurement associated with the parameter
(@unit property in NED).
Further cPar methods and related classes like cExpression and cDynamicExpression are
used by the NED infrastructure to set up and assign parameters. They are documented in the
API Reference but they are normally of little interest to users.
It is possible for modules to be notified when the value of a parameter changes at runtime,
possibly due to another module dynamically changing it. The typical action is to re-read the
parameter and update the module’s state if needed.
To enable notification, redefine the handleParameterChange() method of the module class.
This method will be called back by the simulation kernel with the parameter name as an
argument every time a new value is assigned to a parameter. The method signature is as
follows:
void handleParameterChange(const char *parameterName);
The following example shows a module that re-reads its serviceTime parameter when its
value changes:
void Queue::handleParameterChange(const char *parameterName)
{
if (strcmp(parameterName, "serviceTime") == 0)
serviceTime = par("serviceTime"); // refresh data member
}
Notifications are suppressed while the network (or module) is being set up.6
handleParameterChange() methods need to be implemented carefully because they may be
called at a time when the module has not yet completed all initialization stages.
Also, be extremely careful when changing parameters from inside handleParameterChange(),
as it is easy to accidentally create an infinite notification loop.
Module gates are represented by cGate objects. Gate objects know which other gates they are
connected to and what channel objects are associated with the links.
6 Priorto OMNeT++ 6.0, notifications were also disabled during the initialization phase (see 4.3.3), and additionally,
a handleParameterChange(nullptr) call was made by the simulation kernel after the last stage of initialization.
They are no longer done, and simulation models exploiting the previous behavior needs to be updated.
79
OMNeT++ Simulation Manual – Simple Modules
The cModule class has several member functions that deal with gates. You can look up a gate
by name using the gate() method:
cGate *outGate = gate("out");
This works for input and output gates. However, when a gate was declared inout in NED,
it is actually represented by the simulation kernel with two gates. Therefore, the above call
would result in a gate not found error. The gate() method needs to be told whether you need
the input or output half of the gate. This can be done by appending "$i" or "$o" to the gate
name. The following example retrieves the two gates for the inout gate "g":
cGate *gIn = gate("g$i");
cGate *gOut = gate("g$o");
Another way is to use the gateHalf() function, which takes the name of the inout gate and
either cGate::INPUT or cGate::OUTPUT:
cGate *gIn = gateHalf("g", cGate::INPUT);
cGate *gOut = gateHalf("g", cGate::OUTPUT);
These methods throw an error if the gate does not exist, so they cannot be used to determine
whether the module has a particular gate. For that purpose, there is a hasGate() method.
For example:
if (hasGate("optOut"))
send(new cMessage(), "optOut");
A gate can also be identified and looked up by a numeric gate ID. You can get the ID from
the gate itself (getId() method) or from the module by gate name (findGate() method). The
gate() method also has an overloaded variant that returns the gate from the gate ID.
int gateId = gate("in")->getId(); // or:
int gateId = findGate("in");
Gate IDs are more useful with gate vectors, which will be covered in detail in a later section.
Gate Vectors
Gate vectors have one cGate object per element. To access individual gates in the vector, you
need to call the gate() function with an additional index parameter. The index should be
between zero and size-1. The size of the gate vector can be obtained using the gateSize()
method. The following example iterates through all elements in the gate vector:
for (int i = 0; i < gateSize("out"); i++) {
cGate *gate = gate("out", i);
//...
}
A gate vector cannot have “holes” in it, which means that gate() never returns nullptr or
throws an error if the gate vector exists and the index is within bounds.
For inout gates, gateSize() may be called with or without the "$i"/"$o" suffix and returns
the same number.
80
OMNeT++ Simulation Manual – Simple Modules
The hasGate() method can be used both with and without an index, and they mean two
different things: without an index, it tells whether a gate vector with the given name exists,
regardless of its size (it returns true for an existing vector even if its size is currently zero!);
with an index, it also checks whether the index is within bounds.
Gate IDs
A gate can also be accessed by its ID. A very important property of gate IDs is that they are
contiguous within a gate vector, meaning the ID of a gate g[k] can be calculated as the ID of
g[0] plus k. This allows you to efficiently access any gate in a gate vector because retrieving a
gate by ID is more efficient than by name and index. The index of the first gate can be obtained
with gate("out",0)->getId(), but it is better to use a dedicated method, gateBaseId(),
because it also works when the gate vector size is zero.
Two other important properties of gate IDs are that they are stable and unique (within the
module). By stable we mean that the ID of a gate never changes, and by unique we mean
that at any given time, no two gates have the same IDs, and that IDs of deleted gates are not
reused later. Therefore, gate IDs are unique during the lifetime of a simulation run.
NOTE: Versions of OMNeT++ prior to 4.0 did not guarantee these properties. Resizing a
gate vector could cause its ID range to be relocated if it would have overlapped with the
ID range of other gate vectors. OMNeT++ 4.x solves the same problem by interpreting the
gate ID as a bitfield, basically containing bits that identify the gate name, and other bits
that hold the index. This also means that the theoretical upper limit for a gate size is now
smaller, although it is still large enough to be safely ignored for practical purposes.
If you need to go through all gates of a module, there are two possibilities. One is to use the
getGateNames() method, which returns the names of all gates and gate vectors the module
has. Then you can call isGateVector(name) to determine whether individual names identify
a scalar gate or a gate vector. Gate vectors can be enumerated by index. Also, for inout gates,
getGateNames() returns the base name without the "$i"/"$o" suffix, so the two directions
need to be handled separately. The gateType(name) method can be used to test whether a
gate is inout, input, or output (it returns cGate::INOUT, cGate::INPUT, or cGate::OUTPUT).
Clearly, the above solution can be quite challenging. An alternative is to use the GateItera-
tor class provided by cModule. Here is an example:
for (cModule::GateIterator i(this); !i.end(); i++) {
cGate *gate = *i;
...
}
81
OMNeT++ Simulation Manual – Simple Modules
Here, this denotes the module whose gates are being enumerated (it can be replaced by any
cModule * variable).
NOTE: In earlier versions of OMNeT++, gate IDs used to be small integers, so it made
sense to iterate over all gates of a module by enumerating all IDs from zero to a maximum,
skipping the holes (nullptrs). However, this is no longer the case with OMNeT++ 4.0 and
later versions. Additionally, the gate() method now throws an error when called with an
invalid ID, rather than returning nullptr.
Although rarely needed, it is possible to add and remove gates during simulation. You can
add scalar gates and gate vectors, change the size of gate vectors, and remove scalar gates
and whole gate vectors. However, it is not possible to remove individual random gates from a
gate vector, remove one half of an inout gate (e.g. "gate$o"), or set different gate vector sizes
on the two halves of an inout gate vector.
The cModule methods for adding and removing gates are addGate(name,type,isvector=false)
and deleteGate(name). Gate vector size can be changed using setGateSize(name,size).
None of these methods accept a "$i" or "$o" suffix in gate names.
NOTE: When memory efficiency is a concern, it is useful to know that in OMNeT++ 4.0
and later, a gate vector will consume significantly less memory than the same number of
individual scalar gates.
cGate Methods
The getName() method of cGate returns the name of the gate or gate vector without the
index. If you need a string that contains the gate index as well, use getFullName(). If you
also want to include the hierarchical name of the owner module, call getFullPath().
The getType() method of cGate returns the gate type, either cGate::INPUT or cGate::OUTPUT.
(It cannot return cGate::INOUT because an inout gate is represented by a pair of cGates.)
If you have a gate that represents half of an inout gate (that is, getName() returns something
like "g$i" or "g$o"), you can split the name with the getBaseName() and getNameSuf-
fix() methods. The getBaseName() method returns the name without the $i/$o suffix,
and getNameSuffix() returns just the suffix (including the dollar sign). For normal gates,
getBaseName() is the same as getName(), and getNameSuffix() returns the empty string.
The methods isVector(), getIndex(), getVectorSize() speak for themselves; size() is
an alias for getVectorSize(). For non-vector gates, getIndex() returns 0 and getVector-
Size() returns 1.
The getId() method returns the gate ID (not to be confused with the gate index).
The getOwnerModule() method returns the module to which the gate object belongs.
To illustrate these methods, we can modify the gate iterator example to print some information
about each gate:
for (cModule::GateIterator i(this); !i.end(); i++) {
cGate *gate = *i;
EV << gate->getFullName() << ": ";
EV << "id=" << gate->getId() << ", ";
82
OMNeT++ Simulation Manual – Simple Modules
if (!gate->isVector())
EV << "scalar gate, ";
else
EV << "gate " << gate->getIndex()
<< " in vector " << gate->getName()
<< " of size " << gate->getVectorSize() << ", ";
EV << "type:" << cGate::getTypeName(gate->getType());
EV << "\n";
}
There are further cGate methods to access and manipulate the connection(s) attached to the
gate, which will be covered in the following sections.
4.6.2 Connections
Simple module gates normally have one connection attached. However, compound module
gates need to be connected both inside and outside of the module to be useful. A series of
connections (joined with compound module gates) is called a connection path or just a path.
A path is directed, and it normally starts at an output gate of a simple module, ends at an
input gate of a simple module, and passes through several compound module gates.
Every cGate object contains pointers to the previous gate and the next gate in the path (re-
turned by the getPreviousGate() and getNextGate() methods). Therefore, a path can be
thought of as a double-linked list.
The use of the previous gate and next gate pointers with various gate types is illustrated in
Figure 4.2.
(a) (b)
(c) (d)
Figure 4.2: (a) Simple module output gate, (b) Compound module output gate, (c) Simple
module input gate, (d) Compound module input gate
The start and end gates of the path can be found using the getPathStartGate() and getPa-
thEndGate() methods, which simply follow the previous gate and next gate pointers, respec-
tively, until they are nullptr.
The isConnectedOutside() and isConnectedInside() methods return whether a gate is
connected to the outside or to the inside. They examine either the previous or the next pointer,
depending on the gate type (input or output). For example, an output gate is connected outside
if the next pointer is non-nullptr; the same function for an input gate checks the previous
83
OMNeT++ Simulation Manual – Simple Modules
The channel object associated with a connection is accessible via a pointer stored at the source
gate of the connection. The pointer is returned by the getChannel() method of the gate:
cChannel *channel = gate->getChannel();
The result may be nullptr, meaning that a connection may not have an associated channel
object.
If you have a channel pointer, you can get the source gate of the channel using the get-
SourceGate() method:
cGate *gate = channel->getSourceGate();
cChannel is just an abstract base class for channels, so to access details of the channel, you
might need to cast the resulting pointer into a specific channel class, for example cDelay-
Channel or cDatarateChannel.
Another specific channel type is cIdealChannel, which basically does nothing: it acts as if
there was no channel object assigned to the connection. OMNeT++ sometimes transparently
inserts a cIdealChannel into a channel-less connection, for example to hold the display
string associated with the connection.
Often, you are not really interested in a specific connection’s channel, but rather in the trans-
mission channel (see 4.7.6) of the connection path that starts at a specific output gate. The
transmission channel can be found by following the connection path until you find a channel
whose isTransmissionChannel() method returns true. However, cGate has a convenience
method for this called getTransmissionChannel(). Here is an example usage:
cChannel *txChan = gate("ppp$o")->getTransmissionChannel();
Both methods throw an error if no transmission channel is found. If this is not desirable, you
can use the similar findTransmissionChannel() and findIncomingTransmissionChan-
nel() methods, which simply return nullptr in that case.
Channels are covered in more detail in section 4.8.
84
OMNeT++ Simulation Manual – Simple Modules
4.7.1 Self-Messages
Nearly all simulation models need to schedule future events in order to implement timers,
timeouts, delays, etc. Some typical examples include:
• A source module that periodically creates and sends messages needs to schedule the
next send after every send operation.
• A server that processes jobs from a queue needs to start a timer every time it begins
processing a job. When the timer expires, the finished job can be sent out, and a new
job may start processing.
In OMNeT++, you can solve such tasks by having the simple module send a message to itself;
the message will be delivered to the simple module at a later point in time. Messages used
this way are called self-messages, and the module class has special methods for them that
allow for implementing self-messages without gates and connections.
Scheduling an Event
The module can send a message to itself using the scheduleAt() function. scheduleAt()
accepts an absolute simulation time:
scheduleAt(t, msg);
Since the target time is often relative to the current simulation time, the function has another
variant, scheduleAfter(), which takes a delta instead of an absolute simulation time. The
following calls are equivalent:
scheduleAt(simTime()+delta, msg);
scheduleAfter(delta, msg);
85
OMNeT++ Simulation Manual – Simple Modules
Self-messages are delivered to the module in the same way as other messages (via the usual
receive calls or handleMessage()); the module can call the isSelfMessage() member of any
received message to determine if it is a self-message.
You can determine whether a message is currently in the FES by calling its isScheduled()
member function.
Canceling an Event
Scheduled self-messages can be canceled (i.e. removed from the FES). This feature facilitates
implementing timeouts.
cancelEvent(msg);
The cancelEvent() function takes a pointer to the message to be canceled, and also returns
the same pointer. After canceling it, you may delete the message or reuse it in subsequent
scheduleAt() calls. cancelEvent() has no effect if the message is not scheduled at that
time.
There is also a convenience method called cancelAndDelete(), implemented as if (msg!=nullptr)
delete cancelEvent(msg). This method is primarily useful for writing destructors.
The following example shows how to implement a timeout in a simple imaginary stop-and-wait
protocol. The code uses a timeoutEvent module class data member that stores the pointer of
the cMessage used as a self-message, and compares it to the pointer of the received message
to identify whether a timeout has occurred.
void Protocol::handleMessage(cMessage *msg)
{
if (msg == timeoutEvent) {
// timeout expired, re-send packet and restart timer
send(currentPacket->dup(), "out");
scheduleAt(simTime() + timeout, timeoutEvent);
}
else if (...) { // if acknowledgment received
// cancel timeout, prepare to send next packet, etc.
cancelEvent(timeoutEvent);
...
}
else {
...
}
}
Re-scheduling an Event
To reschedule an event that is currently scheduled to a different simulation time, it must first
be canceled using cancelEvent(). This is shown in the following example code:
if (msg->isScheduled())
cancelEvent(msg);
scheduleAt(simTime() + delay, msg);
86
OMNeT++ Simulation Manual – Simple Modules
For convenience, the above functionality is available as a single call, using the functions
rescheduleAt() and rescheduleAfter(). The first one takes an absolute simulation time,
and the second one takes a delta relative to the current simulation time.
rescheduleAt(t, msg);
rescheduleAfter(delta, msg);
Using these dedicated functions is potentially more efficient than the cancelEvent() + sched-
uleAt() combination.
Once created, a message object can be sent through an output gate using one of the over-
loaded send() methods of cSimpleModule. There are six variations available, as the gate can
be specified in multiple ways and the methods also accept an optional SendOptions structure:
send(cMessage *msg, const char *gateName, int gateIndex=-1);
send(cMessage *msg, cGate *gate);
send(cMessage *msg, int gateId);
send(cMessage *msg, const SendOptions& options, const char *gateName, int gateIndex
send(cMessage *msg, const SendOptions& options, cGate *gate);
send(cMessage *msg, const SendOptions& options, int gateId);
The most common way of specifying the gate is with its name (gateName parameter). If the
name identifies a gate vector, an additional gateIndex parameter is required to select the
desired element of the vector.
send(msg, "out");
send(msg, "outv", 10); // send via outv[10]
To send a message on an inout gate, remember that an inout gate consists of an input and an
output gate combined. The input and output components of an inout gate are distinguished
by appending the $i and $o suffixes to their names, respectively. Thus, the gate name needs
to be specified in the send() call with the $o suffix:
send(msg, "g$o");
send(msg, "g$o", 10); // assuming g[] is an inout gate vector
Using a gate pointer (cGate*) will result in more efficient code, as it spares the lookup inside
the send() call. Typically, the module code obtains the gate pointer once (e.g., as part of the
initialization) and then reuses it throughout the simulation.
cGate *outGate = gate("out");
...
send(msg, outGate);
Using a gate ID (gateId parameter) is slightly less efficient than using the gate pointer, but it
has the advantage that gate vectors can be indexed with it efficiently, taking advantage of the
fact that elements of a gate vector occupy a contiguous ID range.
int outGateBaseId = gateBaseId("outv"); // or: gate("outv", 0)->getId()
...
int index = 10;
send(msg, outGateBaseId + index); // sends on outv[10]
The optional SendOptions, as well as other send variants like sendDelayed() and sendDi-
rect(), will be covered in later sections.
87
OMNeT++ Simulation Manual – Simple Modules
The send() call causes the message to travel along the full length of the connection path that
starts at the module and will be "delivered" to the module at the last gate in the path. The
connection path is the series of connections defined by the getNextGate() method of cGate;
the path ends when getNextGate() returns nullptr.
At each hop of the path, the associated channel object, if there is one, has authority over what
should happen to the message. More precisely, the processMessage() method of cChannel is
invoked with the message as an argument (and with some extra arguments such as SendOp-
tions). Individual channel types override the processMessage() method to apply various
types of processing. For example, they may modify the packet, add (propagation) delay, or
signal that the packet be discarded.
After the message has reached the last gate in the connection path (the gate where get-
NextGate() returns nullptr), the message will be passed to the arrived() method of the
module to which the last gate belongs. By default, the arrived() method inserts the mes-
sage into the FES, scheduled for the message’s arrival time, before returning. The message
will only be actually passed to the module’s handleMessage() (or activity()) method when
the simulation has advanced to the point where the message becomes the first event in the
FES.
The arrived() method is not normally overridden in simulation models. However, it is note-
worthy that the implementation of arrived() in cModule (which commonly represents com-
pound modules) stops the simulation, and displays an error message along the lines of “Mes-
sage arrived at a compound module”.
Broadcasting Messages
In your model, you may need to broadcast a message to several destinations. Broadcasts can
be implemented in a simple module by sending out copies of the same message, for example
on every gate of a gate vector. As previously mentioned, you cannot send the same message
object multiple times; instead, you need to create copies (duplicates) of the message object
and send them.
7 This feature does not significantly increase runtime overhead because it uses object ownership management
(described in Section 7.14); it merely checks that the owner of the message is the module that wants to send it.
88
OMNeT++ Simulation Manual – Simple Modules
Here is an example:
for (int i = 0; i < n; i++) {
cMessage *copy = msg->dup();
send(copy, "out", i);
}
delete msg;
It is important to note that copying the message for the last gate is redundant; you can just
send the original message there. Also, you can use gate IDs to avoid looking up the gate by
name for each send operation. You can exploit the fact that the ID of gate k in a gate vector
can be produced as baseID + k. An improved version of the code looks like this:
int outGateBaseId = gateBaseId("out");
for (int i = 0; i < n; i++)
send(i==n-1 ? msg : msg->dup(), outGateBaseId+i);
Retransmissions
Sometimes it is necessary for a module to hold a message for some time interval and then send
it. In such cases, you can use the scheduleAt() function, but there is a more straightforward
method: delayed sending. There are several methods provided for delayed sending:
sendDelayed(cMessage *msg, double delay, const char *gateName, int gateIndex=-1);
sendDelayed(cMessage *msg, double delay, int gateId);
sendDelayed(cMessage *msg, double delay, cGate *gate);
These methods are similar to the regular send() methods, but with an additional delay pa-
rameter, which must be non-negative. The effect of the function is similar to if the module
had kept the message for the delay interval and then sent it afterward; even the sending time
timestamp of the message will be set to the current simulation time plus the delay.
An example call:
sendDelayed(msg, 0.005, "out");
89
OMNeT++ Simulation Manual – Simple Modules
The sendDelayed() function does not perform a scheduleAt() followed by a send(), but
rather it computes everything about the message sending up front, including the arrival time
and the target module. This has two consequences. First, sendDelayed() is more efficient
than a scheduleAt() followed by a send() because it eliminates one event. Second, changes
in the connection path during the delay will not be taken into account (because everything is
calculated in advance, before the changes take place).
NOTE: Although sendDelayed() is more efficient, you should think twice before using
it in a simulation model. It may be suitable for one-shot simulation models known to
be static, but it is generally not recommended for reusable modules that need to work
correctly in a wide variety of simulation models, where a connection in the path may get
deleted, disabled, or reconnected to another module during the delay period.
The sendDirect() function allows for sending a message directly to an input gate of another
module. This is useful for simulating wireless transmissions. sendDirect() has several
variants because the target gate can be specified in various ways, a propagation delay and
duration can be optionally given, and these two can also be specified using a SendOptions
structure.
Here are the variants of sendDirect():
sendDirect(cMessage *msg, cModule *targetModule, int gateId);
sendDirect(cMessage *msg, cModule *targetModule, const char *gateName,
int gateIndex=-1);
sendDirect(cMessage *msg, cGate *gate);
At the target module, there is no difference between messages received directly and those
received over connections.
The target gate must be an unconnected gate; in other words, modules must have dedicated
gates to be able to receive messages sent via sendDirect(). It is not possible to have a gate
that receives messages via both connections and sendDirect().
It is recommended to tag gates dedicated for receiving messages via sendDirect() with the
@directIn property in the module’s NED declaration. This will cause OMNeT++ not to com-
90
OMNeT++ Simulation Manual – Simple Modules
plain that the gate is not connected in the network or compound module where the module is
used.
Here is an example:
simple Radio {
gates:
input radioIn @directIn; // for receiving air frames
}
The target module can be a simple module or a compound module. The message will follow
the connections that start at the target gate and will be delivered to the end module in the
path, just like with normal connections. The path must end with a simple module.
It is even permitted to send to an output gate, which will also cause the message to follow the
connections starting at that gate. This can be useful, for example, when several submodules
are sending to a single output gate of their parent module.
The transmission duration parameter is important when the message is also a packet, i.e.
subclassed from cPacket. In that case, the duration will be written into the packet, and
can be read by the receiver with the getDuration() method of the packet. For non-packet
messages, the duration parameter is ignored.
The receiver module can choose whether it wants the simulation kernel to deliver the packet
object to it at the start or at the end of the reception period. The default is the latter; the
module can change it by calling setDeliverImmediately() on the final input gate, that is,
on targetGate->getPathEndGate().
When a message is sent out on a gate, it usually travels through a series of connections until
it arrives at the destination module. We call this series of connections a connection path.
Several connections in the path may have an associated channel, but there can be only one
channel per path that models nonzero transmission duration. This restriction is enforced by
the simulation kernel. This channel is called the transmission channel. 8
NOTE: In practice, this means that there can be only one ned.DatarateChannel
in the path. Note that unnamed channels with a datarate parameter also map to
ned.DatarateChannel.
Transmitting a Packet
Packets may only be sent when the transmission channel is idle. This means that after each
transmission, the sender module needs to wait until the channel has finished transmitting
before it can send another packet.
You can get a pointer to the transmission channel by calling the getTransmissionChannel()
method on the output gate. The channel’s isBusy() and getTransmissionFinishTime()
methods can tell you whether a channel is currently transmitting, and when the transmission
is going to finish. (When the latter is less or equal the current simulation time, the channel
is free.) If the channel is currently busy, sending needs to be postponed: the packet can be
8 Moreover,if sendDirect() with a nonzero duration was used to send the packet to the start gate of the path,
then the path cannot have a transmission channel at all. The point is that the a transmission duration must be
unambiguous.
91
OMNeT++ Simulation Manual – Simple Modules
stored in a queue, and a timer (self-message) can be scheduled for the time when the channel
becomes empty.
A code example to illustrate the above process:
cPacket *pkt = ...; // packet to be transmitted
cChannel *txChannel = gate("out")->getTransmissionChannel();
simtime_t txFinishTime = txChannel->getTransmissionFinishTime();
if (txFinishTime <= simTime()) {
// channel free; send out packet immediately
send(pkt, "out");
}
else {
// store packet and schedule timer; when the timer expires,
// the packet should be removed from the queue and sent out
txQueue.insert(pkt);
scheduleAt(txFinishTime, endTxMsg);
}
NOTE: If there is a channel with a propagation delay in the path before the transmission
channel, the delay should be manually subtracted from the value returned by getTrans-
missionFinishTime()! The same applies to isBusy(): it tells whether the channel is
currently busy, and not whether it will be busy when a packet that you send gets there.
It is therefore advisable that you never use propagation delays in front of a transmission
channel in a path.
The getTransmissionChannel() method searches the connection path each time it is called.
If performance is important, it is a good idea to obtain the transmission channel pointer once,
and then cache it. When the network topology changes, the cached channel pointer needs
to be updated; section 4.14.3 describes the mechanism that can be used to get notifications
about topology changes.
Message sending is implemented like this: the arrival time and the bit error flag of a message
are calculated right inside the send() call, then the message is inserted into the FES with the
calculated arrival time. The message does not get scheduled individually for each link. This
implementation was chosen because of its run-time efficiency.
NOTE: The consequence of this implementation is that any change in the channel’s
parameters (delay, data rate, bit error rate, etc.) will only affect messages sent after the
change. Messages already underway will not be influenced by the change.
This is not a huge problem in practice, but if it is important to model channels with
changing parameters, the solution is to insert simple modules into the path to ensure
strict scheduling.
The code which inserts the message into the FES is the arrived() method of the recipient
module. By overriding this method it is possible to perform custom processing at the recipient
module immediately, still from within the send() call. Use only if you know what you are
doing!
92
OMNeT++ Simulation Manual – Simple Modules
NOTE: The receiver has to be prepared to receive transmission updates, and to react to
them appropriately. The details are explained in section 4.7.8.
At a later time, the transmission update can be sent with the following code:
cPacket *pk = new cPacket("update", 0, updatedLength*8);
send(pk, SendOptions().updateTx(transmissionId), "out");
9 Before OMNeT++ version 6.0, using the forceTransmissionFinishTime() channel method was recommended
as a way to implement aborting a transmission. It is now considered obsolete, and should not be used.
93
OMNeT++ Simulation Manual – Simple Modules
For the transmission to be modeled, the simulation kernel needs to obtain values for the
packet duration and the remaining duration. Input for these values may come from multiple
alternative sources:
• If the channel defines a data rate, the duration can be computed from that and the
packet length.
• If the channel does not contain the data rate, the sender must specify it explicitly in
SendOptions.
• Once the duration is known, the remaining duration can be computed by the channel as
start time + duration - current simulation time.
• Or if the channel does not keep track of the transmission start times, the remaining
duration must be specified by the model in SendOptions.
• Etc.
The cDatarateChannel class, the default transmission channel type in OMNeT++, supports
many variations of the above.
For wireless transmissions modeled with sendDirect, there is no channel, so the duration,
the remaining duration, and also the propagation delay must be specified explicitly. Here is
an example of sending the original packet:
cGate *targetGate = peerNode->gate("directIn");
cPacket *pk = new cPacket("directPk", 0, length*8);
transmissionId = pk->getId();
txStartTime = simTime();
propagationDelay = ...;
simtime_t duration = pk->getBitLength()/BITRATE;
sendDirect(pk,
SendOptions().transmissionId(transmissionId).
propagationDelay(propagationDelay).duration(duration),
targetGate);
Packets in OMNeT++ are delivered to modules in the same way as normal messages, through
the handleMessage() method. To access packet-specific methods and fields, it is necessary
to cast the incoming message to cPacket.
cPacket has several fields that provide information about the packet’s last transmission over
the transmission channel. These fields are:
94
OMNeT++ Simulation Manual – Simple Modules
• isReceptionStart(): Returns true if the packet represents the start of the reception
process.
• isReceptionEnd(): Returns true if the packet represents the end of the reception
process.
Based on the information carried by these fields, processing of the received packet typically
involves performing the steps described in the following sections.
Packets may have a bit error flag set due to channel error modeling. It is the receiver’s respon-
sibility to check this flag using hasBitError() and act accordingly, typically by discarding
the packet.
By default, packets are delivered at the end of their reception. To change this behavior,
call gate("in")->setDeliverImmediately(true); in the module’s initialize() method.
This setting causes packets to be delivered at the start of reception.
gate("in")->setDeliverImmediately(true);
This method may only be called on simple module input gates, and it instructs the simulation
kernel to deliver packets arriving through that gate at the simulation time that corresponds to
the beginning of the reception process. The setDeliverImmediately() method only needs
to be called once, so it is typically done in the initialize() method of the module.
When a packet is delivered to the module, you can call the packet’s isReceptionStart()
and isReceptionEnd() methods to determine whether it represents the start or end of the
reception process. (Note that for a transmission update, both methods may return false.)
The receiver should recognize transmission updates using isUpdate() and react accordingly.
Receivers that receive the packet at the end of the reception, which is the default behavior,
will only receive the final update. The original packet and intermediate updates are managed
by the simulation kernel.
Receivers that receive the packet at the start of the reception (as selected by setDeliverIm-
mediately(true) in the previous section) should be prepared to receive the original packet
95
OMNeT++ Simulation Manual – Simple Modules
B C
A delay=1ms
D
datarate=1Gbps
tA tB tC tD
send()
with deliver-
Immediately=true
default
and updates, and handle them appropriately. If an update arrives, the receiver should re-
place the original packet with the update and reschedule any potential end-reception event to
simTime() + pk->getRemainingDuration().
As a safeguard against unprepared modules accidentally processing transmission updates
as independent packets, the receiver is only given transmission updates if it has explic-
itly declared support for them. The module declares support by calling setTxUpdateSup-
port(true), usually in the initialize() method.
Non-transmission channels handle updates in the same way as they handle any other mes-
sages and packets.
Receiving Messages
Modules based on activity() receive messages using the receive() method of cSimple-
Module. The receive() method cannot be used with modules based on handleMessage().
cMessage *msg = receive();
The receive() function accepts an optional timeout parameter (in the form of a delta, not
an absolute simulation time). If no message arrives within the timeout period, the function
returns nullptr. 10
10 Putaside queue and the functions receiveOn(), receiveNew(), and receiveNewOn() were deprecated in OM-
96
OMNeT++ Simulation Manual – Simple Modules
if (msg == nullptr) {
... // handle timeout
}
else {
... // process message
}
The wait() function suspends the execution of the module for a given amount of simulation
time (a delta). wait() cannot be used with modules based on handleMessage().
wait(delay);
for (;;) {
// Wait for some, potentially random, amount of time specified
// in the interarrivalTime volatile module parameter
wait(par("interarrivalTime").doubleValue());
It is a runtime error if a message arrives during the wait interval. If you expect messages
to arrive during the wait period, you can use the waitAndEnqueue() function. It takes a
pointer to a queue object of class cQueue, described in Chapter 7, in addition to the wait
interval. Messages that arrive during the wait interval are accumulated in the queue and can
be processed after the waitAndEnqueue() call returns.
Here is an example:
cQueue queue("queue");
...
waitAndEnqueue(waitTime, &queue);
if (!queue.empty())
{
// Process messages that arrived during the wait interval
...
}
97
OMNeT++ Simulation Manual – Simple Modules
4.8 Channels
4.8.1 Overview
Channels encapsulate parameters and behavior associated with connections. Channel types
are similar to simple modules in that they are declared in NED, and there are C++ implemen-
tation classes underlying them. Section 3.5 describes NED language support for channels
and explains how to associate C++ classes with declared channel types in NED.
C++ channel classes must subclass the abstract base class cChannel. However, when creating
a new channel class, it may be more practical to extend one of the existing C++ channel classes
behind the three predefined NED channel types:
Channel classes need to be registered with the Define_Channel() macro, just like simple
module classes need Define_Module().
The channel base class cChannel inherits from cComponent, so channels participate in the
initialization and finalization protocol (initialize() and finish()) described in 4.3.3.
The parent module of a channel (as returned by getParentModule()) is the module that
contains the connection. If a connection connects two modules that are children of the same
compound module, the channel’s parent is the compound module. If the connection connects
a compound module to one of its submodules, the channel’s parent is also the compound
module.
When subclassing Channel, the following pure virtual member functions need to be overrid-
den:
The first two functions are usually one-liners; the channel behavior is encapsulated in the
third function, processMessage().
Transmission Channels
98
OMNeT++ Simulation Manual – Simple Modules
The Result struct is an inner type of cChannel and looks like this:
struct Result
{
bool discard = false; // whether the channel has lost the msg
simtime_t delay; // propagation delay
simtime_t duration; // transmission duration
simtime_t remainingDuration; // remaining tx duration (for tx update)
};
It also has a constructor that initializes all fields to zero; it is left out for brevity.
The method should model the transmission of the given message starting at the given t time
and store the results (propagation delay, transmission duration, deletion flag) in the result
object. Only the relevant fields in the result object need to be changed; others can be left
untouched.
99
OMNeT++ Simulation Manual – Simple Modules
Transmission duration and bit error modeling only apply to packets (i.e., to instances of
cPacket, where cMessage’s isPacket() returns true); they should be skipped for non-
packet messages. processMessage() does not need to call the setDuration() method on
the packet; this is done by the simulation kernel. However, it should call setBitError(true)
on the packet if error modeling results in bit errors.
If the method sets the discard flag in the result object, that means that the message object
will be deleted by OMNeT++; this facility can be used to model that the message gets lost in
the channel.
The processMessage() method does not need to throw an error on overlapping transmissions
or if the packet’s duration field is already set; these checks are done by the simulation kernel
before processMessage() is called.
To illustrate coding channel behavior, we look at how the built-in channel types are imple-
mented.
cIdealChannel lets messages and packets pass through without any delay or change. Its is-
TransmissionChannel() method returns false, getTransmissionFinishTime() returns
0s, and the body of its processMessage() method is empty:
cDelayChannel implements propagation delay, and it can be disabled; in its disabled state,
messages sent through it will be discarded. This class still models zero transmission duration,
so its isTransmissionChannel() and getTransmissionFinishTime() methods still return
false and 0s. The processMessage() method sets the appropriate fields in the Result
struct:
The handleParameterChange() method is also redefined, so that the channel can update
its internal delay and isDisabled data members if the corresponding channel parameters
100
OMNeT++ Simulation Manual – Simple Modules
11
change during simulation.
cDatarateChannel is different. It performs packet duration modeling (duration is calculated
from the data rate and the length of the packet), so isTransmissionChannel() returns true.
getTransmissionFinishTime() returns the value of a txfinishtime data member, which
gets updated after every packet.
simtime_t cDatarateChannel::getTransmissionFinishTime() const
{
return txfinishtime;
}
// datarate modeling
if (datarate != 0 && msg->isPacket()) {
simtime_t duration = ((cPacket *)msg)->getBitLength() / datarate;
result.duration = duration;
txfinishtime = t + duration;
}
else {
txfinishtime = t;
}
return result;
}
11 This code is a little simplified; the actual code uses a bit in a bitfield to store the value of isDisabled.
101
OMNeT++ Simulation Manual – Simple Modules
endSimulation() is rarely needed in practice because you can specify simulation time and
CPU time limits in the ini file (see later).
When the simulation encounters an error condition, it can throw a cRuntimeError exception
to terminate the simulation with an error message. (Under Cmdenv, the exception also causes
a nonzero program exit code). The cRuntimeError class has a constructor with a printf()-
like argument list. An example:
if (windowSize <= 0)
throw cRuntimeError("Invalid window size %d; must be >=1", windowSize);
Do not include a newline (\n), period, or exclamation mark in the error text; it will be added
by OMNeT++.
The same effect can be achieved by calling the error() method of cModule:
if (windowSize <= 0)
error("Invalid window size %d; must be >=1", windowSize);
Of course, the error() method can only be used when a module pointer is available.
4.10.1 Overview
Finite State Machines (FSMs) can make life easier when dealing with handleMessage(). OM-
NeT++ provides a class and a set of macros to build FSMs.
The key points are:
• There are two kinds of states: transient and steady. On each event (that is, at each call
to handleMessage()), the FSM transitions out of the current (steady) state, undergoes
a series of state changes (runs through a number of transient states), and finally arrives
at another steady state. Thus between two events, the system is always in one of the
steady states. Transient states are therefore not really necessary – they exist only to
group actions to be taken during a transition in a convenient way.
• You can assign program code to handle entering and leaving a state, known as entry/exit
code. Staying in the same state is handled as leaving and re-entering the state.
• Entry code should not modify the state (this is verified by OMNeT++). State changes
(transitions) must be put into the exit code.
102
OMNeT++ Simulation Manual – Simple Modules
OMNeT++’s FSMs can be nested. This means that any state (or rather, its entry or exit code)
may contain a further full-fledged FSM_Switch() (see below). This allows you to introduce
sub-states and thereby bring some structure into the state space if it becomes too large.
FSM state is stored in an object of type cFSM. The possible states are defined by an enum; the
enum is also a place to define which state is transient and which is steady. In the following
example, SLEEP and ACTIVE are steady states and SEND is transient (the numbers in paren-
theses must be unique within the state type and they are used for constructing the numeric
IDs for the states):
enum {
INIT = 0,
SLEEP = FSM_Steady(1),
ACTIVE = FSM_Steady(2),
SEND = FSM_Transient(1),
};
The actual FSM is embedded in a switch-like statement, FSM_Switch(), with cases for enter-
ing and leaving each state:
FSM_Switch(fsm)
{
case FSM_Exit(state1):
//...
break;
case FSM_Enter(state1):
//...
break;
case FSM_Exit(state2):
//...
break;
case FSM_Enter(state2):
//...
break;
//...
};
State transitions are done via calls to FSM_Goto(), which simply stores the new state in the
cFSM object:
FSM_Goto(fsm, newState);
The FSM starts from the state with the numeric code 0; this state is conventionally named
INIT.
Debugging FSMs
FSMs can log their state transitions, with the output looking like this:
...
FSM GenState: leaving state SLEEP
103
OMNeT++ Simulation Manual – Simple Modules
FSMs perform their logging via the FSM_Print() macro, defined as something like this:
#define FSM_Print(fsm,exiting)
(EV << "FSM " << (fsm).getName()
<< ((exiting) ? ": leaving state " : ": entering state ")
<< (fsm).getStateName() << endl)
The log output format can be changed by undefining FSM_Print() after the inclusion of
omnetpp.h, and providing a new definition.
Implementation
An Example
Let us write another bursty packet generator. It will have two states, SLEEP and ACTIVE. In
the SLEEP state, the module does nothing. In the ACTIVE state, it sends messages with a
given inter-arrival time. The code was taken from the Fifo2 sample simulation.
#define FSM_DEBUG
#include <omnetpp.h>
using namespace omnetpp;
104
OMNeT++ Simulation Manual – Simple Modules
// variables used
int i;
cMessage *startStopBurst;
cMessage *sendMessage;
Define_Module(BurstyGenerator);
void BurstyGenerator::initialize()
{
fsm.setName("fsm");
sleepTimeMean = par("sleepTimeMean");
burstTimeMean = par("burstTimeMean");
sendIATime = par("sendIATime");
msgLength = &par("msgLength");
i = 0;
WATCH(i); // always put watches in initialize()
startStopBurst = new cMessage("startStopBurst");
sendMessage = new cMessage("sendMessage");
scheduleAt(0.0,startStopBurst);
}
105
OMNeT++ Simulation Manual – Simple Modules
}
FSM_Goto(fsm,ACTIVE);
break;
case FSM_Enter(ACTIVE):
// schedule next sending
scheduleAt(simTime()+exponential(sendIATime), sendMessage);
break;
case FSM_Exit(ACTIVE):
// transition to either SEND or SLEEP
if (msg==sendMessage) {
FSM_Goto(fsm,SEND);
} else if (msg==startStopBurst) {
cancelEvent(sendMessage);
FSM_Goto(fsm,SLEEP);
} else {
error("invalid event in state ACTIVE");
}
break;
case FSM_Exit(SEND): {
// generate and send out job
char msgname[32];
sprintf(msgname, "job-%d", ++i);
EV << "Generating " << msgname << endl;
cMessage *job = new cMessage(msgname);
job->setBitLength((long) *msgLength);
job->setTimestamp();
send(job, "out");
// return to ACTIVE
FSM_Goto(fsm,ACTIVE);
break;
}
}
}
If a module is part of a module vector, the getIndex() and getVectorSize() member func-
tions can be used to query its index and the vector size:
EV << "This is module [" << module->getIndex() <<
"] in a vector of size [" << module->getVectorSize() << "].\n";
Every component (module and channel) in the network has an ID that can be obtained from
cComponent’s getId() member function:
int componentId = getId();
106
OMNeT++ Simulation Manual – Simple Modules
An ID uniquely identifies a module or channel for the whole duration of the simulation. This
holds even when modules are created and destroyed dynamically because IDs of deleted mod-
ules or channels are never reused for newly created ones.
To look up a component by ID, one needs to use methods of the simulation manager ob-
ject, cSimulation. getComponent() expects an ID and returns the component’s pointer if
the component still exists. Otherwise, it returns nullptr. The method has two variations,
getModule(id) and getChannel(id). They return cModule and cChannel pointers if the
identified component is, in fact, a module or channel, respectively. Otherwise, they return
nullptr.
int id = 100;
cModule *mod = getSimulation()->getModule(id); // exists, and is a module
For example, the parameters of the parent module are accessed like this:
double timeout = getParentModule()->par("timeout");
107
OMNeT++ Simulation Manual – Simple Modules
Without the leading dot, the path is interpreted as absolute. The following lines both find the
tcp submodule of host[2] in the network, regardless of the module on which the getMod-
uleByPath() has been invoked.
cModule *tcp = module->getModuleByPath("Network.host[2].tcp");
cModule *tcp = module->getModuleByPath("host[2].tcp");
To access all modules within a compound module, one can use cModule::SubmoduleIterator.
for (cModule::SubmoduleIterator it(module); !it.end(); it++) {
cModule *submodule = *it;
EV << submodule->getFullName() << endl;
}
To determine the module at the other end of a connection, use cGate’s getPreviousGate(),
getNextGate(), and getOwnerModule() methods. An example:
cModule *neighbour = gate("out")->getNextGate()->getOwnerModule();
• how to inform the simulation kernel that a method call across modules is taking place.
108
OMNeT++ Simulation Manual – Simple Modules
Typically, the called module is in the same compound module as the caller, so the getPar-
entModule() and getSubmodule() methods of cModule can be used to obtain a cModule*
pointer to the called module. (Further ways to obtain the pointer are described in section
4.11). The cModule* pointer then has to be cast to the actual C++ class of the module, so
that its methods become visible.
This can be achieved using the following code:
cModule *targetModule = getParentModule()->getSubmodule("foo");
Foo *target = check_and_cast<Foo *>(targetModule);
target->doSomething();
The check_and_cast<>() template function on the second line is part of OMNeT++. It per-
forms a standard C++ dynamic_cast and checks the result: if it is nullptr, check_and_cast
raises an OMNeT++ error. Using check_and_cast saves you from writing error checking
code: if targetModule from the first line is nullptr because the submodule named "foo"
was not found, or if that module is actually not of type Foo, an exception is thrown from
check_and_cast with an appropriate error message.13
The second issue is how to inform the simulation kernel that a method call across modules
is taking place. Why is this necessary in the first place? First, the simulation kernel always
needs to know which module’s code is currently executing in order for ownership handling
and other internal mechanisms to work correctly. Second, the Qtenv simulation GUI can
animate method calls, but to be able to do that, it needs to know about them. Third, method
calls are also recorded in the event log.
The solution is to add the Enter_Method() or Enter_Method_Silent() macro at the begin-
ning of the methods that may be invoked from other modules. These calls perform context
switching and, in the case of Enter_Method(), notify the simulation GUI so that animation
of the method call can take place. Enter_Method_Silent() does not animate the method
call, but otherwise, it is equivalent to Enter_Method(). Both macros accept a printf()-like
argument list (it is optional for Enter_Method_Silent()), which should produce a string with
the method name and the actual arguments as much as possible. The string is displayed in
the animation (Enter_Method() only) and recorded into the event log.
void Foo::doSomething()
{
Enter_Method("doSomething()");
...
}
Certain simulation scenarios require the ability to dynamically create and destroy modules.
For example, simulating the arrival and departure of new users in a mobile network may be
implemented in terms of adding and removing modules during the course of the simulation.
Loading and instantiating network topology (i.e. nodes and links) from a data file is another
common technique enabled by dynamic module (and link) creation.
13 A check_and_cast_nullable<>() function also exists. It accepts nullptr as input and only complains if the
109
OMNeT++ Simulation Manual – Simple Modules
OMNeT++ allows both simple and compound modules to be created at runtime. When instan-
tiating a compound module, its full internal structure (submodules and internal connections)
is reproduced.
Once created and started, dynamic modules are no different from “static” modules.
4.13.2 Overview
To understand how dynamic module creation works, you have to know a bit about how OM-
NeT++ normally instantiates modules. Each module type (class) has a corresponding fac-
tory object of the class cModuleType. This object is created under the hood by the De-
fine_Module() macro, and it has a factory method which can instantiate the module class
(this function basically only consists of a return new <moduleclass>(...) statement).
The cModuleType object can be looked up by its name string (which is the same as the module
class name). Once you have its pointer, it is possible to call its factory method and create an
instance of the corresponding module class – without having to include the C++ header file
containing the module’s class declaration into your source file.
The cModuleType object also knows what gates and parameters the given module type has to
have. (This information comes from NED files.)
Simple modules can be created in one step. For a compound module, the situation is more
complicated because its internal structure (submodules, connections) may depend on param-
eter values and gate vector sizes. Thus, for compound modules, it is generally required to first
create the module itself, second, set parameter values and gate vector sizes, and then call the
method that creates its submodules and internal connections.
As you already know, simple modules with activity() need a starter message. For statically
created modules, this message is created automatically by OMNeT++, but for dynamically
created modules, you have to do this explicitly by calling the appropriate functions.
Calling initialize() has to take place after insertion of the starter messages because the
initializing code may insert new messages into the FES, and these messages should be pro-
cessed after the starter message.
The first step is to find the factory object. The cModuleType::get() function expects a fully
qualified NED type name and returns the factory object:
cModuleType *moduleType = cModuleType::get("foo.nodes.WirelessNode");
The return value does not need to be checked for nullptr because the function raises an
error if the requested NED type is not found. (If this behavior is not what you need, you can
use the similar cModuleType::find() function, which returns nullptr if the type was not
found.)
110
OMNeT++ Simulation Manual – Simple Modules
If the createScheduleInit() all-in-one method is not applicable, one needs to use the full
procedure. It consists of five steps:
Each step (except for Step 3.) can be done with one line of code.
See the following example, where Step 3 is omitted:
// find factory object
cModuleType *moduleType = cModuleType::get("foo.nodes.WirelessNode");
// create (possibly compound) module and build its submodules (if any)
cModule *module = moduleType->create("node", this);
module->finalizeParameters();
module->buildInside();
If you want to set up parameter values or gate vector sizes (Step 3.), the code goes between
the create() and buildInside() calls:
// create
cModuleType *moduleType = cModuleType::get("foo.nodes.WirelessNode");
cModule *module = moduleType->create("node", this);
module->setGateSize("in", 3);
module->setGateSize("out", 3);
111
OMNeT++ Simulation Manual – Simple Modules
If the module was a compound module, this involves recursively deleting all its submodules.
An activity()-based simple module can also delete itself; in that case, the deleteModule()
call does not return to the caller.
When deleteModule() is called on a compound module, individual modules under the com-
pound module are notified by calling their preDelete() member functions before any change
is actually made.
This notification can be quite useful when the compound module contains modules that hold
pointers to each other, necessitated by their complex interactions via C++ method calls. With
such modules, destruction can be tricky: given a sufficiently complex control flow involving
cascading cross-module method calls and signal listeners, it is actually quite easy to acci-
dentally invoke a method on a module that has already been deleted at that point, resulting
in a crash. (Note that destructors of collaborating modules cannot rely on being invoked in
any particular order because that order is determined factors, e.g. submodule order in NED,
which are out of the control of the C++ code.)
preDelete() is a cComponent virtual method that, similar to handleMessage() and ini-
tialize(), is intended for being overridden by the user. When a compound module is deleted,
deleteModule() first invokes preDelete() recursively on the submodule tree and only starts
deleting modules after that. This gives a chance to modules that override preDelete() to set
pointers to collaborating modules to nullptr, or otherwise ensure that nothing bad will hap-
pen once modules start being deleted.
preDelete() receives an argument: the pointer of the module on which deleteModule()
was invoked. This allows the module to tell apart cases when, for example, it is deleted itself
or as part of a larger unit.
An example:
void Foo::preDelete(cComponent *root)
{
barModule = nullptr;
}
112
OMNeT++ Simulation Manual – Simple Modules
if (fooModule)
fooModule->doSomething();
finish() is called for all modules at the end of the simulation, no matter how the modules
were created. If a module is dynamically deleted before that, finish() will not be invoked
(deleteModule() does not do it). However, you can still manually invoke it before delete-
Module().
You can use the callFinish() function to invoke finish() (It is not a good idea to invoke
finish() directly). If you are deleting a compound module, callFinish() will recursively
invoke finish() for all submodules, and if you are deleting a simple module from another
module, callFinish() will do the context switch for the duration of the call. 14
Example:
mod->callFinish();
mod->deleteModule();
113
OMNeT++ Simulation Manual – Simple Modules
// create connecting
outGate->connectTo(inGate, channel);
The channel object will be owned by the source gate of the connection, and one cannot reuse
the same channel object with several connections.
Instantiating one of the built-in channel types (cIdealChannel, cDelayChannel, or cDatarat-
eChannel) is somewhat simpler because those classes have static create() factory functions
and the step of finding the factory object can be spared. Alternatively, one can use cChannel-
Type’s createIdealChannel(), createDelayChannel(), and createDatarateChannel()
static methods.
The channel object may need to be parameterized before using it for a connection. For ex-
ample, cDelayChannel has a setDelay() method, and cDatarateChannel has setDelay(),
setDatarate(), setBitErrorRate(), and setPacketErrorRate().
An example that sets up a channel with a datarate and a delay between two modules:
cDatarateChannel *datarateChannel = cDatarateChannel::create("channel");
datarateChannel->setDelay(0.001);
datarateChannel->setDatarate(1e9);
outGate->connectTo(inGate, datarateChannel);
Finally, here is a more complete example that creates two modules and connects them in both
directions:
cModuleType *moduleType = cModuleType::get("TicToc");
cModule *a = modtype->createScheduleInit("a", this);
cModule *b = modtype->createScheduleInit("b", this);
a->gate("out")->connectTo(b->gate("in"));
b->gate("out")->connectTo(a->gate("in"));
The disconnect() method of cGate can be used to remove connections. This method has to
be invoked on the source side of the connection. It also destroys the channel object associated
with the connection if one has been set.
srcGate->disconnect();
4.14 Signals
This section describes simulation signals, or signals for short. Signals are a versatile concept
that first appeared in OMNeT++ 4.1.
114
OMNeT++ Simulation Manual – Simple Modules
• exposing statistical properties of the model, without specifying whether and how to
record them
• receiving notifications about simulation model changes at runtime, and acting upon
them
• emitting information for other purposes, for example as input for custom animation
effects
Signals are emitted by components (modules and channels). Signals propagate on the module
hierarchy up to the root. At any level, one can register listeners, that is, objects with callback
methods. These listeners will be notified (their appropriate methods called) whenever a signal
value is emitted. The result of upwards propagation is that listeners registered at a compound
module can receive signals from all components in that submodule tree. A listener registered
at the system module can receive signals from the whole simulation.
NOTE: A channel’s parent is the (compound) module that contains the connection, not
the owner of either gate the channel is connected to.
Signals are identified by signal names (i.e. strings), but for efficiency, at runtime we use
dynamically assigned numeric identifiers (signal IDs, typedef’d as simsignal_t). The mapping
of signal names to signal IDs is global, so all modules and channels asking to resolve a
particular signal name will get back the same numeric signal ID.
Listeners can subscribe to signal names or IDs, regardless of their source. For example, if
two different and unrelated module types, say Queue and Buffer, both emit a signal named
"length", then a listener that subscribes to "length" at some higher compound module will
get notifications from both Queue and Buffer module instances. The listener can still look at
the source of the signal if it wants to distinguish the two (it is available as a parameter to the
callback function), but the signals framework itself does not have such a feature.
NOTE: Because the component type that emits the signal is not part of the signal’s
identity, it is advised to choose signal names carefully. A good naming scheme facilitates
the “merging” of signals that arrive from different sources but mean the same thing, and
reduces the chance of collisions between signals that accidentally have the same name
but represent different things.
When a signal is emitted, it can carry a value with it. There are multiple overloaded versions of
the emit() method for different data types, and also overloaded receiveSignal() methods
in listeners. The signal value can be of selected primitive types, or an object pointer; anything
that is not feasible to emit as a primitive type may be wrapped into an object and emitted as
such.
Even when the signal value is of a primitive type, it is possible to convey extra information to
listeners via an additional details object, which is an optional argument of emit().
115
OMNeT++ Simulation Manual – Simple Modules
These goals have been achieved in the 4.1 version with the following implementation. First,
the data structure that used to store listeners in components is dynamically allocated, so if
there are no listeners, the per-component overhead is only the size of the pointer (which will
be nullptr then).
Second, additionally there are two bitfields in every component that store which one of the
first 64 signals (IDs 0..63) have local listeners and listeners in ancestor modules.15 Using
these bitfields, it is possible to determine in constant time for the first 64 signals whether the
signal has listeners, so emit() can return immediately if there are none. For other signals,
emit() needs to examine the listener lists up to the root every time. Even if a simulation
uses more than 64 signals, in performance-critical situations it is possible to arrange that
frequently emitted signals (e.g. "txBegin") get the “fast” signal IDs, while infrequent signals
(like e.g. "routerDown") get the rest.
Signal-related methods are declared on cComponent, so they are available for both cModule
and cChannel.
Signal IDs
Signals are identified by names, but internally, numeric signal IDs are used for efficiency.
The registerSignal() method takes a signal name as a parameter and returns the corre-
sponding simsignal_t value. The method is static, illustrating the fact that signal names are
global. An example:
simsignal_t lengthSignalId = registerSignal("length");
The getSignalName() method (also static) does the reverse: it accepts a simsignal_t and
returns the name of the signal as a const char * (or nullptr for an invalid signal handle):
const char *signalName = getSignalName(lengthSignalId); // --> "length"
NOTE: Since OMNeT++ 4.3, the lifetime of signal IDs is the entire program, and it is
possible to call registerSignal() from initializers of global variables, e.g., static class
members. In earlier versions, signal IDs were usually allocated in initialize() and
were only valid for that simulation run.
15 It is assumed that there will be typically less than 64 frequently used signals used at a time in a simulation.
116
OMNeT++ Simulation Manual – Simple Modules
Emitting Signals
The emit() family of functions emit a signal from the module or channel. emit() takes a
signal ID (simsignal_t) and a value as parameters:
emit(lengthSignalId, queue.length());
The value can be of type bool, long, double, simtime_t, const char *, or (const) cOb-
ject *. Other types can be cast into one of these types or wrapped into an object subclassed
from cObject.
emit() also has an extra, optional object pointer argument named details, with the type
cObject*. This argument may be used to convey extra information to listeners.16
When there are no listeners, the runtime cost of emit() is usually minimal. However, if
producing a value has a significant runtime cost, then the mayHaveListeners() or hasLis-
teners() method can be used to check beforehand whether the given signal has any listeners
at all. If not, producing the value and emitting the signal can be skipped.
Example usage:
if (mayHaveListeners(distanceToTargetSignal)) {
double d = sqrt((x-targetX)*(x-targetX) + (y-targetY)*(y-targetY));
emit(distanceToTargetSignal, d);
}
The mayHaveListeners() method is very efficient (a constant-time operation) but may return
false positive. In contrast, hasListeners() will search up to the top of the module tree if the
answer is not cached, so it is generally slower. We recommend that you take into account the
cost of producing notification information when deciding between mayHaveListeners() and
hasListeners().
Signal Declarations
Since OMNeT++ 4.4, signals can be declared in NED files for documentation purposes, and
OMNeT++ can check that only declared signals are emitted, and that they actually conform to
the declarations (with regard to the data type, etc.)
The following example declares a queue module that emits a signal named queueLength:
simple Queue
{
parameters:
@signal[queueLength](type=long);
...
}
Signals are declared with the @signal property on the module or channel that emits it. (NED
properties are described in 3.12). The property index corresponds to the signal name, and the
property’s body may declare various attributes of the signal; currently only the data type is
supported.
The type property key is optional; when present, its value should be bool, long, unsigned
long, double, simtime_t, string, or a registered class name optionally followed by a ques-
tion mark. Classes can be registered using the Register_Class() or Register_Abstract_Class()
16 The details parameter was added in OMNeT++ 5.0.
117
OMNeT++ Simulation Manual – Simple Modules
macros; these macros create a cObjectFactory instance, and the simulation kernel will call
cObjectFactory’s isInstance() method to check that the emitted object is really a subclass
of the declared class. isInstance() just wraps a C++ dynamic_cast.)
A question mark after the class name means that the signal is allowed to emit nullptr
pointers. For example, a module named PPP may emit the frame (packet) object every time it
starts transmitting and emit nullptr when the transmission is completed:
simple PPP
{
parameters:
@signal[txFrame](type=PPPFrame?); // a PPPFrame or nullptr
...
}
The property index may contain wildcards, which is important for declaring signals whose
names are only known at runtime. For example, if a module emits signals called session-1-
seqno, session-2-seqno, session-3-seqno, etc., those signals can be declared as:
@signal[session-*-seqno]();
Starting with OMNeT++ 5.0, signal checking is turned on by default when the simulation
kernel is compiled in debug mode, requiring all signals to be declared with @signal. (It is
turned off in release-mode simulation kernels due to performance reasons.)
If needed, signal checking can be disabled with the check-signals configuration option:
check-signals = false
When emitting a signal with a cObject* pointer, you can pass as data an object that you
already have in the model, provided you have a suitable object at hand. However, it is often
necessary to declare a custom class to hold all the details, and fill in an instance just for the
purpose of emitting the signal.
The custom notification class must be derived from cObject. We recommend that you also
add noncopyable as a base class, because then you don’t need to write a copy constructor,
assignment operator, and dup() function, sparing some work. When emitting the signal, you
can create a temporary object and pass its pointer to the emit() function.
An example of custom notification classes are the ones associated with model change notifi-
cations (see 4.14.3). For example, the data class that accompanies a signal that announces
that a gate or gate vector is about to be created looks like this:
class cPreGateAddNotification : public cObject, noncopyable
{
public:
cModule *module;
const char *gateName;
cGate::Type gateType;
bool isVector;
};
118
OMNeT++ Simulation Manual – Simple Modules
Subscribing to Signals
The subscribe() method registers a listener for a signal. Listeners are objects that extend
the cIListener class. The same listener object can be subscribed to multiple signals. sub-
scribe() has two arguments: the signal and a pointer to the listener object:
cIListener *listener = ...;
simsignal_t lengthSignalId = registerSignal("length");
subscribe(lengthSignalId, listener);
For convenience, the subscribe() method has a variant that takes the signal name directly,
so the registerSignal() call can be omitted:
cIListener *listener = ...;
subscribe("length", listener);
One can also subscribe at other modules, not only the local one. For example, to get signals
from all parts of the model, one can subscribe at the system module level:
cIListener *listener = ...;
getSimulation()->getSystemModule()->subscribe("length", listener);
The unsubscribe() method has the same parameter list as subscribe() and unregisters
the given listener from the signal:
unsubscribe(lengthSignalId, listener);
or
unsubscribe("length", listener);
For completeness, there are methods for getting the list of signals that the component has
subscribed to (getLocalListenedSignals()) and the list of listeners for a given signal (get-
LocalSignalListeners()). The former returns a std::vector<simsignal_t>; the latter
takes a signal ID (simsignal_t) and returns a std::vector<cIListener*>.
The following example prints the number of listeners for each signal:
119
OMNeT++ Simulation Manual – Simple Modules
Listeners
Listeners are objects that subclass from the cIListener class, which declares the following
methods:
class cIListener
{
public:
virtual ~cIListener() {}
virtual void receiveSignal(cComponent *src, simsignal_t id,
bool value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
intval_t value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
uintval_t value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
double value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
simtime_t value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
const char *value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
cObject *value, cObject *details) = 0;
virtual void finish(cComponent *component, simsignal_t id) {}
virtual void subscribedTo(cComponent *component, simsignal_t id) {}
virtual void unsubscribedFrom(cComponent *component, simsignal_t id) {}
};
• Several overloaded receiveSignal() methods, one for each data type. Whenever a
signal is emitted (via emit()), the matching receiveSignal() method is invoked on the
subscribed listeners.
• finish() is called by a component on its local listeners after the component’s finish()
method was called. If the listener is subscribed to multiple signals or at multiple compo-
nents, the method will be called multiple times. Note that finish() methods in general
are not invoked if the simulation terminates with an error, so that method is not a place
for doing cleanup.
• subscribedTo(), unsubscribedFrom() are called when this listener object is sub-
scribed/unsubscribed to (from) a signal. These methods give the opportunity for lis-
teners to track whether and where they are subscribed. It is also OK for a listener to
delete itself in the last statement of the unsubscribedFrom() method, but you must be
sure that there are no other places the same listener is still subscribed.
120
OMNeT++ Simulation Manual – Simple Modules
Since cIListener has a large number of pure virtual methods, it is more convenient to
subclass from cListener, a do-nothing implementation instead. It defines finish(), sub-
scribedTo(), and unsubscribedFrom() with an empty body, and the receiveSignal()
methods with bodies that throw a "Data type not supported" error. You can redefine the
receiveSignal() method(s) whose data type you want to support, and signals emitted with
other (unexpected) data types will result in an error instead of going unnoticed.
The order in which listeners will be notified is undefined (it is not necessarily the same order
in which listeners were subscribed.)
NOTE: If your module has added listeners to other modules (e.g., the top-level module),
these listeners must be unsubscribed in the module destructor at the latest. Remember
to make sure the modules still exist before you call unsubscribe() on them, unless they
are an ancestor of your module in the module tree.
NOTE: Whenever you see a cModule*, cChannel*, cGate*, or similar pointer kept as
state in a simple module, you should think about how it will be kept up-to-date if the
model changes at runtime.
The solution is, of course, signals. OMNeT++ has two built-in signals, PRE_MODEL_CHANGE
and POST_MODEL_CHANGE (these macros are simsignal_t values, not names) that are emitted
before and after each model change.
Pre/post model change notifications are emitted with data objects that carry the details of the
change. The data classes are:
• cPreModuleAddNotification / cPostModuleAddNotification
• cPostModuleBuildNotification
• cPostComponentInitializeNotification
17 This behavior is new in OMNeT++ 6.0. Prior versions mandated that the listener be already unsubscribed from all
places when its destructor runs, but did not automatically unsubscribe.
121
OMNeT++ Simulation Manual – Simple Modules
• cPreModuleDeleteNotification / cPostModuleDeleteNotification
• cPreModuleReparentNotification / cPostModuleReparentNotification
• cPreGateAddNotification / cPostGateAddNotification
• cPreGateDeleteNotification / cPostGateDeleteNotification
• cPreGateVectorResizeNotification / cPostGateVectorResizeNotification
• cPreGateConnectNotification / cPostGateConnectNotification
• cPreGateDisconnectNotification / cPostGateDisconnectNotification
• cPrePathCreateNotification / cPostPathCreateNotification
• cPrePathCutNotification / cPostPathCutNotification
• cPreParameterChangeNotification / cPostParameterChangeNotification
• cPreDisplayStringChangeNotification / cPostDisplayStringChangeNotification
They all subclass from cModelChangeNotification, which is, of course, a cObject. Inside
the listener, you can use dynamic_cast<> to figure out what notification arrived.
NOTE: Please look up these classes in the API documentation to see their data fields,
when exactly they get fired, and what one needs to be careful about when using them.
If you’d like to get notification about the deletion of any module, you need to install the listener
on the system module:
getSimulation()->getSystemModule()->subscribe(PRE_MODEL_CHANGE, listener);
NOTE: PRE_MODEL_CHANGE and POST_MODEL_CHANGE are fired on the module (or chan-
nel) affected by the change, and not on the module which executes the code that causes
the change. For example, pre-module-deleted is fired on the module to be removed, and
post-module-deleted is fired on its parent (because the original module no longer exists),
and not on the module that contains the deleteModule() call.
122
OMNeT++ Simulation Manual – Simple Modules
NOTE: A listener will not receive pre/post-module-deleted notifications if the whole sub-
module tree that contains the subscription point is deleted. This is because compound
module destructors begin by unsubscribing all modules/channels in the subtree before
starting recursive deletion.
4.15.1 Motivation
One use of signals is to expose variables for result collection without specifying where, how,
and whether to record them. With this approach, modules only publish the variables, and
the actual result recording takes place in listeners. Listeners may be added by the simulation
framework (based on the configuration) or by other modules (for example, by dedicated result
collection modules).
The signals approach allows for several possibilities:
• Provides a controllable level of detail: in some simulation runs, you may want to record
all values as a time series; in other runs, you may only want to record the mean, time
average, minimum/maximum value, standard deviation, etc.; and in yet other runs, you
may want to record the distribution as a histogram.
• Depending on the purpose of the simulation experiment, you may want to process the
results before recording them. For example, you may want to record a smoothed or
filtered value, the percentage of time the value is nonzero or over a threshold, the sum
of the values, etc.
• You may want aggregate statistics, such as recording the total number of packet drops
or the average end-to-end delay for the entire network.
• You may want to record combined statistics, for example, a drop percentage (drop coun-
t/total number of packets).
• You may want to ignore results generated during the warm-up period or during other
transients.
Introduction
In order to record simulation results based on signals, you need to add @statistic properties
to the NED definition of the simple module or channel. A @statistic property defines the
name of the statistic, which signal(s) are used as input, what processing steps are to be applied
to them (e.g., smoothing, filtering, summing, differential quotient), and what properties are to
be recorded (minimum, maximum, average, etc.) and in which form (vector, scalar, histogram).
Record items can be marked optional, which allows you to denote a “default” and a more
comprehensive “all” result set to be recorded. The list of record items can be further tweaked
from the configuration. You can also specify a descriptive name (“title”) for the statistic, as
well as a measurement unit.
The following example declares a queue module with a queue length statistic:
123
OMNeT++ Simulation Manual – Simple Modules
simple Queue
{
parameters:
@statistic[queueLength](record=max,timeavg,vector?);
gates:
input in;
output out;
}
As you can see, statistics are represented with indexed NED properties (see 3.12). The prop-
erty name is always statistic, and the index (here, queueLength) is the name of the statis-
tic. The property value, that is, everything inside the parentheses, provides hints and extra
information for recording.
The above @statistic declaration assumes that the module’s C++ code emits the queue’s
updated length as signal queueLength whenever elements are inserted into the queue or
removed from it. By default, the maximum and the time average of the queue length will be
recorded as scalars. You can also instruct the simulation to record “all” results, which will
turn on optional record items marked with a question mark. In this case, the queue lengths
will also be recorded into an output vector.
NOTE: The configuration lets you fine-tune the list of result items even beyond the
default and all settings. See section 12.2.3 for more information.
In the above example, the signal to be recorded was taken from the statistic name. However, if
this is not suitable, you can use the source property key to specify a different signal as input
for the statistic. The following example assumes that the C++ code emits a qlen signal and
declares a queueLength statistic based on that:
simple Queue
{
parameters:
@signal[qlen](type=int); // optional
@statistic[queueLength](source=qlen; record=max,timeavg,vector?);
...
}
Note that the source=qlen property key has been added to specify the qlen signal as the
input for the statistic. Additionally, a signal declaration (@signal property) has been added
for the qlen signal. Although signal declarations are currently optional and ignored by the
system, it is good practice to include them.
You can also apply processing to a signal before recording it. Consider the following example:
@statistic[dropCount](source=count(drop); record=last,vector?);
This records the total number of packet drops as a scalar and, optionally, the number of
packets dropped over time as a vector. This assumes that the C++ code emits a drop signal
every time a packet is dropped. Here, count() is a result filter.
NOTE: Starting from OMNeT++ 4.4, items containing parentheses (e.g., count(drop))
no longer need to be enclosed in quotation marks.
Another example:
124
OMNeT++ Simulation Manual – Simple Modules
@statistic[droppedBytes](source=sum(packetBytes(pkdrop)); record=last,vector?);
This assumes that the C++ code emits a pkdrop signal with a cPacket pointer as the value.
Based on that signal, it records the total number of bytes dropped as a scalar and optionally
as a vector. The packetBytes() filter extracts the number of bytes from each packet using
the getByteLength() method in cPacket, and the sum() filter sums up the values.
Arithmetic expressions can also be used. For example, the following line computes the number
of dropped bytes using the packetBits() filter:
@statistic[droppedBytes](source=sum(8*packetBits(pkdrop)); record=last,
vector?);
When using multiple signals, a value arriving on either signal will result in one output value.
The computation will use the last values of the other signals (sample-hold interpolation).
However, the same signal cannot occur twice, as it would cause glitches in the output.
Record items can also be expressions and contain filters. For example, the following statistic
is equivalent to one of the previous examples. It computes and records the total number of
bytes dropped, using a cPacket*-valued signal as input. However, some of the computations
have been moved to the recorder part.
@statistic[droppedBytes](source=packetBits(pkdrop); record=last(8*sum),
vector(8*sum)?);
Property Keys
source : Defines the input for the recorders (see the record= key). If omitted, the statistic
name is taken as the signal name.
record : Contains a list of recording modes, separated by commas. Recording modes define
how to record the source (see the source= key).
title : A longer, descriptive name for the statistic signal. Result visualization tools may use it
as the chart label (e.g., in the legend).
unit : The unit of measurement for the values. This may also appear in charts.
interpolationmode : Defines how to interpolate signal values where needed (e.g., for draw-
ing). Possible values are none, sample-hold, backward-sample-hold, linear.
enum : Defines symbolic names for various integer signal values. The property value must be
a string containing name=value pairs separated by commas. For example: "IDLE=1,BUSY=2,DOWN=3
The following table contains a list of predefined result filters. All filters in the table output a
value for each input value.
125
OMNeT++ Simulation Manual – Simple Modules
Filter Description
count Computes and outputs the count of values received so far.
sum Computes and outputs the sum of values received so far.
min Computes and outputs the minimum of values received so
far.
max Computes and outputs the maximum of values received so
far.
mean Computes and outputs the average (sum / count) of values
received so far.
timeavg Regards the input values and their timestamps as a step
function (sample-hold style), and computes and outputs
their time average (integral divided by duration).
constant0 Outputs a constant 0 for each received value (independent
of the value).
constant1 Outputs a constant 1 for each received value (independent
of the value).
packetBits Expects cPacket pointers as values and outputs the bit
length for each received one. Non-cPacket values are ig-
nored.
packetBytes Expects cPacket pointers as values and outputs the byte
length for each received one. Non-cPacket values are ig-
nored.
sumPerDuration For each value, computes the sum of values received so
far, divides it by the duration, and outputs the result.
removeRepeats Removes repeated values, i.e., discards values that are the
same as the previous one.
Recorder Description
last Records the last value into an output scalar.
count Records the count of the input values into an output
scalar; functionally equivalent to last(count).
sum Records the sum of the input values into an output scalar
(or zero if there were none); functionally equivalent to
last(sum).
min Records the minimum of the input values into an output
scalar (or positive infinity if there were none); functionally
equivalent to last(min).
max Records the maximum of the input values into an output
scalar (or negative infinity if there were none); functionally
equivalent to last(max).
mean Records the mean of the input values into an output scalar
(or NaN if there were none); functionally equivalent to
last(mean).
timeavg Regards the input values with their timestamps as a step
function (sample-hold style), and records the time aver-
age of the input values into an output scalar; functionally
equivalent to last(timeavg).
126
OMNeT++ Simulation Manual – Simple Modules
NOTE: You can print the list of available result filters and result recorders by executing
the opp_run -h resultfilters and opp_run -h resultrecorders commands.
The names of recorded result items are formed by concatenating the statistic name and the
recording mode with a colon between them: "<statisticName>:<recordingMode>".
Thus, the following statistics
@statistic[dropRate](source=count(drop)/count(pk); record=last,vector?);
@statistic[droppedBytes](source=packetBytes(pkdrop); record=sum,vector(sum)?);
will produce the following scalars: dropRate:last, droppedBytes:sum, and the following
vectors: dropRate:vector, droppedBytes:vector(sum).
All property keys (except for record) are recorded as result attributes into the vector file
or scalar file. The title property will be modified slightly before recording, by adding the
recording mode after a comma. Otherwise, all result items saved from the same statistic
would have exactly the same name. Examples: "Dropped Bytes, sum", "Dropped Bytes,
vector(sum)".
It is allowed to use other property keys as well, but they won’t be interpreted by the OMNeT++
runtime or the result analysis tool.
To fully understand source and record, it is useful to see how result recording is set up.
When a module or channel is created in the simulation, the OMNeT++ runtime checks the
@statistic properties on its NED declaration and adds listeners to the signals mentioned
as input. There are two types of listeners associated with result recording: result filters and
result recorders. Result filters can be chained, and at the end of the chain, there is always
a recorder. So, there may be a recorder directly subscribed to a signal, or there may be a
chain of one or more filters plus a recorder. You can think of it as a pipeline or a “pipe tree”,
where the tree roots are signals, the leaves are result recorders, and the intermediate nodes
are result filters.
Result filters typically perform some processing on the values they receive on their inputs
(from the previous filter in the chain or directly from the signal) and propagate them to their
outputs (to chained filters and recorders). A filter may also discard values (i.e., not propagate
them). Recorders may write the received values into an output vector or record output scalar(s)
at the end of the simulation.
127
OMNeT++ Simulation Manual – Simple Modules
Many operations exist in both filter and recorder form. For example, the sum filter passes on
the sum of the values received on its input to its output, while the sum recorder computes the
sum of the received values to record it as an output scalar on simulation completion.
The next figure illustrates which filters and recorders are created and how they are connected
for the following statistics:
@statistic[droppedBits](source=8*packetBytes(pkdrop); record=sum,vector(sum));
sum
sum
Figure 4.4: Result filters and recorders chained
HINT: To see how result filters and recorders are set up for a particular simulation,
run the simulation with the debug-statistics-recording configuration option. For
example, specify -debug-statistics-recording=true on the command line.
The demux result filter in OMNeT++ provides a mechanism for recording a breakdown of simu-
lation results based on runtime attributes. It facilitates the separation of results into multiple
streams or categories, leveraging the properties of emitted signals. This is particularly advan-
tageous in scenarios with multiple interacting entities or modules.
The demux filter works by demultiplexing its input into several outputs, dynamically creating
new outputs as required. The filter uses the name string of the details object associated
with the emitted signal as the selector for this demultiplexing process. This capability enables
dynamic categorization of statistics based on runtime conditions, such as signal sources.
Let’s consider a practical example. Suppose we have a network simulation where a sink mod-
ule receives packets from multiple senders. We’d like to separately record the total number of
bytes received from each sender.
First, we define a sink module in NED, which is equipped with a signal and a statistic that
uses the demux filter:
simple Sink {
@signal[packetReceived];
@statistic[bytesReceivedPerSender](source=packetReceived;record=sum(demux));
}
128
OMNeT++ Simulation Manual – Simple Modules
Next, we implement the sink module to emit a signal each time it receives a packet, tagging
the emission with the sender’s name:
class Sink : public cSimpleModule {
protected:
virtual void handleMessage(cMessage *msg) override {
if (msg->isPacket()) {
cPacket *pkt = check_and_cast<cPacket *>(msg);
static simsignal_t packetReceivedSignal = registerSignal("packetReceive
const char *senderName = pkt->getSendingModule()->getFullName();
cNamedObject senderDetails(senderName);
emit(packetReceivedSignal, pkt->getByteLength(), &senderDetails);
delete msg;
}
}
};
With the demux filter, the generated statistic names will include the demux label (i.e., the
sender’s name), resulting in statistic names such as:
• bytesReceivedPerSender:Sender1:sum
• bytesReceivedPerSender:Sender2:sum
• ...
Each statistic records the sum of the bytes received from its corresponding sender, providing
a detailed breakdown of the data volume by source.
It is often convenient to have a module record statistics per session, per connection, per client,
etc. One way to handle this is by registering signals dynamically (e.g., session1-jitter,
session2-jitter, ...), and setting up @statistic-style result recording for each.
The NED file would look like this:
@signal[session*-jitter](type=simtime_t); // note the wildcard
@statisticTemplate[sessionJitter](record=mean,vector?);
In the C++ code of the module, you need to register each new signal with registerSignal()
and, in addition, inform OMNeT++ to set up statistics recording for it as described by the
@statisticTemplate property. This can be done by calling getEnvir()->addResultRecorders().
char signalName[32];
sprintf(signalName, "session%d-jitter", sessionNum);
simsignal_t signal = registerSignal(signalName);
char statisticName[32];
sprintf(statisticName, "session%d-jitter", sessionNum);
cProperty *statisticTemplate =
getProperties()->get("statisticTemplate", "sessionJitter");
getEnvir()->addResultRecorders(this, signal, statisticName, statisticTemplate);
129
OMNeT++ Simulation Manual – Simple Modules
In the @statisticTemplate property, the source key will be ignored (as the parameter signal
will be used as the source). The actual name and index of the property will also be ignored.
(In the case of @statistic, the index holds the result name, but here the name is explicitly
specified in the statisticName parameter.)
When recording multiple signals using a common @statisticTemplate, you may want the
titles of the recorded statistics to differ for each signal. This can be achieved by using dollar
variables in the title key of the @statisticTemplate. The following variables are available:
The following code example sets up recording to an output vector after removing duplicate
values. It is essentially equivalent to the following @statistic line:
@statistic[queueLength](source=qlen; record=vector(removeRepeats);
title="Queue Length"; unit=packets);
cResultFilter *warmupFilter =
cResultFilterType::get("warmup")->create();
cResultFilter *removeRepeatsFilter =
cResultFilterType::get("removeRepeats")->create();
cResultRecorder *vectorRecorder =
cResultRecorderType::get("vector")->create();
opp_string_map *attrs = new opp_string_map;
(*attrs)["title"] = "Queue Length";
(*attrs)["unit"] = "packets";
cResultRecorder::Context ctx { this, "queueLength", "vector",
nullptr, attrs};
130
OMNeT++ Simulation Manual – Simple Modules
vectorRecorder->init(&ctx);
subscribe(signal, warmupFilter);
warmupFilter->addDelegate(removeRepeatsFilter);
removeRepeatsFilter->addDelegate(vectorRecorder);
Emitting signals for statistical purposes is not much different from emitting signals for any
other purpose. Statistic signals are primarily expected to contain numeric values, so the
overloaded emit() functions that take long, double, and simtime_t are typically used.
Emitting with a timestamp. By default, the emitted values are associated with the current
simulation time. However, there might be cases where you want to associate the values with a
different timestamp. For example, you may want to associate values with past timestamps, as
is done with the recordWithTimestamp() method of cOutVector (see 7.10.1). This situation
can arise when, for example, you want to emit a value with a timestamp that reflects the start
of an event, even though the event’s outcome (the value) can only be known after the event
has completed.
To emit a value with a different timestamp, you need to construct an object that contains a
(timestamp, value) pair, and use the emit(simsignal_t, cObject*) method to emit it. The
cTimestampedValue class provides this functionality, with two public data members: time
(of type simtime_t) and value (of type double). It also has a convenience constructor that
takes these two values.
NOTE: cTimestampedValue is not part of the signal mechanism per se. However, the
result recording listeners provided by OMNeT++ are designed to understand cTimes-
tampedValue and know how to handle it.
If performance is critical, you can make the cTimestampedValue object a class member or a
static variable to eliminate the construction/destruction time.18
Timestamps must be monotonically increasing.
Emitting non-numeric values. Sometimes, it is practical to have multi-purpose signals or
retrofit an existing non-statistical signal so that it can be recorded as a result. For this reason,
signals with non-numeric types (i.e., const char * and cObject *) may also be recorded
as results. The built-in result recording listeners follow these rules when interpreting non-
numeric values:
there isn’t a listener somewhere that would modify the same static variable during the firing process.
131
OMNeT++ Simulation Manual – Simple Modules
• Other objects are recorded as 1.0, except for nullptr, which is recorded as 0.0.
cITimestampedValue is a C++ interface that can be used as an additional base class for any
class. It is declared as follows:
class cITimestampedValue {
public:
virtual ~cITimestampedValue() {}
virtual double getSignalValue(simsignal_t signalID) = 0;
virtual simtime_t getSignalTime(simsignal_t signalID);
};
The getSignalValue() function is pure virtual (i.e., it must return some value), but the
getSignalTime() function has a default implementation that returns the current simulation
time. Note that the signalID argument allows the same class to serve multiple signals (i.e.,
to return different values for each).
You can define your own result filters and recorders in addition to the built-in ones. To do
this, you need to write the implementation in C++ and register it with a macro to let OMNeT++
know about it. The new result filter or recorder can then be used in the source= and record=
attributes of @statistic properties, just like the built-in ones.
Result filters must be subclassed from cResultFilter or one of its more specific subclasses
(cNumericResultFilter and cObjectResultFilter). The new result filter class needs to be
registered using the Register_ResultFilter(NAME, CLASSNAME) macro.
Similarly, a result recorder must be subclassed from cResultRecorder or the more specific
cNumericResultRecorder class, and be registered using the Register_ResultRecorder(NAME,
CLASSNAME) macro.
Here is an example implementation of a result filter taken from the simulation runtime:
/**
* Filter that outputs the sum of signal values divided by the measurement
* interval (simtime minus warmup period).
*/
class SumPerDurationFilter : public cNumericResultFilter
{
protected:
double sum;
protected:
virtual bool process(simtime_t& t, double& value, cObject *details);
public:
SumPerDurationFilter() {sum = 0;}
};
Register_ResultFilter("sumPerDuration", SumPerDurationFilter);
132
OMNeT++ Simulation Manual – Simple Modules
cIListener
cResultListener
cResultFilter cResultRecorder
CountFilter, CountR
cNumericResultFilter cObjectResultFilter cNumericResultRecorder
...
VectorRecorder,
LastValueRecorder,
SumFilter,
HistogramRecorder,
MinFilter, PacketBitsFilter,
SumRecorder,
MaxFilter, PacketBytesFilter,
MinRecorder,
TimeAverageFilter, ...
MaxRecorder,
...
TimeAverageRecorder,
...
return true;
}
133
OMNeT++ Simulation Manual – Simple Modules
134
OMNeT++ Simulation Manual – Messages and Packets
Chapter 5
5.1 Overview
Messages are a central concept in OMNeT++. In the model, message objects represent events,
packets, commands, jobs, customers, or other kinds of entities, depending on the model
domain.
Messages are represented with the cMessage class and its subclass cPacket. cPacket is
used for network packets (frames, datagrams, transport packets, etc.) in a communication
network, and cMessage is used for everything else. Users are free to subclass both cMessage
and cPacket to create new types and to add data.
cMessage has the following fields; some are used by the simulation kernel, and others are
provided for the convenience of the simulation programmer:
• The name field is a string (const char *), which can be freely used by the simulation
programmer. The message’s name is displayed in many places in the graphical runtime
interface, so it is generally useful to choose a descriptive name. The message’s name is
inherited from cObject (see section 7.1.2).
• Message kind is an integer field. Some negative values are reserved by the simulation
library, but zero and positive values can be freely used in the model for any purpose.
The message kind is typically used to carry a value that conveys the role, type, category,
or identity of the message.
• The scheduling priority field is used by the simulation kernel to determine the delivery
order of messages that have the same arrival time values. This field is rarely used in
practice.
• The send time, arrival time, source module, source gate, destination module, destination
gate fields store information about the message’s last sending or scheduling, and should
not be modified from the model. These fields are primarily used internally by the simu-
lation kernel while the message is in the future events set (FES), but the information is
still in the message object when the message is delivered to a module.
• Time stamp (not to be confused with arrival time) is a utility field that the programmer
can freely use for any purpose. The time stamp is not examined or changed by the
simulation kernel at all.
135
OMNeT++ Simulation Manual – Messages and Packets
• The parameter list, control info, and context pointer fields make some simulation tasks
easier to program, and they will be discussed later.
The cPacket class extends cMessage with fields that are useful for representing network
packets:
• The packet length field represents the length of the packet in bits. It is used by the
simulation kernel to compute the transmission duration when a packet travels through
a connection that has an assigned data rate, and also for error modeling on channels
with a nonzero bit error rate.
• The encapsulated packet field helps in modeling protocol layers by supporting the con-
cept of encapsulation and decapsulation.
• The bit error flag field carries the result of error modeling after the packet is sent through
a channel that has a nonzero packet error rate (PER) or bit error rate (BER). It is up to
the receiver to examine this flag after receiving the packet and to act upon it.
• The duration field carries the transmission duration after the packet was sent through a
channel with a data rate.
• The is-reception-start flag tells whether this packet represents the start or the end of the
reception after the packet has travelled through a channel with a data rate. This flag is
controlled by the deliver-on-reception-start flag of the receiving gate.
The cMessage constructor accepts an object name and a message kind, both optional:
cMessage(const char *name=nullptr, short kind=0);
Descriptive message names can be very useful when tracing, debugging or demonstrating the
simulation, so it is recommended to use them. Message kind is usually initialized with a sym-
bolic constant (e.g. an enum value) which signals what the message object represents. Only
positive values and zero can be used – negative values are reserved for use by the simulation
kernel.
The following lines show some examples of message creation:
cMessage *msg1 = new cMessage();
cMessage *msg2 = new cMessage("timeout");
cMessage *msg3 = new cMessage("timeout", KIND_TIMEOUT);
Once a message has been created, its basic data members can be set with the following
methods:
void setName(const char *name);
void setKind(short k);
void setTimestamp();
void setTimestamp(simtime_t t);
void setSchedulingPriority(short p);
136
OMNeT++ Simulation Manual – Messages and Packets
The getName()/setName() methods are inherited from a generic base class in the simulation
library, cNamedObject.
Two more interesting methods:
The isPacket() method returns true if the particular message object is a subclass of cPacket,
and false otherwise. As isPacket() is implemented as a virtual function that just con-
tains a return false or a return true statement, it might be faster than calling dy-
namic_cast<cPacket*>.
The getCreationTime() method returns the creation time of the message. It is worthwhile
to mention that with cloned messages (see dup() later), the creation time of the original
message is returned and not the time of the cloning operation. This is particularly useful when
modeling communication protocols, because many protocols clone the transmitted packages
to be able to do retransmissions and/or segmentation/reassembly.
It is often necessary to duplicate a message or a packet, for example, to send one and keep a
copy. Duplication can be done in the same way as for any other OMNeT++ object:
The resulting message (or packet) will be an exact copy of the original including message
parameters and encapsulated messages, except for the message ID field. The creation time
field is also copied, so for cloned messages getCreationTime() will return the creation time
of the original, not the time of the cloning operation. 1
When subclassing cMessage or cPacket, one needs to reimplement dup(). The recommended
implementation is to delegate to the copy constructor of the new class:
1 Note, however, that the simulation library may delay the duplication of the encapsulated message until it is really
137
OMNeT++ Simulation Manual – Messages and Packets
Every message object has a unique numeric message ID. It is normally used for identifying
the message in a recorded event log file, but may occasionally be useful for other purposes as
well. When a message is cloned (msg->dup()), the clone will have a different ID.
There is also another ID called tree ID. The tree ID is initialized to the message ID. However,
when a message is cloned, the clone will retain the tree ID of the original. Thus, messages
that have been created by cloning the same message or its clones will have the same tree
ID. Message IDs are of the type long, which is is usually enough so that IDs remain unique
during the simulation run (i.e. the counter does not wrap).
The methods for obtaining message IDs:
long getId() const;
long getTreeId() const;
One of the main application areas of OMNeT++ is the simulation of telecommunication net-
works. Here, protocol layers are usually implemented as modules which exchange packets.
Packets themselves are represented by messages subclassed from cPacket.
However, communication between protocol layers requires sending additional information to
be attached to packets. For example, a TCP implementation sending down a TCP packet to
IP will want to specify the destination IP address and possibly other parameters. When IP
passes up a packet to TCP after decapsulation from the IP header, it will want to let TCP know
at least the source IP address.
This additional information is represented by control info objects in OMNeT++. Control info
objects have to be subclassed from cObject (a small footprint base class with no data mem-
bers), and can be attached to any message. cMessage has the following methods for this
purpose:
void setControlInfo(cObject *controlInfo);
cObject *getControlInfo() const;
cObject *removeControlInfo();
When a "command" is associated with the message sending (such as TCP OPEN, SEND,
CLOSE, etc), the message kind field (getKind(), setKind() methods of cMessage) should
carry the command code. When the command doesn’t involve a data packet (e.g. TCP CLOSE
command), a dummy packet (empty cMessage) can be sent.
An object set as control info via setControlInfo() will be owned by the message object.
When the message is deallocated, the control info object is deleted as well.
The following methods return the sending and arrival times that correspond to the last sending
of the message.
simtime_t getSendingTime() const;
simtime_t getArrivalTime() const;
138
OMNeT++ Simulation Manual – Messages and Packets
The following methods can be used to determine where the message came from and which
gate it arrived on (or will arrive if it is currently scheduled or under way.) There are two sets
of methods, one returning module/gate Ids, and the other returning pointers.
int getSenderModuleId() const;
int getSenderGateId() const;
int getArrivalModuleId() const;
int getArrivalGateId() const;
cModule *getSenderModule() const;
cGate *getSenderGate() const;
cModule *getArrivalModule() const;
cGate *getArrivalGate() const;
There are further convenience functions to tell whether the message arrived on a specific gate
given with id or with name and index.
bool arrivedOn(int gateId) const;
bool arrivedOn(const char *gatename) const;
bool arrivedOn(const char *gatename, int gateindex) const;
Display strings affect the message’s visualization in graphical user interfaces like Qtenv. Mes-
sage objects do not store a display string by default, but contain a getDisplayString()
method that can be overridden in subclasses to return the desired string. The method:
const char *getDisplayString() const;
5.3 Self-Messages
Messages are often used to represent events internal to a module, such as a periodically firing
timer to represent the expiry of a timeout. A message is termed a self-message when it is used
in such a scenario – otherwise, self-messages are normal messages of the cMessage class or
a class derived from it.
When a message is delivered to a module by the simulation kernel, the isSelfMessage()
method can be used to determine if it is a self-message; that is, whether it was scheduled
with scheduleAt(), or sent with one of the send...() methods. The isScheduled() method
returns true if the message is currently scheduled. A scheduled message can also be cancelled
using cancelEvent().
bool isSelfMessage() const;
bool isScheduled() const;
139
OMNeT++ Simulation Manual – Messages and Packets
The methods getSendingTime() and getArrivalTime() are also useful with self-messages:
they return the time the message was scheduled and arrived (or will arrive; while the message
is scheduled, arrival time is the time it will be delivered to the module).
The cMessage class contains a context pointer of type void*, which can be accessed by the
following functions:
void setContextPointer(void *p);
void *getContextPointer() const;
The context pointer is not used or memory-managed by the simulation kernel. It is typically
used in modules that manage multiple self-messages or timers to distinguish which specific
timer has triggered upon message arrival. By pointing to a module’s internal data structure,
this pointer can convey essential information about the event’s context.
The cPacket constructor is similar to the cMessage constructor, but it accepts an additional
bit length argument:
cPacket(const char *name=nullptr, short kind=0, int64 bitLength=0);
The most important field that cPacket has over cMessage is the message length. This field is
kept in bits, but it can also be set/get in bytes. If the bit length is not a multiple of eight, the
getByteLength() method will round it up.
void setBitLength(int64_t l);
void setByteLength(int64_t l);
void addBitLength(int64_t delta);
void addByteLength(int64_t delta);
int64_t getBitLength() const;
int64_t getByteLength() const;
Another extra field is the bit error flag. It can be accessed with the following methods:
void setBitError(bool e);
bool hasBitError() const;
In the OMNeT++ protocol models, the protocol type is usually represented in the message
subclass. For example, instances of the IPv6Datagram class represent IPv6 datagrams and
EthernetFrame represents Ethernet frames. The C++ dynamic_cast operator can be used to
determine if a message object is of a specific protocol.
An example:
140
OMNeT++ Simulation Manual – Messages and Packets
When a packet has been received, some information can be obtained about the transmission,
namely the transmission duration and the is-reception-start flag. They are returned by the
following methods:
simtime_t getDuration() const;
bool isReceptionStart() const;
The encapsulate() function encapsulates a packet into another one. The length of the packet
will grow by the length of the encapsulated packet. An exception: when the encapsulating
(outer) packet has zero length, OMNeT++ assumes it is not a real packet but an out-of-band
signal, so its length is left at zero.
A packet can only hold one encapsulated packet at a time; the second encapsulate() call
will result in an error. It is also an error if the packet to be encapsulated is not owned by the
module.
Decapsulation, that is, removing the encapsulated packet, is done by the decapsulate()
method. decapsulate() will decrease the length of the packet accordingly, except if it was
zero. If the length would become negative, an error occurs.
The getEncapsulatedPacket() function returns a pointer to the encapsulated packet, or
nullptr if no packet is encapsulated.
Example usage:
cPacket *data = new cPacket("data");
data->setByteLength(1024);
udp->encapsulate(data);
EV << udp->getByteLength() << endl; // --> 8+1024 = 1032
141
OMNeT++ Simulation Manual – Messages and Packets
Since the 3.2 release, OMNeT++ implements reference counting of encapsulated packets,
meaning that when a packet containing an encapsulated packet is cloned (dup()), the en-
capsulated packet will not be duplicated, only a reference count is incremented. Duplication
of the encapsulated packet is deferred until decapsulate() actually gets called. If the outer
packet is deleted without its decapsulate() method ever being called, then the reference
count of the encapsulated packet is simply decremented. The encapsulated packet is deleted
when its reference count reaches zero.
Reference counting can significantly improve performance, especially in LAN and wireless
scenarios. For example, in the simulation of a broadcast LAN or WLAN, the IP, TCP and
higher layer packets won’t be duplicated (and then discarded without being used) if the MAC
address doesn’t match in the first place.
The reference counting mechanism works transparently. However, there is one implication:
one must not change anything in a packet that is encapsulated into another! That is,
getEncapsulatedPacket() should be viewed as if it returned a pointer to a read-only object
(it returns a const pointer indeed), for quite obvious reasons: the encapsulated packet may
be shared between several packets, and any change would affect those other packets as well.
The cPacket class does not directly support encapsulating more than one packet, but one
can subclass cPacket or cMessage to add the necessary functionality.
Encapsulated packets can be stored in a fixed-size or a dynamically allocated array, or in a
standard container like std::vector. In addition to storage, object ownership needs to be
taken care of as well. The message class has to take ownership of the inserted messages, and
release them when they are removed from the message. These tasks are done via the take()
and drop() methods.
Here is an example that assumes that the class has an std::list member called messages
for storing message pointers:
One also needs to provide an operator=() method to make sure that message objects are
copied and duplicated properly. Section 7.13 covers requirements and conventions associated
with deriving new classes in more detail.
142
OMNeT++ Simulation Manual – Messages and Packets
The cMessage class has an internal cArray object that can carry objects. Only objects de-
rived from cObject can be attached. The addObject(), getObject(), hasObject(), and
removeObject() methods use the object’s name (as returned by the getName() method) as
the key to the array.
An example where the sender attaches an object, and the receiver checks for the object’s
existence and obtains a pointer to it:
// sender:
cHistogram *histogram = new cHistogram("histogram");
msg->addObject(histogram);
// receiver:
if (msg->hasObject("histogram")) {
cObject *obj = msg->getObject("histogram");
cHistogram *histogram = check_and_cast<cHistogram *>(obj);
...
}
One needs to ensure that the names of the attached objects don’t conflict with each other.
Note that message parameters (cMsgPar, see the next section) are also attached in the same
way, so their names also count.
When no objects are attached to a message (and getParList() is not invoked), the internal
cArray object is not created. This saves both storage and execution time.
Non-cObject data can be attached to messages by wrapping them into cObject, for example
into cMsgPar, which has been designed specifically for this purpose. cMsgPar will be covered
in the next section.
The preferred way to extend messages with new data fields is to use message definitions (see
chapter 6).
The old and deprecated way of adding new fields to messages is by attaching cMsgPar ob-
jects. There are several downsides to this approach, with the worst being large memory and
execution time overhead. cMsgPars are heavyweight and fairly complex objects themselves.
It has been reported that using cMsgPar message parameters might account for a large part
of execution time, sometimes as much as 80%. Using cMsgPar is also error-prone because
cMsgPar objects have to be added dynamically and individually to each message object. In
contrast, subclassing benefits from static type checking: if one mistypes the name of a field
in the C++ code, the compiler can detect the mistake.
If one still needs cMsgPars for some reason, here is a short summary. At the sender side,
one can add a new named parameter to the message with the addPar() member function,
143
OMNeT++ Simulation Manual – Messages and Packets
and then set its value with one of the methods setBoolValue(), setLongValue(), set-
StringValue(), setDoubleValue(), setPointerValue(), setObjectValue(), and setXM-
LValue(). There are also overloaded assignment operators for the corresponding C/C++
types.
At the receiver side, one can look up the parameter object on the message by name and
obtain a reference to it with the par() member function. hasPar() can be used to first check
whether the message object has a parameter object with the given name. Then the value
can be read with the methods boolValue(), longValue(), stringValue(), doubleValue(),
pointerValue(), objectValue(), xmlValue(), or by using the provided overloaded type
cast operators.
Example usage:
msg->addPar("destAddr");
msg->par("destAddr").setLongValue(168);
...
long destAddr = msg->par("destAddr").longValue();
144
OMNeT++ Simulation Manual – Message Definitions
Chapter 6
Message Definitions
6.1 Introduction
In practice, various fields need to be added to cMessage or cPacket to make them useful.
For example, when modeling communication networks, message/packet objects need to carry
protocol header fields. Since the simulation library is written in C++, the natural way to
extend cMessage/cPacket is by subclassing them. However, at least three items have to be
added to the new class for each field (a private data member, a getter, and a setter method),
and the resulting class needs to integrate with the simulation framework. This means that
writing the necessary C++ code can be a tedious and time-consuming task.
OMNeT++ offers a more convenient way called message definitions. Message definitions pro-
vide 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.
Let us begin with a simple example. Suppose we need a packet type that carries a source and
a destination address as well as a hop count. The corresponding C++ code can be generated
from the following definition in a MyPacket.msg file:
packet MyPacket
{
int srcAddress;
int destAddress;
int remainingHops = 32;
};
145
OMNeT++ Simulation Manual – Message Definitions
As you can see, for each field, the generated class contains a protected data member, and
a public getter and setter method. The names of the methods will begin with get and set,
followed by the field name with its first letter converted to uppercase.
The MyPacket_m.cc file contains the implementation of the generated MyPacket class as well
as “reflection” code (see cClassDescriptor) that allows inspection of these data structures
under graphical user interfaces like Qtenv. The MyPacket_m.cc file should be compiled and
linked into the simulation; this is normally taken care of automatically.
To use the MyPacket class from a C++ source file, the generated header file needs to be
included:
#include "MyPacket_m.h"
...
MyPacket *pkt = new MyPacket("pkt");
pkt->setSrcAddress(localAddr);
...
• Packet, message, and class definitions are translated into C++ class definitions. The
three types are very similar; they practically only differ in the choice of the default base
class (cPacket, cMessage, and no base class, respectively).
• Struct definitions are translated into C-like structs, where fields are represented with
public data members (there are no getters and setters).
• Enum definitions are translated into C++ enums.
• Namespace declarations define the namespace for subsequent definitions.
146
OMNeT++ Simulation Manual – Message Definitions
• Properties are metadata annotations of the syntax @name or @name(...) that may occur
at the file, class (packet, struct, etc.) definition, and field level as well. There are many
predefined properties, and a large subset of them deals with the details of what C++
code to generate for the item they occur with. For example, @getter(getFoo) on a field
requests that the generated getter function have the name getFoo.
• C++ blocks are used for injecting literal C++ code fragments into the generated source
files. The target (the place where to insert the code) can be specified.
For packet, the default base class is cPacket; or if a base class is explicitly named, it must
be a subclass of cPacket. Similarly, for message, the default base class is cMessage, or if a
base class is specified, it must be a subclass of cMessage.
For class, the default base class is none. However, it is often a good idea to choose cObject
as the base class.1
NOTE: It is recommended to use cObject as the base class because it adds zero overhead
to the generated class and, at the same time, makes the class more interoperable with
the rest of the simulation library. cObject only defines virtual methods but no data
members, so the only overhead would be the vptr; however, the generated class already
has a vptr because the generated methods are also virtual.
The base class is specified with the extends keyword. For example:
packet FooPacket extends PacketBase
{
...
};
one needs to add extends cObject to class definitions lacking an "extends" clause.
147
OMNeT++ Simulation Manual – Message Definitions
The generated class will have a constructor and also a copy constructor. An assignment
operator (operator=()) and a cloning method (dup()) will also be generated.
The argument list of the generated constructor depends on the base class. For classes derived
from cMessage, it will accept an object name and message kind. For classes derived from
cNamedObject, it will accept an object name. The arguments are optional (they have default
values).
class FooPacket : public PacketBase
{
public:
FooPacket(const char *name=nullptr, int kind=0);
FooPacket(const FooPacket& other);
FooPacket& operator=(const FooPacket& other);
virtual FooPacket *dup() const;
...
Additional base classes can be added by listing them in the @implements class property.
6.2.2 Structs
Message definitions allow you to define C-style structs, where “C-style” means “containing
only data and no methods”. These structs can be useful as fields in message classes.
The syntax is similar to that of defining messages:
struct Place
{
int type;
string description;
double coords[3];
};
The generated struct has public data members and no getter or setter methods. The following
code is generated from the above definition:
// generated C++
struct Place
{
int type;
omnetpp::opp_string description;
double coords[3];
};
Note that string fields are generated with the opp_string C++ type, which is a minimalistic
string class that wraps const char* and takes care of allocation/deallocation. It was chosen
instead of std::string because of its significantly smaller memory footprint. (std::string
is significantly larger than a const char* pointer because it also needs to store length and
capacity information in some form.)
Inheritance is supported for structs:
148
OMNeT++ Simulation Manual – Message Definitions
struct Base
{
...
};
6.3 Enums
An enum is declared with the enum keyword, using the following syntax:
enum PayloadType
{
NONE = 0;
VOICE = 1;
VIDEO = 2;
DATA = 3;
};
The second way is to tag a field of the type int or any other integral type with the @enum
property and the name of the enum, like this:
packet FooPacket
{
int16_t payloadType @enum(PayloadType);
};
In the generated C++ code, the field will have the original type (in this case, int16_t). How-
ever, additional code generated by the message compiler will allow Qtenv to display the sym-
bolic name of the field’s value in addition to the numeric value.
149
OMNeT++ Simulation Manual – Message Definitions
6.4 Imports
Import directives are used to make definitions in one message file available to another one.
Importing an MSG file makes the definitions in that file available to the file that imports it,
but has no further side effect (and in particular, it will generate no C++ code).
To import a message file, use the import keyword followed by a name that identifies the
message file within its project:
import inet.linklayer.common.MacAddress;
The imported name is interpreted as a relative file path (by replacing dots with slashes, and
appending .msg), which is searched for in folders listed in the message import path, much like
C/C++ include files are searched for in the compiler’s include path, Python modules in the
Python module search path, or NED files in the NED path.
The message import path can be specified to the message compiler via a series of -I command-
line options.
6.5 Namespaces
To place generated types into a namespace, add a namespace directive above the types in
question:
namespace inet;
Hierarchical (nested) namespaces are declared using double colons in the namespace defini-
tion, similar to nested namespace definitions introduced in C++ in version C++17.
namespace inet::ieee80211;
The above code will be translated into multiple nested namespaces in the C++ code:
There can be multiple namespace directives in a message file. The effect of the namespace
directive extends from the place of the directive until the next namespace directive or the end
of the message file. Each namespace directive opens a completely new namespace, i.e. not a
namespace within the previous one. An empty namespace directive (namespace;) returns to
the global namespace. For example:
namespace foo::bar;
class A {} // defines foo::bar::A
namespace baz;
class B {} // defines baz::B
namespace;
class C {} // defines ::C
150
OMNeT++ Simulation Manual – Message Definitions
6.6 Properties
Properties are metadata annotations of the syntax @name or @name(...) that may occur
on file, class (packet, struct, etc.) definitions, and field levels. There are many predefined
properties, and a large subset of them deals with the details of what C++ code to generate
for the item they occur with. For example, @getter(getFoo) on a field requests that the
generated getter function has the name getFoo.
Here is a syntax example. Note that class properties are placed in the fields list (fields and
properties may be mixed in an arbitrary order), and field properties are written after the field
name.
@foo;
class Foo {
@customize(true);
string value @getter(...) @setter(...) @hint("...");
}
Syntactically, the mandatory part of a property is the @ character followed by the property
name. They are then optionally followed by an index and a parameter list. The index is a
name in square brackets, and it is rarely used. The parameter list is enclosed in parentheses,
and in theory, it may contain a value list and key-value list pairs, but almost all properties
expect to find just a single value there.
For boolean properties, the value may be true or false; if the value is missing, true is
assumed. Thus, @customize is equivalent to @customize(true).
As a guard against mistyping property names, properties need to be declared before they can
be used. Properties are declared using the @property property, with the name of the new
property in the index, and the type and other attributes of the property in the parameter list.
Examples for property declarations, including the declaration of @property itself, can be seen
by listing the built-in definitions of the message compiler (opp_msgtool -h builtindefs).
The complete list of properties understood by the message compiler and other OMNeT++ tools
can be found in Appendix F.
• C/C++ primitive data types: bool, char, short, int, long, unsigned char, unsigned
short, unsigned int, unsigned long, float, double.
• string. Getters and setters use the const char* data type; nullptr is not allowed.
Setters store a copy of the string, not just the pointer.
• C99-style fixed-size integer types: int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t,
uint32_t, uint64_t.2
In addition, OMNeT++ class names such as simtime_t and cMessage are also made avail-
able without the need to import anything. These names are accepted both with and without
spelling out the omnetpp namespace name.
Numeric fields are initialized to zero, booleans to false, and string fields to the empty string.
2 These type names are accepted without the _t suffix as well, but you are responsible to ensure that the generated
code compiles, i.e., the shortened type names must be defined in a header file you include.
151
OMNeT++ Simulation Manual – Message Definitions
6.7 Fields
A scalar field is one that holds a single value. It is defined by specifying the data type and the
field name, for example:
int timeToLive;
For each field, the generated class will have a protected data member, and a public getter
and setter method. The names of the methods will begin with get and set, followed by the
field name with its first letter converted to uppercase. Thus, the above field will generate the
following methods in the C++ class:
int getTimeToLive() const;
void setTimeToLive(int timeToLive);
NOTE: All methods are generated to be virtual, but we omit the virtual keyword here
and in further examples.
The method names are derived from the field name, but they can be customized with the
@getter and @setter properties, as shown below:
int timeToLive @getter(getTTL) @setter(setTTL);
The choice of C++ type used for the data member and the getter/setter methods can be
overridden with the help of the @cppType property (and on a more fine-grained level, with
@datamemberType, @argType and @returnType), although this is rarely useful.
Initial values for fields can be specified after an equal sign, like so:
int version = HTTP_VERSION;
string method = "GET";
string resource = "/";
bool keepAlive = true;
int timeout = 5*60;
Any phrase that is a valid C++ expression can be used as an initializer value. (The message
compiler does not check the syntax of the values, it merely copies them into the generated
C++ file.)
For array fields, the initializer specifies the value for individual array elements. There is no
syntax for initializing an array with a list of values.
In a subclass, it is possible to override the initial value of an inherited field. The syntax is
similar to that of a field definition with an initial value, only the data type is missing.
An example:
152
OMNeT++ Simulation Manual – Message Definitions
packet Ieee80211Frame
{
int frameType;
...
};
It may seem like the message compiler would need the definition of the base class to check
the definition of the field being assigned. However, this is not the case. The message compiler
trusts that such a field exists; or rather, it leaves the check to the C++ compiler.
What the message compiler actually does is derive a setter method name from the field name
and generate a call to it into the constructor. Thus, the generated constructor for the above
packet type would be something like this:
Ieee80211DataFrame::Ieee80211DataFrame(const char *name, int kind) :
Ieee80211Frame(name, kind)
{
this->setFrameType(DATA_FRAME);
...
}
This implementation also lets one initialize cMessage /cPacket fields such as message kind
or packet length:
packet UDPPacket
{
byteLength = 16; // results in 'setByteLength(16);' being placed into ctor
};
A field can be marked as const by using the const keyword. A const field only has a (const)
data member and a getter function, but no setter. The value can be provided via an initializer.
An example:
const int foo = 24;
This generates a const int data member in the class, initialized to 24, and a getter member
function that returns its value:
int getFoo() const;
153
OMNeT++ Simulation Manual – Message Definitions
plied for the getter. The custom getter can then encapsulate the computation of the field value.
Customization is covered in section 6.10.
NOTE: To add actual constants (as opposed to getter-only fields) to a class, it is better to
use a targeted cplusplus block to inject their definitions into the C++ class declaration.
Abstract fields are a way to allow custom implementation (such as storage, getter/setter meth-
ods, etc.) to be provided for a field. For a field marked as abstract, the message compiler does
not generate a data member, and generated getter/setter methods will be pure virtual. It
is expected that the pure virtual methods will be implemented in a subclass (possibly via
@customize, see 6.10).
A field is declared abstract by using the abstract keyword or the @abstract property (the
two are equivalent).
abstract bool urgentBit; // or: bool urgentBit @abstract;
Alternatives to abstract, at least for certain use cases, are @custom and @customImpl (see
section 6.10).
Fixed-size arrays can be declared with the usual syntax of putting the array size in square
brackets after the field name:
int route[4];
The generated getter and setter methods will have an extra k argument (the array index), and
a third method that returns the array size is also generated:
int getRoute(size_t k) const;
void setRoute(size_t k, int route);
size_t getRouteArraySize() const;
When the getter or setter method is called with an index that is out of bounds, an exception
is thrown.
The method names can be overridden with the @getter, @setter, and @sizeGetter proper-
ties. To use another C++ type for array size and indices instead of the default size_t, specify
the @sizeType property.
NOTE: Use a singular noun for the field name instead of a plural noun (route[] in-
stead of routes[]), otherwise, method names will look confusing (getRoutes(), appen-
dRoutes(), etc., for methods that deal with a single route).
When a default value is given, it is interpreted as a scalar for filling the array with. There is
no syntax for initializing an array with a list of values.
154
OMNeT++ Simulation Manual – Message Definitions
If the array size is not known in advance, the field can be declared to have a variable size by
using an empty pair in brackets:
int route[];
In this case, the generated class will have extra methods in addition to the getter and setter:
one for resizing the array, one for getting the array size, plus methods for inserting an element
at a given position, appending an element, and erasing an element at a given position.
int getRoute(size_t k) const;
void setRoute(size_t k, int route);
void setRouteArraySize(size_t size);
size_t getRouteArraySize() const;
void insertRoute(size_t k, int route);
void appendRoute(int route);
void eraseRoute(size_t k);
The default array size is zero. Elements can be added by calling the inserter or the appender
method or resizing the array and setting individual elements.
Internally, all methods that change the array size (inserter, appender, resizer) always allocate
a new array and copy existing values over to the new array. Therefore, when adding a large
number of elements, it is recommended to resize the array first instead of calling the appender
method multiple times.
The method names can be overridden with the @getter, @setter, @sizeGetter, @sizeSet-
ter, @inserter, @appender, and @eraser field properties. To use another C++ type for array
size and indices instead of the default size_t, specify the @sizeType property.
When a default value is given, it is used for initializing new elements when the array is ex-
panded.
int route[] = -1;
Classes and structs may also be used as fields, not only primitive types and string. For
example, given a class named IPAddress, one can write the following field:
IPAddress sourceAddress;
155
OMNeT++ Simulation Manual – Message Definitions
Note that in addition to the getter and setter, a mutable getter (get...ForUpdate) is also
generated, which allows the stored value (object or struct) to be modified in place.
By default, values are passed by reference. This can be changed by specifying the @byValue
property:
IPAddress sourceAddress @byValue;
Note that both member functions use pass-by-value, and that the mutable getter function is
not generated.
Specifying const will cause only a getter function to be generated but no setter or mutable
getter, as shown before in 6.7.4.
Array fields are treated similarly, the difference being that the getter and setter methods take
an extra index argument:
IPAddress route[];
The field type may be a pointer, both for scalar and array fields. Pointer fields come in two
flavors: owning and non-owning. A non-owning pointer field just stores the pointer value
regardless of the ownership of the object it points to, while an owning pointer holds the
ownership of the object. This section discusses non-owning pointer fields.
Example:
cModule *contextModule; // missing @owner: non-owning pointer field
If the field is marked const, then the setter will take a const pointer, and the getForUp-
date() method is not generated:
const cModule *contextModule;
156
OMNeT++ Simulation Manual – Message Definitions
This section discusses pointer fields that own the objects they point to, that is, are respon-
sible for deallocating the object when the object containing the field (let’s refer to it as the
“container” object) is deleted.
For all owning pointer fields in a class, the destructor of the class deletes the owned objects,
the dup() method and the copy constructor duplicate the owned objects for the newly created
object, and the assignment operator (operator=) does both: the old objects in the destination
object are deleted, and replaced by clones of the objects in the source object.
When the owned object is a subclass of cOwnedObject that keeps track of its owner, the code
generated for the container class invokes the take() and drop() methods at the appropriate
times to manage the ownership.
Example:
cPacket *payload @owned;
The getter and mutable getter return the stored pointer (or nullptr if there is none).
The remover method releases the ownership of the stored object, sets the field to nullptr,
and returns the object.
The setter method behavior depends on the presence of the @allowReplace property. By
default (when @allowReplace is absent), the setter does not allow replacing the object. That
is, when the setter is invoked on a field that already contains an object (the pointer is non-
null), an error is raised: "A value is already set, remove it first with removePayload()". One
must call removePayload() before setting a new object.
When @allowReplace is specified for the field, there is no need to call the remover method
before setting a new value because the setter method deletes the old object before storing the
new one.
cPacket *payload @owned @allowReplace; // allow setter to delete the old object
If the field is marked const, then the getForUpdate() method is not generated, and the
setter takes a const pointer.
const cPacket *payload @owned;
The name of the remover method (which is the only extra method compared to non-pointer
fields) can be customized using the @remover property.
157
OMNeT++ Simulation Manual – Message Definitions
The target can be h (the generated header file – this is the default), cc (the generated .cc file),
the name of a type generated in the same message file (content is inserted in the declaration
of the type, just before the closing curly brace), or a member function name of one such type.
cplusplus blocks with the target h are commonly used to insert #include directives, com-
monly used constants or macros (e.g., #defines), or, rarely, typedefs and other elements into
the generated header. The fragments are pasted into the namespace which is open at that
point. Note that includes should always be placed into a cplusplus(h) block above the first
namespace declaration in the message file.
cplusplus blocks with the target cc allow you to insert code into the .cc file, for example, im-
plementations of member functions. This is useful, for instance, with custom-implementation
fields (@customImpl, see 6.10.4).
cplusplus blocks with a type name as the target allow you to insert new data members and
member functions into the class. This is useful, for example, with custom fields (@custom, see
6.10.5).
To inject code into the implementation of a member function of a generated class, specify
<classname>::<methodname> as the target. Supported methods include the constructor,
copy constructor (use Foo& as the name), destructor, operator=, copy(), parsimPack(),
parsimUnpack(), etc., and the per-field generated methods (setter, getter, etc.).
158
OMNeT++ Simulation Manual – Message Definitions
For the first step, you can use the @existingClass property. When a type (class or struct)
is annotated with @existingClass, the message compiler remembers the definition but as-
sumes that the class (or struct) already exists in the C++ code and does not generate it.
(However, it will still generate a class descriptor, see section 6.11.)
NOTE: Support for C++-style type announcements is no longer part of the message
definitions syntax; they were removed in OMNeT++ version 6.0.
The second step is achieved by adding a cplusplus block with an #include directive to the
message file.
For example, suppose we have a hand-written ieee802::MACAddress class defined in MACAd-
dress.h that we would like to use for fields in multiple message files. One way to make this
possible is to add a MACAddress.msg file alongside the header with the following content:
// MACAddress.msg
cplusplus {{
#include "MACAddress.h"
}}
As exemplified above, for existing classes, it is possible to announce them with their namespace-
qualified name; there is no need for a separate namespace line.
This message file can be imported into all other message files that need the MACAddress, for
example, like this:
import MACAddress;
packet EthernetFrame {
ieee802::MACAddress source;
ieee802::MACAddress destination;
...
}
• Custom fields
159
OMNeT++ Simulation Manual – Message Definitions
• Abstract fields
The names and some other properties of generated methods can be influenced with metadata
annotations (properties).
The following field properties exist for overriding method names: @getter, @setter, @getter-
ForUpdate, @remover, @sizeGetter, @sizeSetter, @inserter, @appender and @eraser.
To override data types used by the data member and its accessor methods, use @cppType,
@datamemberType, @argType, or @returnType.
To override the default size_t type used for array size and indices, use @sizeType.
Consider the following example:
packet IPPacket {
int ttl @getter(getTTL) @setter(setTTL);
Option options[] @sizeGetter(getNumOptions)
@sizeSetter(setNumOptions)
@sizetype(short);
}
The generated class would have the following methods (note the differences from the de-
fault 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 prop-
erty. 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.
Generally, literal C++ blocks (the cplusplus keyword) are the way to inject code into the body
of individual methods, as described in 6.8.
160
OMNeT++ Simulation Manual – Message Definitions
The @beforeChange class property can be used to designate a member function that is to be
called before any mutator code (in setters, non-const getters, assignment operator, etc.) exe-
cutes. This can be used to implement, for example, a dirty flag or some form of immutability
(i.e. freeze the state of the object).
The @str class property aims to simplify adding an str() method in the generated class.
Having an str() method is often useful for debugging, and it also has a special role in class
descriptors (see 6.11.6).
When @str is present, an std::string str() const method is generated for the class. The
method’s implementation will contain a single return keyword, with the value of the @str
property copied after it.
Example:
class Location {
double lat;
double lon;
@str("(" + std::to_string(getLat()) + "," + std::to_string(getLon()) + ")");
}
It will result in the following str() method to be generated as part of the Location class:
std::string Location::str() const
{
return "(" + std::to_string(getLat()) + "," + std::to_string(getLon()) + ")";
}
When member functions generated for a field need customized implementation and method-
targeted C++ blocks are not sufficient, the customImpl property can be of help. When a field
is marked customImpl, the message compiler will skip generating the implementations of its
accessor methods in the .cc file, allowing the user to supply their own versions.
Here is a simple example. The methods in it do not perform anything extra compared to the
default generated versions, but they illustrate the principle.
class Packet
{
int hopCount @customImpl;
}
cplusplus(cc) {{
int Packet::getHopCount() const
{
return hopCount; // replace/extend with extra code
}
161
OMNeT++ Simulation Manual – Message Definitions
If a field is marked with @custom, the field will only appear in the class descriptor, but no
code is generated for it at all. One can inject the code that implements the field (data member,
getter, setter, etc.) via targeted cplusplus blocks (6.8). @custom is a good way to go when
you want the field to have a different underlying storage or different accessor methods than
normally generated by the message compiler. (For the latter case, however, be aware that the
generated class descriptor assumes the presence of certain accessor methods for the field,
although the set of expected methods can be customized to a degree. See 6.11 for details.)
The following example uses @custom to implement a field that acts as a stack (has push()
and pop() methods), and uses std::vector as the underlying data structure.
cplusplus {{
#include <vector>
}}
class MPLSHeader
{
int32_t label[] @custom @sizeGetter(getNumLabels) @sizeSetter(setNumLabels);
}
cplusplus(MPLSHeader) {{
protected:
std::vector<int32_t> labels;
public:
// expected methods:
virtual void setNumLabels(size_t size) {labels.resize(size);}
virtual size_t getNumLabels() const {return labels.size();}
virtual int32_t getLabel(size_t k) const {return labels.at(k);}
virtual void setLabel(size_t k, int32_t label) {labels.at(k) = label;}
// new methods:
virtual void pushLabel(int32_t label) {labels.push_back(label);}
virtual int32_t popLabel() {auto l=labels.back();labels.pop_back();return l;}
}}
cplusplus(MPLSHeader::copy) {{
labels = other.labels;
}}
The last C++ block is needed so that the copy constructor and the operator= method also
copy the new field. (copy() is a member function where the common part of the above two
are factored out, and the C++ block injects code in there.)
162
OMNeT++ Simulation Manual – Message Definitions
Another way of customizing the generated code is by employing what is known as the Gener-
ation Gap design pattern, proposed by John Vlissides. The idea is that the customization can
be done while subclassing the generated class, overriding whichever member functions need
to be different from their generated versions.
This feature is enabled by adding the @customize property to the class. Doing so will cause
the message compiler to generate an intermediate class instead of the final one, and the user
will subclass the intermediate class to obtain the real class. The name of the intermediate
class is obtained by appending _Base to the class name. The subclassing code can be in an
entirely different header and .cc file from the generated one, so this method does not require
the use of cplusplus blocks.
Consider the following example:
packet FooPacket
{
@customize(true);
...
};
The message compiler will generate a FooPacket_Base class instead of FooPacket. It is then
the user’s task to subclass FooPacket_Base to derive FooPacket, while adding extra data
members and adding/overriding methods to achieve the goals that motivated the customiza-
tion.
There is a minimum amount of code you have to write for FooPacket, because not everything
can be pre-generated as part of FooPacket_Base (e.g. constructors cannot be inherited). This
minimum code, which usually goes into a header file, is the following:
class FooPacket : public FooPacket_Base
{
private:
void copy(const FooPacket& other) { ... }
public:
FooPacket(const char *s=nullptr, short kind=0) : FooPacket_Base(s,kind) {}
FooPacket(const FooPacket& other) : FooPacket_Base(other) {copy(other);}
FooPacket& operator=(const FooPacket& other) {if (this==&other) return *this;
FooPacket_Base::operator=(other); copy(other); return *this;}
virtual FooPacket *dup() const override {return new FooPacket(*this);}
};
NOTE: The above boilerplate code can be copied out of the generated C++ header, which
contains it as a comment.
The generated constructor, copy constructor, operator=, dup() can usually be copied verba-
tim. The only method that needs custom code is copy(). It is shared by the copy constructor
and operator=, and should take care of copying the new data members you added as part of
FooPacket.
In addition to the above, the implementation (.cc) file should contain the registration of the
new class:
Register_Class(FooPacket);
163
OMNeT++ Simulation Manual – Message Definitions
Abstract fields, introduced in 6.7.5, are an alternative to @custom (see 6.10.5) for allowing
a custom implementation (such as storage, getter/setter methods, etc.) to be provided for a
field. For a field marked abstract, the message compiler does not generate a data member,
and generated getter/setter methods will be pure virtual.
Abstract fields are most often used together with the Generation Gap pattern (see 6.10.6), so
that one can immediately supply a custom implementation.
The following example demonstrates the use of abstract fields for creating an array field that
uses std::vector as the underlying implementation:
packet FooPacket
{
@customize(true);
abstract int foo[]; // impl will use std::vector<int>
}
If you compile the above code, in the generated C++ code you will only find abstract methods
for foo, but no underlying data member or method implementation. You can implement
everything as you like. You can then write the following C++ file to implement foo with
std::vector (some details omitted for brevity):
#include <vector>
#include "FooPacket_m.h"
public:
// constructor and other methods omitted, see below
...
virtual int getFoo(size_t k) {return foo[k];}
virtual void setFoo(size_t k, int x) {foo[k]=x;}
virtual void addFoo(int x) {foo.push_back(x);}
virtual void setFooArraySize(size_t size) {foo.resize(size);}
virtual size_t getFooArraySize() const {return foo.size();}
};
Register_Class(FooPacket);
Some additional boilerplate code is needed so that the class conforms to conventions, and
duplication and copying work properly:
FooPacket(const char *name=nullptr, int kind=0) : FooPacket_Base(name,kind) {
}
FooPacket(const FooPacket& other) : FooPacket_Base(other.getName()) {
operator=(other);
}
FooPacket& operator=(const FooPacket& other) {
if (&other==this) return *this;
FooPacket_Base::operator=(other);
164
OMNeT++ Simulation Manual – Message Definitions
foo = other.foo;
return *this;
}
virtual FooPacket *dup() {
return new FooPacket(*this);
}
For each generated class and struct, the message compiler also generates an associated de-
scriptor class. This class carries “reflection” information about the new class. The descriptor
class encapsulates virtually all the information that the original message definition contains,
and exposes it via member functions. Reflection information allows inspecting object con-
tents down to the field level in Qtenv, filtering objects by a filter expression that refers to
object fields, serializing messages-packets in a readable form for the eventlog file, and has
several further potential uses.
6.11.1 cClassDescriptor
The descriptor class is subclassed from cClassDescriptor. It has methods for enumerating
fields (getFieldCount(), getFieldName(), getFieldTypeString(), etc.), for getting and
setting a field’s value in string form (getFieldAsString(), setFieldAsString()) and as
cValue (getFieldValue(), setFieldValue()), for exploring the class hierarchy (getBase-
ClassDescriptor(), etc.), for accessing class and field properties, and for similar tasks.
Classes derived from cObject have a virtual member function getDescriptor() that returns
their associated descriptor. For other classes, it is possible to obtain the descriptor using
cClassDescriptor::getDescriptorFor() with the class name as the argument.
Several properties control the creation and details of the class descriptor.
The @descriptor class property can be used to control the generation of the descriptor class.
@descriptor(readonly) instructs the message compiler not to generate field setters for the
descriptor, and @descriptor(false) instructs it not to generate a descriptor class for the
class at all.
It is also possible to use (or abuse) the message compiler for generating a descriptor class for
an existing class. To do that, write a message definition for your existing class (for example,
if it has int getFoo() and setFoo(int) methods, add an int foo field to the message
definition), and mark it with @existingClass. This will tell the message compiler that it
should not generate an actual class (as it already exists), only a descriptor class.
165
OMNeT++ Simulation Manual – Message Definitions
When an object is shown in Qtenv’s Object Inspector pane, Qtenv obtains all the information
it displays from the object’s descriptor. There are several properties that can be used to
customize how a field appears in the Object Inspector:
Several of the properties which are for overriding field accessor method names (@getter,
@setter, @sizeGetter, @sizeSetter, etc., see 6.10.1) have a secondary purpose. When
generating a descriptor for an existing class (see @existingClass), those properties specify
how the descriptor can access the field, i.e. what code to generate in the implementation of the
descriptor’s various methods. In that use case, such properties may contain code fragments
or a function call template instead of a method name.
6.11.6 toString/fromString
• @toString specifies the code to convert the return type of the setter to a string;
• @fromString specifies the code to convert a string to the setter’s argument type.
These properties can be specified on the class (where it will be applied to fields of that type),
or directly on fields. Multiple syntaxes are accepted:
Example:
class IPAddress
{
@existingClass;
@opaque;
@toString(.str()); // use IPAddress::str() to produce a string
@fromString(IPAddress($)); // use constructor; '$' will be replaced by the str
}
166
OMNeT++ Simulation Manual – Message Definitions
If the @toString property is missing, the message compiler generates code that calls the
str() member function on the value returned by the getter, provided that it knows for certain
that the corresponding type has such a method (the type is derived from cObject, or has the
@str property).
If there is no @toString property and no (known) str() method, the descriptor will return
the empty string.
6.11.7 toValue/fromValue
There are several boolean-valued properties that enable/disable various features in the de-
scriptor:
• @opaque: If true, it treats the field as an atomic (non-compound) type, i.e., having no
descriptor class. When specified on a class, it determines the default for fields of that
type.
• @editable: If set, the value of the field (or value of fields that are instances of this
type) can be set via the class descriptor’s setFieldValueFromString() and setField-
Value() methods.
• @replaceable: If set, the field is a pointer whose value can be set via the class descrip-
tor’s setFieldStructValuePointer() and setFieldValue() methods.
• @resizable: If set, the field is a variable-size array whose size can be set via the class
descriptor’s setFieldArraySize() method.
• @readonly: This is simply a shorthand for @editable(false) @replaceable(false)
@resizable(false).
167
OMNeT++ Simulation Manual – Message Definitions
168
OMNeT++ Simulation Manual – The Simulation Library
Chapter 7
OMNeT++ has an extensive C++ class library available to the user for implementing simulation
models and model components. Part of the class library’s functionality has already been cov-
ered in the previous chapters, including discrete event simulation basics, the simple module
programming model, module parameters and gates, scheduling events, sending and receiving
messages, channel operation and programming model, finite state machines, dynamic module
creation, signals, and more.
This chapter discusses the rest of the simulation library. Topics will include logging, random
number generation, queues, topology discovery and routing support, and statistics and result
collection. This chapter also covers some of the conventions and internal mechanisms of the
simulation library to allow one extending it and using it to its full potential.
7.1 Fundamentals
Classes in the OMNeT++ simulation library are part of the omnetpp namespace. To use the
OMNeT++ API, one must include the omnetpp.h header file and either import the namespace
with using namespace omnetpp, or qualify names with the omnetpp:: prefix.
Thus, simulation models will contain the
#include <omnetpp.h>
When writing code that should work with various versions of OMNeT++, it is often useful to
have compile-time access to the OMNeT++ version in a numeric form. The OMNETPP_VERSION
macro exists for that purpose, and it is defined by OMNeT++ to hold the version number in
the form major*256+minor. For example, in OMNeT++ 4.6 it was defined as
169
OMNeT++ Simulation Manual – The Simulation Library
Most classes in the simulation library are derived from cObject, or its subclasses cNamedOb-
ject and cOwnedObject. cObject defines several virtual member functions that are either
inherited or redefined by subclasses. Otherwise, cObject is a zero-overhead class as far as
memory consumption goes: it purely defines an interface but has no data members. Thus,
having cObject as a base class does not add anything to the size of a class if it already has
at least one virtual member function.
cObject
cNamedObject
cOwnedObject
... ...
Figure 7.1: cObject is the base class for most of the simulation library
The subclasses cNamedObject and cOwnedObject add data members to implement more
functionality. The following sections discuss some of the practically important functionality
defined by cObject.
The most useful and most visible member functions of cObject are getName() and getFull-
Name(). The idea behind them is that many objects in OMNeT++ have names by default (for
example, modules, parameters and gates), and even for other objects, having a printable name
is a huge gain when it comes to logging and debugging.
getFullName() is important for gates and modules, which may be part of gate or module
vectors. For them, getFullName() returns the name with the index in brackets, while get-
Name() only returns the name of the module or gate vector. That is, for a gate out[3] in the
gate vector out[10], getName() returns "out", and getFullName() returns "out[3]". For
other objects, getFullName() simply returns the same string as getName(). An example:
170
OMNeT++ Simulation Manual – The Simulation Library
NOTE: When printing out the name of an object, prefer getFullName() to getName(),
especially if the runtime type is not known. This will ensure that the vector index will
also be printed if the object has one.
cObject merely defines these member functions, but they return an empty string. Actual
storage for a name string and a setName() method is provided by the class cNamedObject,
which is also an (indirect) base class for most library classes. Thus, one can assign names to
nearly all user-created objects. It is also recommended to do so, because a name makes an
object easier to identify in graphical runtimes like Qtenv.
By convention, the object name is the first argument to the constructor of every class, and it
defaults to the empty string. To create an object with a name, pass the name string (a const
char* pointer) as the first argument of the constructor. For example:
cMessage *timeoutMsg = new cMessage("timeout");
Both the constructor and setName() make an internal copy of the string, instead of just
storing the pointer passed to them.1
For convenience and efficiency reasons, the empty string "" and nullptr are treated as
interchangeable by library objects. That is, "" is stored as nullptr but returned as "". If
one creates a message object with either nullptr or "" as its name string, it will be stored as
nullptr, and getName() will return a pointer to a static "".
Hierarchical Name
getFullPath() returns the object’s hierarchical name. This name is produced by prepending
the full name (getFullName()) with the parent or owner object’s getFullPath(), separated
by a dot. For example, if the out[3] gate in the previous example belongs to a module
named classifier, which in turn is part of a network called Queueing, then the gate’s
getFullPath() method will return "Queueing.classifier.out[3]".
cGate *gate = gate("out", 3); // out[3]
EV << gate->getName(); // prints "out"
EV << gate->getFullName(); // prints "out[3]"
EV << gate->getFullPath(); // prints "Queueing.classifier.out[3]"
The getFullName() and getFullPath() methods are extensively used in graphical runtime
environments like Qtenv, and also when assembling runtime error messages.
In contrast to getName() and getFullName() which return const char * pointers, get-
FullPath() returns std::string. This makes no difference when logging via EV«, but when
getFullPath() is used as a "%s" argument to sprintf(), one needs to write getFull-
Path().c_str().
char buf[100];
sprintf("msg is '%80s'", msg->getFullPath().c_str()); // note c_str()
1 In a simulation, there are usually many objects with the same name: modules, parameters, gates, etc. To conserve
memory, several classes keep names in a shared, reference-counted name pool instead of making separate copies for
each object. The runtime cost of looking up an existing string in the name pool and incrementing its reference count
also compares favorably to the cost of allocation and copying.
171
OMNeT++ Simulation Manual – The Simulation Library
Class Name
The getClassName() member function returns the class name as a string, including the
namespace. getClassName() internally relies on C++ RTTI.
An example:
const char *className = msg->getClassName(); // returns "omnetpp::cMessage"
Cloning Objects
The dup() member function creates an exact copy of the object, duplicating contained objects
also if necessary. This is especially useful in the case of message objects.
cMessage *copy = msg->dup();
dup() delegates to the copy constructor. Classes also declare an assignment operator (oper-
ator=()) which can be used to copy the contents of an object into another object of the same
type. dup(), the copy constructor and the assignment operator all perform deep copying:
objects contained in the copied object will also be duplicated if necessary.
operator=() differs from the other two in that it does not copy the object’s name string, i.e.
does not invoke setName(). The rationale is that the name string is often used for identifying
the particular object instance, as opposed to being considered part of its contents.
7.1.3 Iterators
There are several container classes in the library (cQueue, cArray, etc.) For many of them,
there is a corresponding iterator class that one can use to loop through the objects stored in
the container.
For example:
cQueue queue;
//...
for (cQueue::Iterator it(queue); !it.end(); ++it) {
cObject *containedObject = *it;
//...
}
When library objects detect an error condition, they throw a C++ exception. This exception
is then caught by the simulation environment, which pops up an error dialog or displays the
error message.
At times it can be useful to be able to stop the simulation at the place of the error (just before
the exception is thrown) and use a C++ debugger to look at the stack trace and examine
variables. Enabling the debug-on-errors or the debugger-attach-on-error configuration
option lets you do that – check it in section 11.12.
172
OMNeT++ Simulation Manual – The Simulation Library
The exact way log messages are displayed to the user depends on the user interface. In the
command-line user interface (Cmdenv), the log is simply written to the standard output. In
the Qtenv graphical user interface, the main window has an area for displaying the log output
from the currently displayed compound module.
All logging must be categorized into one of the predefined log levels. The assigned log level
determines how important and how detailed a log statement is. When deciding which log
level is appropriate for a particular log statement, keep in mind that they are meant to be
local to components. There’s no need for a global agreement among all components, because
OMNeT++ provides per component filtering. Log levels are mainly useful because log output
can be filtered based on them.
173
OMNeT++ Simulation Manual – The Simulation Library
• LOGLEVEL_DETAIL should be used for low-level protocol-specific details that may be use-
ful and understandable to the users of the component. These messages may help to
track down various protocol-specific issues without actually looking too deeply into the
code. For example, a MAC layer protocol component could log state machine updates,
acknowledge timeouts, and selected back-off periods using this level.
• LOGLEVEL_TRACE is the lowest log level. It should be used for low-level implementation-
specific technical details that are mostly useful for the developers of the component.
For example, a MAC layer protocol component could log control flow in loops and if
statements, and entering/leaving methods and code blocks using this level.
OMNeT++ provides several C++ macros for the actual logging. Each one of these macros acts
like a C++ stream, so they can be used similarly to std::cout with operator« (shift operator).
The actual logging is as simple as writing information into one of these special log streams as
follows:
EV_ERROR << "Connection to server is lost.\n";
EV_WARN << "Queue is full, discarding packet.\n";
EV_INFO << "Packet received, sequence number = " << seqNum << "." << endl;
EV_TRACE << "routeUnicastPacket(" << packet << ");" << endl;
NOTE: It is not recommended to use plain printf() or std::cout for logging. Output
from EV_INFO and the other log macros can be controlled more easily from omnetpp.ini,
and it is more convenient to view using Qtenv.
The above C++ macros work well from any C++ class, including OMNeT++ modules. In fact,
they automatically capture several context-specific information such as the current event,
current simulation time, context module, this pointer, source file, and line number. The
174
OMNeT++ Simulation Manual – The Simulation Library
final log lines will be automatically extended with a prefix that is created from the captured
information (see section 10.6).
In static class member functions or in non-class member functions, an extra EV_STATICCONTEXT
macro must be present to make sure that normal log macros compile. 2
void findModule(const char *name, cModule *from)
{
EV_STATICCONTEXT;
EV_TRACE << "findModule(" << name << ", " << from << ");" << endl;
Sometimes it might be useful to further classify log statements into user-defined log cate-
gories. In the OMNeT++ logging API, a log category is an arbitrary string provided by the
user.
For example, a module test may check for a specific log message in the test’s output. Putting
the log statement into the test category ensures that extra care is taken when someone
changes the wording in the statement to match the one in the test.
Similarly to the normal C++ log macros, there are separate log macros for each log level which
also allow specifying the log category. Their name is the same as the normal variants’ but
simply extended with the _C suffix. They take the log category as the first parameter before
any shift operator calls:
EV_INFO_C("test") << "Received " << numPacket << " packets in total.\n";
Occasionally it’s easier to produce a log line using multiple statements. Mostly because some
computation has to be done between the parts. This can be achieved by omitting the new
line from the log statements which are to be continued. And then subsequent log statements
must use the same log level, otherwise, an implicit new line would be inserted.
EV_INFO << "Line starts here, ";
... // some other code without logging
EV_INFO << "and it continues here" << endl;
Assuming a simple log prefix that prints the log level in brackets, the above code fragment
produces the following output in Cmdenv:
[INFO] Line starts here, and it continues here
Sometimes it might be useful to split a line into multiple lines to achieve better formatting. In
such cases, there’s no need to write multiple log statements. Simply insert new lines into the
sequence of shift operator calls:
EV_INFO << "First line" << endl << "second line" << endl;
In the produced output, each line will have the same log prefix, as shown below:
2 This is due to the fact that in C++ it is impossible to determine at compile-time whether a this pointer is
accessible.
175
OMNeT++ Simulation Manual – The Simulation Library
The OMNeT++ logging API also supports direct printing to a log stream. This is mainly useful
when printing is really complicated algorithmically (e.g., printing a multi-dimensional value).
The following code could produce multiple log lines each having the same log prefix.
void Matrix::print(std::stream &output) { ... }
void Matrix::someFunction()
{
print(EV_INFO);
7.2.6 Implementation
OMNeT++ does its best to optimize the performance of logging. The implementation fully
supports conditional compilation of log statements based on their log level. It automatically
checks whether the log is recorded anywhere. It also checks global and per-component run-
time log levels. The latter is efficiently cached in the components for subsequent checks. See
section 10.6 for more details on how to configure these log levels.
The implementation of the C++ log macros makes use of the fact that the operator« is bound
more loosely than the conditional operator (?:). This solves conditional compilation, and
also helps runtime checks by redirecting the output to a null stream. Unfortunately, the
operator« calls are still evaluated on the null stream, even if the log level is disabled.
Rarely, just the computation of log statement parameters may be very expensive, and thus
it must be avoided if possible. In this case, it is a good idea to make the log statement
conditional on whether the output is actually being displayed or recorded anywhere. The
cEnvir::isLoggingEnabled() call returns false when the output is disabled, such as in
“express” mode. Thus, one can write code like this:
if (!getEnvir()->isLoggingEnabled())
EV_DEBUG << "CRC: " << computeExpensiveCRC(packet) << endl;
176
OMNeT++ Simulation Manual – The Simulation Library
It is often advantageous for simulations to use random numbers from multiple RNG instances.
For example, a wireless network simulation may use one RNG for generating traffic and an-
other RNG for simulating transmission errors in the noisy wireless channel. Since seeds for
individual RNGs can be configured independently, this arrangement allows one to perform
several simulation runs with the same traffic but with bit errors occurring in different places.
A simulation technique called variance reduction is also related to the use of different random
number streams. OMNeT++ makes it easy to use multiple RNGs in various flexible configura-
tions.
When assigning seeds, it is important that different RNGs and also different simulation runs
use non-overlapping series of random numbers. Overlap in the generated random number
sequences can introduce unwanted correlation in the simulation results.
Mersenne Twister
By default, OMNeT++ uses the Mersenne Twister RNG (MT) by M. Matsumoto and T. Nishimura
[MN98]. MT has a period of 219937 − 1, and a 623-dimensional equidistribution property is as-
sured. MT is also very fast: as fast or faster than ANSI C’s rand().
OMNeT++ releases prior to 3.0 used a linear congruential generator (LCG) with a cycle length
of 231 −2, described in [Jai91], pp. 441-444,455. This RNG is still available and can be selected
from omnetpp.ini (Chapter 11). This RNG is only suitable for small-scale simulation studies.
As shown by Karl Entacher et al. in [EHW02], the cycle length of about 231 is too small (on
today’s fast computers it is easy to exhaust all random numbers), and the structure of the
generated “random” points is too regular. The [Hel98] paper provides a broader overview of
issues associated with RNGs used for simulation, and it is well worth reading. It also contains
useful links and references on the topic.
When a simulation is executed under Akaroa control (see section 11.20), it is also possible to
let OMNeT++ use Akaroa’s RNG. This needs to be configured in omnetpp.ini (section 10.5).
Other RNGs
OMNeT++ allows the plugging in of your own RNGs as well. This mechanism, based on the
cRNG interface, is described in section 17.5. For example, one candidate to include could be
L’Ecuyer’s CMRG [LSCK02] which has a period of about 2191 and can provide a large number
of guaranteed independent streams.
177
OMNeT++ Simulation Manual – The Simulation Library
OMNeT++ can be configured to make several RNGs available for the simulation model. These
global or physical RNGs are numbered from 0 to numRN Gs − 1, and can be seeded indepen-
dently.
However, usually model code doesn’t directly work with those RNGs. Instead, there is an
indirection step introduced for additional flexibility. When random numbers are drawn in
a model, the code usually refers to component-local or logical RNG numbers. These local
RNG numbers are mapped to global RNG indices to arrive at actual RNG instances. This
mapping occurs on a per-component basis. That is, each module and channel object contains
a mapping table similar to the following:
In the example, the module or channel in question has 6 local (logical) RNGs that map to 4
global (physical) RNGs.
NOTE: Local RNG number 0 is special in the sense that all random number functions
use that RNG, unless explicitly told otherwise by specifying an rng=k argument.
The local-to-global mapping, as well as the number of global RNGs and their seeding can be
configured in omnetpp.ini (see section 10.5).
The mapping can be set up arbitrarily, with the default being an identity mapping (that is,
local RNG k refers to global RNG k.) The mapping allows for flexibility in RNG and random
number streams configuration – even for simulation models that were not written with RNG
awareness. For example, even if modules in a simulation only use the default, local RNG
number 0, one can set up a mapping so that different groups of modules use different physical
RNGs.
In theory, RNGs could also be instantiated and used directly from C++ model code. However,
doing so is not recommended because the model would lose configurability via omnetpp.ini.
RNGs are represented via subclasses of the abstract class cRNG. In addition to random num-
ber generation methods like intRand() and doubleRand(), the cRNG interface also includes
methods like selfTest() for basic integrity checking and getNumbersDrawn() to query the
number of random numbers generated.
RNGs can be accessed by local RNG number via cComponent’s getRNG(k) method. To access
global RNGs directly by their indices, one can use cEnvir’s getRNG(k) method. However,
RNGs rarely need to be accessed directly. Most simulations will only use them via random
variate generation functions, described in the next section.
178
OMNeT++ Simulation Manual – The Simulation Library
Distribution Description
Continuous distributions
uniform(a, b) uniform distribution in the range [a,b)
exponential(mean) exponential distribution with the given mean
normal(mean, stddev) normal distribution with the given mean and stan-
dard deviation
truncnormal(mean, stddev) normal distribution truncated to nonnegative values
gamma_d(alpha, beta) gamma distribution with parameters alpha>0,
beta>0
beta(alpha1, alpha2) beta distribution with parameters alpha1>0, al-
pha2>0
erlang_k(k, mean) Erlang distribution with k>0 phases and the given
mean
chi_square(k) chi-square distribution with k>0 degrees of freedom
student_t(i) student-t distribution with i>0 degrees of freedom
cauchy(a, b) Cauchy distribution with parameters a,b where b>0
triang(a, b, c) triangular distribution with parameters a<=b<=c,
a!=c
lognormal(m, s) lognormal distribution with mean m and variance
s>0
weibull(a, b) Weibull distribution with parameters a>0, b>0
pareto_shifted(a, b, c) generalized Pareto distribution with parameters a, b
and shift c
Discrete distributions
intuniform(a, b) uniform integer from a..b
bernoulli(p) result of a Bernoulli trial with probability 0<=p<=1 (1
with probability p and 0 with probability (1-p))
binomial(n, p) binomial distribution with parameters n>=0 and
0<=p<=1
geometric(p) geometric distribution with parameter 0<=p<=1
negbinomial(n, p) negative binomial distribution with parameters n>0
and 0<=p<=1
poisson(lambda) Poisson distribution with parameter lambda
Some notes:
• intuniform() generates integers including both the lower and upper limit, so for example
the outcome of tossing a coin could be written as intuniform(1,2).
• truncnormal() is the normal distribution truncated to nonnegative values; its implemen-
tation generates a number with normal distribution and if the result is negative, it keeps
generating other numbers until the outcome is nonnegative.
There are several ways to generate random numbers from these distributions, as described in
the next sections.
179
OMNeT++ Simulation Manual – The Simulation Library
The preferred way is to use methods defined on cComponent, the common base class of mod-
ules and channels:
double uniform(double a, double b, int rng=0) const;
double exponential(double mean, int rng=0) const;
double normal(double mean, double stddev, int rng=0) const;
...
These methods work with the component’s local RNGs, and accept the RNG index (default 0)
in their extra int parameter.
Since most simulation code is located in methods of simple modules, these methods can
usually be called in a concise way, without an explicit module or channel pointer. An example:
scheduleAt(simTime() + exponential(1.0), msg);
There are two additional methods, intrand() and dblrand(). intrand(n) generates random
integers in the range [0, n − 1], and dblrand() generates a random double on [0, 1). They also
accept an additional local RNG index that defaults to 0.
It is sometimes useful to be able to pass around random variate generators as objects. The
classes cUniform, cExponential, cNormal, etc. fulfill this need.
These classes subclass from the cRandom abstract class. cRandom was designed to encap-
sulate random number streams. Its most important method is draw() that returns a new
random number from the stream. cUniform, cExponential and other classes essentially
bind the distribution’s parameters and an RNG to the generation function.
cRandom
Let us see for example cNormal. The constructor expects an RNG (cRNG pointer) and the
parameters of the distribution, mean and standard deviation. It also has a default constructor,
as it is a requirement for Register_Class(). When the default constructor is used, the
parameters can be set with setRNG(), setMean() and setStddev(). setRNG() is defined on
cRandom. The draw() method, of course, is redefined to return a random number from the
normal distribution.
An example that shows the use of a random number stream as an object:
cNormal *normal = new cNormal(getRNG(0), 0, 1); // unit normal distr.
printRandomNumbers(normal, 10);
...
180
OMNeT++ Simulation Manual – The Simulation Library
Another important property of cRandom is that it can encapsulate state. That is, subclasses
can be implemented that, for example, return autocorrelated numbers, numbers from a
stochastic process, or simply elements of a stored sequence (e.g. one loaded from a trace
file).
Both the cComponent methods and the random number stream classes described above have
been implemented with the help of standalone generator functions. These functions take a
cRNG pointer as their first argument.
double uniform(cRNG *rng, double a, double b);
double exponential(cRNG *rng, double mean);
double normal(cRNG *rng, double mean, double stddev);
...
One can also specify a distribution as a histogram. The cHistogram, cKSplit and cPSquare
classes can be used to generate random numbers from histograms. This feature is docu-
mented later, with the statistical classes.
One can easily add support for new distributions. We recommend that you write a standalone
generator function first. Then you can add a cRandom subclass that wraps it, and/or module
(channel) methods that invoke it with the module’s local RNG. If the function is registered with
the Define_NED_Function() macro (see 7.12), it will be possible to use the new distribution
in NED files and ini files, as well.
If you need a random number stream that has state, you need to subclass from cRandom.
Basic Usage
cQueue is a container class that acts as a queue. cQueue can hold objects of types derived
from cObject (almost all classes from the OMNeT++ library), such as cMessage, cPar, etc.
Normally, new elements are inserted at the back and removed from the front.
181
OMNeT++ Simulation Manual – The Simulation Library
fro nt T
N
O
R
F
ba ck
rem ov al insertion
pop() insert()
The member functions dealing with insertion and removal are insert() and pop().
cQueue queue("my-queue");
cMessage *msg;
// insert messages
for (int i = 0; i < 10; i++) {
msg = new cMessage;
queue.insert(msg);
}
// remove messages
while(!queue.isEmpty()) {
msg = (cMessage *)queue.pop();
delete msg;
}
The length() member function returns the number of items in the queue, and empty() tells
whether there is anything in the queue.
There are other functions dealing with insertion and removal. The insertBefore() and
insertAfter() functions insert a new item exactly before or after a specified one, regardless
of the ordering function.
The front() and back() functions return pointers to the objects at the front and back of the
queue, without affecting queue contents.
The pop() function can be used to remove items from the tail of the queue, and the remove()
function can be used to remove any item known by its pointer from the queue:
queue.remove(msg);
Priority Queue
By default, cQueue implements a FIFO, but it can also act as a priority queue; that is, it
can keep the inserted objects ordered. To use this feature, one needs to provide a comparison
function that takes two cObject pointers and returns -1, 0, or 1 (see the reference for details).
An example of setting up an ordered cQueue:
cQueue queue("queue", someCompareFunc);
182
OMNeT++ Simulation Manual – The Simulation Library
If the queue object is set up as an ordered queue, the insert() function uses the ordering
function: it searches the queue contents from the head until it reaches the position where the
new item needs to be inserted and inserts it there.
Iterators
The cQueue::Iterator class lets one iterate over the contents of the queue and examine
each object.
The cQueue::Iterator constructor expects the queue object in the first argument. Normally,
forward iteration is assumed, and the iteration is initialized to point at the front of the queue.
For reverse iteration, specify reverse=true as the optional second argument. After that, the
class acts as any other OMNeT++ iterator: one can use the ++ and - operators to advance it,
the * operator to get a pointer to the current item, and the end() member function to check
whether the iterator has reached the end (or the beginning) of the queue.
Forward iteration:
for (cQueue::Iterator iter(queue); !iter.end(); iter++) {
cMessage *msg = (cMessage *) *iter;
//...
}
Reverse iteration:
for (cQueue::Iterator iter(queue, true); !iter.end(); iter--) {
cMessage *msg = (cMessage *) *iter;
//...
}
Basic Usage
cArray is a container class that holds objects derived from cObject. cArray implements
a dynamic-size array: its capacity grows automatically when it becomes full. cArray stores
pointers to objects inserted instead of making copies.
Creating an array:
cArray array("array");
Adding an object at a given index (if the index is occupied, you will get an error message):
cMsgPar *p = new cMsgPar("par");
int index = array.addAt(5,p);
183
OMNeT++ Simulation Manual – The Simulation Library
You can also search the array or get a pointer to an object by the object’s name:
int index = array.find("par");
Par *p = (cPar *) array["par"];
You can remove an object from the array by calling remove() with the object name, the index
position, or the object pointer:
array.remove("par");
array.remove(index);
array.remove(p);
The remove() function doesn’t deallocate the object; it returns the object pointer. If you also
want to deallocate it, you can write:
delete array.remove(index);
Iteration
cArray has no iterator, but it is easy to loop through all the indices with an integer variable.
The size() member function returns the largest index plus one.
for (int i = 0; i < array.size(); i++) {
if (array[i]) { // is this position used?
cObject *obj = array[i];
EV << obj->getName() << endl;
}
}
7.6.1 Overview
The cTopology class was designed primarily to support routing in communication networks.
A cTopology object stores an abstract representation of the network in a graph form:
One can specify which modules to include in the graph. Compound modules may also be
selected. The graph will include all connections among the selected modules. In the graph,
all nodes are at the same level; there is no submodule nesting. Connections that span across
compound module boundaries are also represented as one graph edge. Graph edges are
directed, just as module gates are.
If you are writing a router or switch model, the cTopology graph can help you determine what
nodes are available through which gate and also find optimal routes. The cTopology object
can calculate shortest paths between nodes for you.
184
OMNeT++ Simulation Manual – The Simulation Library
The mapping between the graph (nodes, edges) and the network model (modules, gates, con-
nections) is preserved: one can find the corresponding module for a cTopology node and vice
versa.
One can extract the network topology into a cTopology object with a single method call. There
are several ways to specify which modules should be included in the topology:
• by module type
• by a parameter’s presence and value
• with a user-supplied Boolean function
First, you can specify which node types you want to include. The following code extracts all
modules of type Router or Host. (Router and Host can be either simple or compound module
types.)
cTopology topo;
topo.extractByModuleType("Router", "Host", nullptr);
Any number of module types can be supplied; the list must be terminated by nullptr.
A dynamically assembled list of module types can be passed as a nullptr-terminated array of
const char* pointers, or in an STL string vector std::vector<std::string>. An example
of the former:
cTopology topo;
const char *typeNames[3];
typeNames[0] = "Router";
typeNames[1] = "Host";
typeNames[2] = nullptr;
topo.extractByModuleType(typeNames);
Second, you can extract all modules that have a certain parameter:
topo.extractByParameter("ipAddress");
You can also specify that the parameter must have a certain value for the module to be
included in the graph:
cMsgPar yes = "yes";
topo.extractByParameter("includeInTopo", &yes);
The third form allows you to pass a function that can determine for each module whether it
should or should not be included. You can have cTopology pass supplemental data to the
function through a void* pointer. An example that selects all top-level modules (and does not
use the void* pointer):
int selectFunction(cModule *mod, void *)
{
return mod->getParentModule() == getSimulation()->getSystemModule();
}