Using Python to Run ns-3

Python bindings allow the C++ code in ns-3 to be called from Python.

This chapter shows you how to create a Python script that can run ns-3 and also the process of creating Python bindings for a C++ ns-3 module.

Introduction

Python bindings provide support for importing ns-3 model libraries as Python modules. Coverage of most of the ns-3 C++ API is provided. The intent has been to allow the programmer to write complete simulation scripts in Python, to allow integration of ns-3 with other Python tools and workflows. The intent is not to provide a different language choice to author new ns-3 models implemented in Python.

Python bindings for ns-3 use a tool called PyBindGen (https://github.com/gjcarneiro/pybindgen) to create Python modules from the C++ libraries built by CMake. The Python bindings that PyBindGen uses are maintained in a bindings directory in each module, and must be maintained to match the C++ API of that ns-3 module. If the C++ API changes, the Python bindings file must either be modified by hand accordingly, or the bindings must be regenerated by an automated scanning process.

If a user is not interested in Python, no action is needed; by default, Python bindings are disabled (and must be explicitly enabled at CMake configure time). In this case, changes to the C++ API of a provided module will not cause the module to fail to compile.

The process for automatically generating Python bindings relies on a toolchain involving a development installation of the Clang compiler, a program called CastXML (https://github.com/CastXML/CastXML), and a program called pygccxml (https://github.com/gccxml/pygccxml). The toolchain can be installed using the ns-3 bake build tool.

As of ns-3.36, the Python bindings framework (including many of its constituent tools) has lacked active maintainers for several years, and there are constraints on the compatible Python version and on the C++ language features that may be used in the C++ header files. The API scanning framework (in particular, pygccxml) requires Python 3.6 through 3.8 but is not compatible with Python 3.9 or 3.10. Compiling the existing bindings should work through Python 3.10. Additionally, many newer C++ language features beyond C++11 may not be supported by pybindgen.

Python bindings may be removed from future ns-3 releases if new maintainers are not found.

An Example Python Script that Runs ns-3

Here is some example code that is written in Python and that runs ns-3, which is written in C++. This Python example can be found in examples/tutorial/first.py:

import ns.applications
import ns.core
import ns.internet
import ns.network
import ns.point_to_point

ns.core.LogComponentEnable("UdpEchoClientApplication", ns.core.LOG_LEVEL_INFO)
ns.core.LogComponentEnable("UdpEchoServerApplication", ns.core.LOG_LEVEL_INFO)

nodes = ns.network.NodeContainer()
nodes.Create(2)

pointToPoint = ns.point_to_point.PointToPointHelper()
pointToPoint.SetDeviceAttribute("DataRate", ns.core.StringValue("5Mbps"))
pointToPoint.SetChannelAttribute("Delay", ns.core.StringValue("2ms"))

devices = pointToPoint.Install(nodes)

stack = ns.internet.InternetStackHelper()
stack.Install(nodes)

address = ns.internet.Ipv4AddressHelper()
address.SetBase(ns.network.Ipv4Address("10.1.1.0"), ns.network.Ipv4Mask("255.255.255.0"))

interfaces = address.Assign (devices);

echoServer = ns.applications.UdpEchoServerHelper(9)

serverApps = echoServer.Install(nodes.Get(1))
serverApps.Start(ns.core.Seconds(1.0))
serverApps.Stop(ns.core.Seconds(10.0))

echoClient = ns.applications.UdpEchoClientHelper(interfaces.GetAddress(1), 9)
echoClient.SetAttribute("MaxPackets", ns.core.UintegerValue(1))
echoClient.SetAttribute("Interval", ns.core.TimeValue(ns.core.Seconds (1.0)))
echoClient.SetAttribute("PacketSize", ns.core.UintegerValue(1024))

clientApps = echoClient.Install(nodes.Get(0))
clientApps.Start(ns.core.Seconds(2.0))
clientApps.Stop(ns.core.Seconds(10.0))

ns.core.Simulator.Run()
ns.core.Simulator.Destroy()

Running Python Scripts

First, we need to enable the build of Python bindings:

$ ./ns3 configure --enable-python-bindings

Other options such as --enable-examples may be passed to the above command. ns3 contains some options that automatically update the python path to find the ns3 module. To run example programs, there are two ways to use ns3 to take care of this. One is to run a ns3 shell; e.g.:

$ ./ns3 shell
$ python3 examples/wireless/mixed-wireless.py

and the other is to use the ‘run’ option to ns3:

$ ./ns3 run examples/wireless/mixed-wireless.py

Use the --no-build option to run the program without invoking a project rebuild. This option may be useful to improve execution time when running the same program repeatedly but with different arguments, such as from scripts.

$ ./ns3 run --no-build examples/wireless/mixed-wireless.py

To run a python script under the C debugger:

$ ./ns3 shell
$ gdb --args python3 examples/wireless/mixed-wireless.py

To run your own Python script that calls ns-3 and that has this path, /path/to/your/example/my-script.py, do the following:

$ ./ns3 shell
$ python3 /path/to/your/example/my-script.py

Caveats

In addition to limitations in Python and C++ versions supported (as discussed above), some additional limitations are listed here.

Incomplete Coverage

First of all, keep in mind that not 100% of the API is supported in Python. Some of the reasons are:

  1. some of the APIs involve pointers, which require knowledge of what kind of memory passing semantics (who owns what memory). Such knowledge is not part of the function signatures, and is either documented or sometimes not even documented. Annotations are needed to bind those functions;
  2. Sometimes a unusual fundamental data type or C++ construct is used which is not yet supported by PyBindGen;
  3. CastXML does not report template based classes unless they are instantiated.

Most of the missing APIs can be wrapped, given enough time, patience, and expertise, and will likely be wrapped if bug reports are submitted. However, don’t file a bug report saying “bindings are incomplete”, because the project does not have maintainers to maintain every API.

Conversion Constructors

Conversion constructors are not fully supported yet by PyBindGen, and they always act as explicit constructors when translating an API into Python. For example, in C++ you can do this:

Ipv4AddressHelper ipAddrs;
ipAddrs.SetBase ("192.168.0.0", "255.255.255.0");
ipAddrs.Assign (backboneDevices);

In Python, you have to do:

ipAddrs = ns.internet.Ipv4AddressHelper()
ipAddrs.SetBase(ns.network.Ipv4Address("192.168.0.0"), ns.network.Ipv4Mask("255.255.255.0"))
ipAddrs.Assign(backboneDevices)

CommandLine

CommandLine::AddValue() works differently in Python than it does in ns-3. In Python, the first parameter is a string that represents the command-line option name. When the option is set, an attribute with the same name as the option name is set on the CommandLine() object. Example:

NUM_NODES_SIDE_DEFAULT = 3

cmd = ns3.CommandLine()

cmd.NumNodesSide = None
cmd.AddValue("NumNodesSide", "Grid side number of nodes (total number of nodes will be this number squared)")

cmd.Parse(argv)

[...]

if cmd.NumNodesSide is None:
    num_nodes_side = NUM_NODES_SIDE_DEFAULT
else:
    num_nodes_side = int(cmd.NumNodesSide)

Tracing

Callback based tracing is not yet properly supported for Python, as new ns-3 API needs to be provided for this to be supported.

Pcap file writing is supported via the normal API.

ASCII tracing is supported via the normal C++ API translated to Python. However, ASCII tracing requires the creation of an ostream object to pass into the ASCII tracing methods. In Python, the C++ std::ofstream has been minimally wrapped to allow this. For example:

ascii = ns3.ofstream("wifi-ap.tr") # create the file
ns3.YansWifiPhyHelper.EnableAsciiAll(ascii)
ns3.Simulator.Run()
ns3.Simulator.Destroy()
ascii.close() # close the file

There is one caveat: you must not allow the file object to be garbage collected while ns-3 is still using it. That means that the ‘ascii’ variable above must not be allowed to go out of scope or else the program will crash.

Working with Python Bindings

Python bindings are built on a module-by-module basis, and can be found in each module’s bindings directory.

Overview

The python bindings are generated into an ‘ns’ namespace. Examples:

from ns.network import Node
n1 = Node()

or

import ns.network
n1 = ns.network.Node()

The best way to explore the bindings is to look at the various example programs provided in ns-3; some C++ examples have a corresponding Python example. There is no structured documentation for the Python bindings like there is Doxygen for the C++ API, but the Doxygen can be consulted to understand how the C++ API works.

Python Bindings Workflow

The process by which Python bindings are handled is the following:

  1. Periodically a developer uses a CastXML (https://github.com/CastXML/CastXML) based API scanning script, which saves the scanned API definition as bindings/python/ns3_module_*.py files or as Python files in each modules’ bindings directory. These files are kept under version control in the main ns-3 repository;
  2. Other developers clone the repository and use the already scanned API definitions;
  3. When configuring ns-3, pybindgen will be automatically downloaded if not already installed. Released ns-3 tarballs will ship a copy of pybindgen.

If something goes wrong with compiling Python bindings and you just want to ignore them and move on with C++, you can disable Python by configuring ns-3 without Python bindings enabled.

Regenerating the Python bindings

ns-3 will fail to successfully compile the Python bindings if the C++ headers are changed and no longer align with the stored Python bindings. In this case, the developer has two main choices: 1) disable Python as described above, or 2) update the bindings to align with the new C++ API.

Process Overview

ns-3 has an automated process to regenerate Python bindings from the C++ header files. The process is only supported for Linux at the moment because the project has not found a contributor yet to test and document the capability on macOS. In short, the process currently requires the following steps on a Linux machine.

  1. Prepare the system for scanning by installing the prerequisites, including a development version of clang, the CastXML package, pygccxml, and pybindgen.
  2. Perform a scan of the module of interest or all modules

Installing a clang development environment

Make sure you have a development version of the clang compiler installed on your system. This can take a long time to build from source. Linux distributions provide binary library packages such as libclang-dev (Ubuntu) or clang-devel (Fedora).

Installing other prerequisites

cxxfilt is a new requirement, typically installed using pip or pip3; e.g.

pip3 install --user cxxfilt

See also the wiki for installation notes for your system.

Set up a bake build environment

Try the following commands:

$ cd bake
$ export PATH=`pwd`/build/bin:$PATH
$ export LD_LIBRARY_PATH=`pwd`/build/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
$ export PYTHONPATH=`pwd`/build/lib${PYTHONPATH:+:$PYTHONPATH}
$ mkdir -p build/lib

Configure

Perform a configuration at the bake level:

$ ./bake.py configure -e ns-3-dev -e pygccxml

The output of ./bake.py show should show something like this:

$ ./bake.py show

Should say (note: some are OK to be ‘Missing’ for Python bindings scanning):

-- System Dependencies --
 > clang-dev - OK
 > cmake - OK
 > cxxfilt - OK
 > g++ - OK
 > gi-cairo - OK or Missing
 > gir-bindings - OK or Missing
 > llvm-dev - OK
 > pygobject - OK or Missing
 > pygraphviz - OK or Missing
 > python3-dev - OK
 > python3-setuptools - OK
 > qt - OK or Missing
 > setuptools - OK

Note that it is not harmful for Python bindings if some of the items above report as Missing. For Python bindings, the important prerequisites are clang-dev, cmake, cxxfilt, llvm-dev, python3-dev, and python3-setuptools. In the following process, the following programs and libraries will be locally installed: castxml, pybindgen, pygccxml, and ns-3.

Note also that the ns-3-allinone target for bake will also include the pygccxml and ns-3-dev targets (among other libraries) and can be used instead, e.g.:

$ ./bake.py configure -e ns-3-allinone

Download

Issue the following download command. Your output may vary depending on what is present or missing on your system.

$ ./bake.py download
 >> Searching for system dependency llvm-dev - OK
 >> Searching for system dependency clang-dev - OK
 >> Searching for system dependency qt - Problem
 > Problem: Optional dependency, module "qt" not available
   This may reduce the  functionality of the final build.
   However, bake will continue since "qt" is not an essential dependency.
   For more information call bake with -v or -vvv, for full verbose mode.

 >> Searching for system dependency g++ - OK
 >> Searching for system dependency cxxfilt - OK
 >> Searching for system dependency setuptools - OK
 >> Searching for system dependency python3-setuptools - OK
 >> Searching for system dependency gi-cairo - Problem
 > Problem: Optional dependency, module "gi-cairo" not available
   This may reduce the  functionality of the final build.
   However, bake will continue since "gi-cairo" is not an essential dependency.
   For more information call bake with -v or -vvv, for full verbose mode.

 >> Searching for system dependency gir-bindings - Problem
 > Problem: Optional dependency, module "gir-bindings" not available
   This may reduce the  functionality of the final build.
   However, bake will continue since "gir-bindings" is not an essential dependency.
   For more information call bake with -v or -vvv, for full verbose mode.

 >> Searching for system dependency pygobject - Problem
 > Problem: Optional dependency, module "pygobject" not available
   This may reduce the  functionality of the final build.
   However, bake will continue since "pygobject" is not an essential dependency.
   For more information call bake with -v or -vvv, for full verbose mode.

 >> Searching for system dependency pygraphviz - Problem
 > Problem: Optional dependency, module "pygraphviz" not available
   This may reduce the  functionality of the final build.
   However, bake will continue since "pygraphviz" is not an essential dependency.
   For more information call bake with -v or -vvv, for full verbose mode.

 >> Searching for system dependency python3-dev - OK
 >> Searching for system dependency cmake - OK
 >> Downloading castxml - OK
 >> Downloading netanim - OK
 >> Downloading pybindgen - OK
 >> Downloading pygccxml - OK
 >> Downloading ns-3-dev - OK

Build

Next, try the following command:

$ ./bake.py build

A build report should be printed for each package, such as:

>> Building castxml - OK
>> Building netanim - OK
>> Building pybindgen - OK
>> Building pygccxml - OK
>> Building ns-3-dev - OK

However, if there is a problem with the bindings compilation (or with the C++ code), it will report a failure instead:

>> Building ns-3-dev - Problem
> Error:  Critical dependency, module "ns-3-dev" failed
  For more information call Bake with --debug and/or -v, -vvv, for full verbose mode (bake --help)

At this point, it is recommended to change into the ns-3-dev directory and work further from there, because the API scanning dependencies have been built and installed successfully into the build directory. The output of ‘./ns3 configure’ can be inspected to see if Python API scanning support is enabled:

Python API Scanning Support   : enabled

It may say something like this, if the support is not active or something went wrong in the build process:

Python API Scanning Support   : not enabled (Missing 'pygccxml' Python module)

In this case, the user must take additional steps to resolve. For the API scanning support to be detected, the castxml binary must be in the shell’s PATH, and pygccxml must be in the PYTHONPATH.

LP64 vs ILP32 bindings

Linux (64-bit, as most modern installations use) and MacOS use different data models, as explained here: https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/com.ibm.zos.v2r3.cbcpx01/datatypesize64.htm

Linux uses the LP64 model, and MacOS (as well as 32-bit Linux) use the ILP32 model. Users will note that there are two versions of bindings files in each ns-3 module directory; one with an ILP32.py suffix and one with an LP64.py suffix. Only one is used on any given platform. The main difference is in the representation of the 64 bit integer type as either a ‘long’ (LP64) or ‘long long’ (ILP32).

The process (only supported on Linux at present) generates the LP64 bindings using the toolchain and then copies the LP64 bindings to the ILP32 bindings with some type substitutions automated by CMake scripts.

Rescanning a module

To re-scan a module, it is recommended to clean the installation, and then to configure with Python scanning enabled:

$ cd source/ns-3-dev
$ ./ns3 clean
$ ./ns3 configure -- -DNS3_SCAN_PYTHON_BINDINGS=ON

Ensure in the configure output that pygccxml and castxml were found. To scan an individual module, such as wifi, append -apiscan to the module name:

$ ./ns3 build wifi-apiscan

To re-scan all modules (which can take some time):

$ cd source/ns-3-dev
$ ./ns3 apiscan-all

Then, to check whether the rescanned bindings can be compiled, enable the Python bindings in the build:

$ ./ns3 configure --enable-python-bindings
$ ./ns3 build

Generating bindings on MacOS

In principle, this should work (and should generate the 32-bit bindings). However, maintainers have not been available to complete this port to date. We would welcome suggestions on how to enable scanning for MacOS.

Regenerating the Python bindings using gitlab-ci-local

The ns-3 GitLab.com continuous integration (CI) system can be run on a local machine using `gitlab-ci-local < https://github.com/firecow/gitlab-ci-local>`_. From within the ns-3 directory:

$ gitlab-ci-local --file ./utils/tests/gitlab-ci.yml pybindgen-21.04

If everything works, the diff with the re-scanned bindings will be in ./gitlab-ci-local/artifacts/pybindgen-21.04.

Regenerating the Python bindings using Docker

An alternative to the above Bake install or gitlab-ci-local is to use a Docker container. Basically, the below steps are the low-level commands used by the gitlab-ci-local approach above. The following steps are used by the ns-3 CI system on an Ubuntu 20.04 image.

As a prerequisite, install Docker using a package manager; on Ubuntu, use:

$ apt install docker.io

To allow an unprivileged user to use Docker, perform the following:

$ sudo chmod 666 /var/run/docker.sock

The following command will obtain a raw shell for an Ubuntu 20.04 image:

$ docker run -it ubuntu:20.04

However, if you prefer to work on your current directory from the container shell, use the -v option to request a bind mount of a host directory to a container directory, such as follows:

$ docker run -it -v /path/to/your/current/directory:/path/to/container/mount ubuntu:20.04
$ cd /path/to/container/mount

where /path/to/container/mount can be something like /ns-3-dev.

Then, from within a directory that contains ns-3, perform the following steps to rescan the APIs and test them:

$ apt-get update
$ apt-get install -y g++ cmake ninja-build ccache libgsl-dev libgtk-3-dev libboost-dev wget git python3 python3-pip
$ pip install cxxfilt pygccxml pybindgen castxml
$ ./ns3 clean
$ ./ns3 configure -G Ninja -- -DNS3_SCAN_PYTHON_BINDINGS=ON
$ ./ns3 build apiscan-all
$ ./ns3 configure --enable-python-bindings
$ ./ns3 build
$ ./ns3 run ./utils/python-unit-tests.py

If you only want to scan one module, specify MODULENAME-apiscan (such as wifi-apiscan) instead of apiscan-all in the above.

If you exit the container, the package state will be lost, but from another terminal window, you may be able to further test and evaluate the new bindings. Note that the new bindings and build files will have root file ownership.

Organization of the Modular Python Bindings

The src/<module>/bindings directory may contain the following files, some of them optional:

  • callbacks_list.py: this is a scanned file, DO NOT TOUCH. Contains a list of Callback<…> template instances found in the scanned headers;
  • modulegen__gcc_LP64.py: this is a scanned file, DO NOT TOUCH. Scanned API definitions for the GCC, LP64 architecture (64-bit)
  • modulegen__gcc_ILP32.py: this is a scanned file, DO NOT TOUCH. Scanned API definitions for the GCC, ILP32 architecture (32-bit)
  • modulegen_customizations.py: you may optionally add this file in order to customize the pybindgen code generation
  • scan-header.h: you may optionally add this file to customize what header file is scanned for the module. Basically this file is scanned instead of ns3/<module>-module.h. Typically, the first statement is #include “ns3/<module>-module.h”, plus some other stuff to force template instantiations;
  • module_helpers.cc: you may add additional files, such as this, to be linked to python extension module. They will be automatically scanned;
  • <module>.py: if this file exists, it becomes the “frontend” python module for the ns3 module, and the extension module (.so file) becomes _<module>.so instead of <module>.so. The <module>.py file has to import all symbols from the module _<module> (this is more tricky than it sounds, see src/core/bindings/core.py for an example), and then can add some additional pure-python definitions.

Historical Information

If you are a developer and need more background information on ns-3’s Python bindings, please see the Python Bindings wiki page. Please note, however, that some information on that page is stale.