Building Simulation Programs

Overview

This chapter describes the process and tools for building executable simulation models from their source code.

As described in the previous chapters, the source of an OMNeT++ model usually contains the following files:

  • C++ (.cc and .h) files, containing simple module implementations and other code;

  • Message (.msg) files, containing message definitions to be translated into C++ classes;

  • NED (.ned) files with component declarations and topology descriptions;

  • Configuration (.ini) files with model parameter assignments and other settings.

The process to turn the source into an executable form is this, in a nutshell:

  1. Message files are translated into C++ using the message compiler, opp_msgc

  2. C++ sources are compiled into object form (.o files)

  3. Object files are linked with the simulation kernel and other libraries to get an executable or a shared library

Note that apart from the first step, the process is the same as building any C/C++ program. Also, note that NED and ini files do not play a part in this process, as they are loaded by the simulation program at runtime.

One needs to link with the following libraries:

  • The simulation kernel and class library (the oppsim library) and its dependencies (oppenvir, oppcommon, oppnedxml, etc).

  • Optionally, one or more user interface libraries (oppqtenv, oppcmdenv). Note that these libraries themselves may depend on other libraries.

The exact file names of libraries depend on the platform and a number of additional factors. [1]

Fig. 9.1 shows an overview of the process of building (and running) simulation programs.

g cc C++ sources linking Compiling and linking cc->linking m_cc *_m.cc/h files m_cc->linking msg MSG files opp_msgc opp_msgc msg->opp_msgc ned NED files running Running ned->running ini omnetpp.ini ini->running libs Simulation kernel and user interface libraries libs->linking prog Simulation program prog->running result Result files opp_msgc->m_cc linking->prog running->result

Building and running simulation

You can see that the build process is not complicated. Tools such as make and opp_makemake, to be described in the rest of the chapter, are primarily needed to optimize rebuilds (if a message file has been translated already, there is no need to repeat the translation for every build unless the file has changed) and for automation.

Using opp_makemake and Makefiles

There are several tools available for managing the build of C/C++ programs. OMNeT++ uses the traditional way, Makefiles. Writing a Makefile is usually a tedious task. However, OMNeT++ provides a tool that can generate the Makefile for the user, saving manual labor.

opp_makemake can automatically generate a Makefile for simulation programs, based on the source files in the current directory and (optionally) in subdirectories.

Command-line Options

The most important options accepted by opp_makemake are:

  • -f, --force: Force overwriting an existing Makefile.

  • -o <filename>: Name of the simulation executable or library to be built.

  • -O <directory>, --out <directory>: Specifies the name of the output directory tree for out-of-directory build.

  • --deep: Generates a “deep” Makefile. A deep Makefile will cover the whole source tree under the make directory, not just files in that directory.

  • -r, --recurse: Causes make to recursively descend into all subdirectories; subdirectories are expected to contain Makefiles themselves.

  • -X <directory>, -X<directory>, --except <directory>: With -r or --deep: ignore the given directory.

  • -d<subdir>, -d <subdir>, --subdir <subdir>: Causes make to switch to the given directory and invoke a Makefile in that directory.

  • -n, --nolink: Produce object files but do not create an executable or library.

  • -s, --make-so: Build a shared library (.so, .dll, or .dylib).

  • -a, --make-lib: Create a static library (.a or .lib).

  • -I<dir>: Add the given directory to the C++ include path.

  • -D<name>[=<value>], -D <name>[=<value>], –define <name>[=<value>]: Define the given symbol in the C++ compiler.

  • -L<dir>: Add the given directory to the library path.

  • -l<library>: Additional library to link against.

There are several other options; run opp_makemake -h to see the complete list.

Basic Use

Assuming the source files (*.ned, *.msg, *.cc, *.h) are located in a single directory, one can change to that directory and type:

$ opp_makemake

This will create a file named Makefile. Now, running the make program will build a simulation executable.

$ make

Important

The generated Makefile will contain the names of the source files, so you need to re-run opp_makemake every time new files are added to or removed from the project.

To regenerate an existing Makefile, add the -f option to the command line, otherwise opp_makemake will refuse to overwrite it.

$ opp_makemake -f

The name of the output file will be derived from the name of the project directory (see later). It can be overridden with the -o option:

$ opp_makemake -f -o aloha

The generated Makefile supports the following targets:

  • all: Builds the simulation; this is also the default target.

  • clean: Deletes files that were produced by the make process.

Debug and Release Builds

opp_makemake generates a Makefile that can create both release and debug builds. By default, it creates a release version, but it is easy to override this behavior by defining the MODE variable on the make command line.

$ make MODE=debug

It is also possible to generate a Makefile that defaults to debug builds. This can be achieved by adding the --mode option to the opp_makemake command line.

$ opp_makemake --mode debug

Debugging the Makefile

opp_makemake generates a Makefile that prints only minimal information during the build process (only the name of the compiled file). To see the full compiler commands executed by the Makefile, add the V=1 parameter to the make command line.

$ make V=1

Using External C/C++ Libraries

If the simulation model relies on an external library, the following opp_makemake options can be used to make the simulation link with the library.

  • Use the -I<dir> option to specify the location of the header files. The directory will be added to the compiler’s include path. This option is not needed if the header files are at a standard location, e.g. installed under /usr/include on Linux.

  • Use the -L<dir> to specify the location of the binaries (static or shared library files). Again, this option is not needed if the binaries are at a standard place, e.g., under /usr/lib.

  • Use the -l<libname> to specify the name of the library. The name is normally the file name without the lib prefix and the file name extension (e.g., .a, .so, .dylib).

For example, linking with a hypothetical Foo library installed under /opt might require the following additional opp_makemake options: -I/opt/foo/include -L/opt/foo/lib -lfoo.

Building Directory Trees

It is possible to build a whole source directory tree with a single Makefile. A source tree will generate a single output file (executable or library). A source directory tree will always have a Makefile in its root, and source files may be placed anywhere in the tree.

To turn on this option, use the opp_makemake --deep option. opp_makemake will collect all .cc and .msg files from the whole subdirectory tree and generate a Makefile that covers all. To exclude a specific directory, use the -X exclude/dir/path option. (Multiple -X options are accepted.)

An example:

$ opp_makemake -f --deep -X experimental -X obsolete

In the C++ code, include statements should contain the location of the file relative to the Makefile’s location. [2] For example, if Foo.h is under utils/common/ in the source tree, it needs to be included as

#include "utils/common/Foo.h"

Dependency Handling

The make program can utilize dependency information in the Makefile to shorten build times by omitting build steps whose input has not changed since the last build. Dependency information is automatically created and kept up-to-date during the build process.

Dependency information is kept in .d files in the output directory.

Out-of-Directory Build

The build system creates object and executable files in a separate directory, called the output directory. By default, the output directory is out/<configname>, where the <configname> part depends on the compiler toolchain and build mode settings. (For example, the result of a debug build with GCC will be placed in out/gcc-debug.) The subdirectory tree inside the output directory will mirror the source directory structure.

Note

Generated source files (i.e. those created by opp_msgc) will be placed in the source tree rather than the output directory.

By default, the out directory is placed in the project root directory. This location can be changed with opp_makemake’s -O option.

$ opp_makemake -O ../tmp/obj

Note

The project directory is identified as the first ancestor of the current directory that contains a .project file.

Building Shared and Static Libraries

By default, the Makefile will create an executable file, but it is also possible to build shared or static libraries. Shared libraries are usually a better choice.

Use --make-so to create shared libraries, and --make-lib to build static libraries. The --nolink option completely omits the linking step, which is useful for top-level Makefiles that only invoke other Makefiles, or when custom linking commands are needed.

Note

The Microsoft Visual C++ compiler handles shared library (DLL) linking differently, and requires additional steps to compile and link correctly. Namely, one must choose a <symbol> for the project, and annotate all classes, functions, and variables to be exported from the DLL with <symbol>_API. Then, Makefiles need to be generated with the -p <symbol> option. This option will cause the <symbol>_EXPORT macro to be passed to the compiler, causing <symbol>_API to be defined as __declspec(dllexport) in the sources. This step allows Visual C++ to correctly generate DLL exports in Windows.

Recursive Builds

The --recurse option enables recursive make; when you build the simulation, make descends into the subdirectories and runs make in them too. By default, --recurse descends into all subdirectories; the -X <dir> option can be used to ignore certain subdirectories. This option is especially useful for top-level Makefiles.

The --recurse option automatically discovers subdirectories, but this is sometimes inconvenient. Your source directory tree may contain parts that need their own hand-written Makefile. This can happen if you include source files from another non-OMNeT++ project. With the -d <dir> or --subdir <dir> option, you can explicitly specify which directories to recurse into, and also, the directories need not be direct children of the current directory.

The recursive make options (--recurse, -d, --subdir) imply -X, that is, the directories recursed into will be automatically excluded from deep Makefiles.

You can control the order of traversal by adding dependencies into the makefrag file (see Customizing the Makefile)

Note

With -d, it is also possible to create infinite recursions. opp_makemake cannot detect them, it is your responsibility that cycles do not occur.

Motivation for recursive builds:

  • Top-level Makefile.

  • Integrating sources that have their own Makefile.

Customizing the Makefile

It is possible to add rules or otherwise customize the generated Makefile by providing a makefrag file. When you run opp_makemake, it will automatically insert the content of the makefrag file into the resulting Makefile. With the -i option, you can also name other files to be included in the Makefile.

makefrag will be inserted after the definitions but before the first rule, so it is possible to override existing definitions and add new ones, and also to override the default target.

makefrag can be useful if some of your source files are generated from other files (for example, you use generated NED files), or you need additional targets in your Makefile or just simply want to override the default target in the Makefile.

Note

If you change the content of the makefrag file, you must recreate the Makefile using the opp_makemake command.

Projects with Multiple Source Trees

In the case of a large project, your source files may be spread across several directories and your project may generate more than one executable file (i.e., several shared libraries, examples, etc.).

Once you have created your Makefiles with opp_makemake in every source directory tree, you will need a top-level Makefile. The top-level Makefile usually calls only the Makefiles recursively in the source directory trees.

A Multi-Directory Example

For a complex example of using opp_makemake, we will show how to create the Makefiles for a large project. First, take a look at the project’s directory structure and find the directories that should be used as source trees:

project/
    doc/
    images/
    simulations/
    contrib/ <-- source tree (build libmfcontrib.so from this dir)
    core/ <-- source tree (build libmfcore.so from this dir)
    test/ <-- source tree (build testSuite executable from this dir)

Additionally, there are dependencies between these output files: mfcontrib requires mfcore and testSuite requires mfcontrib (and indirectly mfcore).

First, we create the Makefile for the core directory. The Makefile will build a shared library from all .cc files in the core subtree and will name it mfcore:

$ cd core && opp_makemake -f --deep --make-so -o mfcore -O out

The contrib directory depends on mfcore, so we use the -L and -l options to specify the library we should link with.

$ cd contrib && opp_makemake -f --deep --make-so -o mfcontrib -O out \
  -I../core -L../out/\$\(CONFIGNAME\)/core -lmfcore

The testSuite will be created as an executable file that depends on both mfcontrib and mfcore.

$ cd test && opp_makemake -f --deep -o testSuite -O out \
    -I../core -I../contrib -L../out/\$\(CONFIGNAME\)/contrib -lmfcontrib

Now, let us specify the dependencies among the above directories. Add the lines below to the makefrag file in the project root directory.

contrib_dir: core_dir
test_dir: contrib_dir

Now the last step is to create a top-level Makefile in the root of the project that calls the previously created Makefiles in the correct order. We will use the --nolink option, exclude every subdirectory from the build (-X.), and explicitly call the above Makefiles using -d <dir>. opp_makemake will automatically include the above created makefrag file.

$ opp_makemake -f --nolink -O out -d test -d core -d contrib -X.

Project Features

Long compile times are often an inconvenience when working with large OMNeT++-based model frameworks. OMNeT++ has a facility named project features that lets you reduce build times by excluding or disabling parts of a large model library. For example, you can disable modules that you do not use for the current simulation study. The word feature refers to a piece of the project’s codebase that can be turned off as a whole.

Additional benefits of project features include enforcing cleaner separation of unrelated parts in the model framework, being able to exclude code written for other platforms, and a less cluttered model palette in the NED editor.

Note

Modularization could also be achieved by breaking up the model framework into several smaller projects, but that would cause other kinds of inconveniences for model developers and users alike.

Project features can be enabled/disabled from both the IDE and the command line. It is possible to query the list of enabled project features and use this information in creating a Makefile for the project.

What is a Project Feature

Features can be defined per project. As already mentioned, a feature is a piece of the project’s codebase that can be turned off as a whole, that is, excluded from the C++ sources (and thus from the build) and also from NED. Feature definitions are typically written and distributed by the author of the project; end users are only presented with the option of enabling/disabling those features. A feature definition contains:

  • Feature name; for example “UDP” or “Mobility examples”.

  • Feature description; This is a few sentences of text describing what the feature is or does; for example “Implementation of the UDP protocol”.

  • Labels; This is a list of labels or keywords that facilitate grouping or finding features.

  • Initially enabled. This is a boolean flag that determines the initial enablement of the feature.

  • Required features. Some features may be built on top of others; for example, an HMIPv6 protocol implementation relies on MIPv6, which in turn relies on IPv6. Thus, HMIPv6 can only be enabled if MIPv6 and IPv6 are enabled as well.

  • NED packages; This is a list of NED package names that identify the code that implements the feature. When you disable the feature, NED types defined in those packages and their subpackages will be excluded; also, C++ code in the folders that correspond to the packages (i.e. in the same folders as excluded NED files) will also be excluded.

  • Extra C++ source folders; If the feature contains C++ code that lives outside NED source folders (non-typical), those folders are listed here.

  • Compile options. When the feature is enabled, the compiler options listed here are added to the compiler command line of all C++ files in the project. Defines (-D options) are treated somewhat specially: the project can be set up so that defines go into a generated header file as #define lines instead of being added to the compiler command line. It is customary for each feature to have a corresponding symbol (WITH_FOO for a feature called Foo), so that other parts of the code can contain conditional blocks that are only compiled in when the given feature is enabled (or disabled).

  • Linker options. When the feature is enabled, the linker options listed here are added to the linker command line. A typical use of this field is linking with additional libraries that the feature’s code requires, for example libavcodec. Currently only the -l option (link with library) is supported here.

The opp_featuretool Program

Project features can be queried and manipulated using the opp_featuretool program. The first argument to the program must be a command; the most frequently used ones are list, enable and disable. The operation of commands can be refined with further options. One can obtain the full list of commands and options using the -h option.

Here are some examples of using the program.

Listing all features in the project:

$ opp_featuretool list

Listing all enabled features in the project:

$ opp_featuretool list -e

Enabling all features:

$ opp_featuretool enable all

Disabling a specific feature:

$ opp_featuretool disable Foo

The following command prints the command line options that should be used with opp_makemake to create a Makefile that builds the project with the currently enabled features:

$ opp_featuretool options

The easiest way to pass the output of the above command to opp_makemake is the $(…) shell construct:

$ opp_makemake --deep $(opp_featuretool options)

Often it is convenient to put feature defines (e.g. WITH_FOO) into a header file instead of passing them to the compiler via -D options. This makes it easier to detect feature enablements from derived projects, and also makes it easier for C++ code editors to correctly highlight conditional code blocks that depend on project features.

The header file can be generated with opp_featuretool using the following command:

$ opp_featuretool defines >feature_defines.h

At the same time, -D options must be removed from the compiler command line. opp_featuretool options has switches to filter them out. The modified command for Makefile generation:

$ opp_makemake --deep $(opp_featuretool options -fl)

It is advisable to create a Makefile rule that regenerates the header file when feature enablements change:

feature_defines.h: $(wildcard .oppfeaturestate) .oppfeatures
        opp_featuretool defines >feature_defines.h

The .oppfeatures File

Project features are defined in the .oppfeatures file in your project’s root directory. This is an XML file, and it has to be written by hand (there is no specialized editor for it).

The root element is <features>, and it may have several <feature> child elements, each defining a project feature. The fields of a feature are represented with XML attributes; attribute names are and linkerFlags. Items within attributes that represent lists (requires, labels, etc.) are separated by spaces.

Here is an example feature from the INET Framework:

<feature
  id="TCP_common"
  name="TCP Common"
  description = "The common part of TCP implementations"
  initiallyEnabled = "true"
  requires = "IPv4"
  labels = "Transport"
  nedPackages = "inet.transport.tcp_common
                 inet.applications.tcpapp
                 inet.util.headerserializers.tcp"
  extraSourceFolders = ""
  compileFlags = "-DWITH_TCP_COMMON"
  linkerFlags = ""
  />

Project feature enablements are stored in the .featurestate file.

How to Introduce a Project Feature

If you plan to introduce a project feature in your project, here’s what you’ll need to do:

  • Isolate the code that implements the feature into a separate source directory (or several directories). This is because only whole folders can be declared as part of a feature, individual source files cannot.

  • Check the remainder of the project. If you find source lines that reference code from the new feature, use conditional compilation (#ifdef WITH_YOURFEATURE) to make sure that the code compiles (and either works sensibly or throws an error) when the new feature is disabled. (Your feature should define the WITH_YOURFEATURE symbol, i.e. -DWITH_YOURFEATURE will need to be added to the feature compile flags.)

  • Add the feature description into the .oppfeatures file of your project.

  • Test. A rudimentary test is to verify that the project compiles at all, both with the new feature enabled and disabled. For projects with many features, automated build tests that compile the project using various feature configurations can be very useful. Such build tests can be written on top of opp_featuretool.