.. include:: replace.txt .. highlight:: python .. heading hierarchy: ------------- Chapter ************* Section (#.#) ============= Subsection (#.#.#) ############# Paragraph (no number) Using Python to Run |ns3| ------------------------- Python bindings allow the C++ code in |ns3| to be called from Python. This chapter shows you how to create a Python script that can run |ns3| and also the process of creating Python bindings for a C++ |ns3| module. Python bindings are also needed to run the Pyviz visualizer. Introduction ************ Python bindings provide support for importing |ns3| model libraries as Python modules. Coverage of most of the |ns3| C++ API is provided. The intent has been to allow the programmer to write complete simulation scripts in Python, to allow integration of |ns3| with other Python tools and workflows. The intent is not to provide a different language choice to author new |ns3| models implemented in Python. As of ns-3.37 release or later, Python bindings for |ns3| use a tool called Cppyy (https://cppyy.readthedocs.io/en/latest/) to create a Python module from the C++ libraries built by CMake. The Python bindings that Cppyy uses are built at runtime, by importing the C++ libraries and headers for each |ns3| module. This means that even if the C++ API changes, the Python bindings will adapt to them without requiring any preprocessing or scanning. If a user is not interested in Python, no action is needed; the Python bindings are only built on-demand by Cppyy, and only if the user enables them in the configuration of |ns3|. It is also important to note that the current capability is provided on a lightly maintained basis and not officially supported by the project (in other words, we are currently looking for a Python bindings maintainer). The Cppyy framework could be replaced if it becomes too burdensome to maintain as we are presently doing. Prior to ns-3.37, the previous Python bindings framework was based on `Pybindgen `_. An Example Python Script that Runs |ns3| **************************************** Here is some example code that is written in Python and that runs |ns3|, which is written in C++. This Python example can be found in ``examples/tutorial/first.py``: :: from ns import ns 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)) address = ns.addressFromIpv4Address(interfaces.GetAddress(1)) echoClient = ns.applications.UdpEchoClientHelper(address, 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 ********************** The main prerequisite is to install `cppyy`. Depending on how you may manage Python extensions, the installation instructions may vary, but you can first check if it installed by seeing if the `cppyy` module can be successfully imported: .. sourcecode:: bash $ python3 Python 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import cppyy >>> If not, you may try to install via `pip` or whatever other manager you are using; e.g.: .. sourcecode:: bash $ python3 -m pip install --user cppyy First, we need to enable the build of Python bindings: .. sourcecode:: bash $ ./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.: .. sourcecode:: bash $ ./ns3 shell $ python3 examples/wireless/mixed-wireless.py and the other is to use the 'run' option to ns3: .. sourcecode:: bash $ ./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. .. sourcecode:: bash $ ./ns3 run --no-build examples/wireless/mixed-wireless.py To run a python script under the C debugger: .. sourcecode:: bash $ ./ns3 shell $ gdb --args python3 examples/wireless/mixed-wireless.py To run your own Python script that calls |ns3| and that has this path, ``/path/to/your/example/my-script.py``, do the following: .. sourcecode:: bash $ ./ns3 shell $ python3 /path/to/your/example/my-script.py Caveats ******* Some of the limitations of the Cppyy-based bindings 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: Memory-management issues ######################## 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. You may need to workaround these issues by instantiating variables on the C++ side with a Just-In-Time (JIT) compiled function. For example, when handling command-line arguments, we could set additional parameters like in the following code: .. sourcecode:: python # Import the ns-3 C++ modules with Cppyy from ns import ns # To pass the addresses of the Python variables to c++, we need to use ctypes from ctypes import c_bool, c_int, c_double, c_char_p, create_string_buffer verbose = c_bool(True) nCsma = c_int(3) throughputKbps = c_double(3.1415) BUFFLEN = 4096 outputFileBuffer = create_string_buffer(b"default_output_file.xml", BUFFLEN) outputFile = c_char_p(outputFileBuffer.raw) # Cppyy will transform the ctype types into the appropriate reference or raw pointers cmd = ns.CommandLine(__file__) cmd.AddValue("verbose", "Tell echo applications to log if true", verbose) cmd.AddValue("nCsma", "Number of extra CSMA nodes/devices", nCsma) cmd.AddValue("throughputKbps", "Throughput of nodes", throughputKbps) cmd.AddValue("outputFile", "Output file name", outputFile, BUFFLEN) cmd.Parse(sys.argv) # Printing values of the different ctypes passed as arguments post parsing print("Verbose:", verbose.value) print("nCsma:", nCsma.value) print("throughputKbps:", throughputKbps.value) print("outputFile:", outputFile.value) Note that the variables are passed as references or raw pointers. Reassigning them on the Python side (e.g. ``verbose = verbose.value``) can result in the Python garbage collector destroying the object since its only reference has been overwritten, allowing the garbage collector to reclaim that memory space. The C++ side will then have a dangling reference to the variable, which can be overwritten with unexpected values, which can be read later, causing ns-3 to behave erratically due to the memory corruption. String values are problematic since Python and C++ string lifetimes are handled differently. To workaround that, we need to use null-terminated C strings (``char*``) to exchange strings between the bindings and ns-3 module libraries. However, C strings are particularly dangerous, since overwriting the null-terminator can also result in memory corruption. When passing a C string, remember to allocate a large buffer and perform bounds checking whenever possible. The CommandLine::AddValue variant for ``char*`` performs bounds checking and aborts the execution in case the parsed value does not fit in the buffer. Make sure to pass the complete size of the buffer, including the null terminator. There is an example below demonstrating how the memory corruption could happen in case there was no bounds checking in CommandLine::AddValue variant for ``char*``. .. sourcecode:: python from ns import ns from ctypes import c_char_p, c_char, create_string_buffer, byref, cast # The following buffer represent the memory contents # of a program containing two adjacent C strings # This could be the result of two subsequent variables # on the stack or dynamically allocated memoryContents = create_string_buffer(b"SHORT_STRING_CONTENTS\0"+b"DoNotWriteHere_"*5+b"\0") lenShortString = len(b"SHORT_STRING_CONTENTS\0") # In the next lines, we pick pointers to these two C strings shortStringBuffer = cast(byref(memoryContents, 0), c_char_p) victimBuffer = cast(byref(memoryContents, lenShortString), c_char_p) cmd = ns.core.CommandLine(__file__) # in the real implementation, the buffer size of 21+1 bytes containing SHORT_STRING_CONTENTS\0 is passed cmd.AddValue("shortString", "", shortStringBuffer) print("Memory contents before the memory corruption") print("Full Memory contents", memoryContents.raw) print("shortStringBuffer contents: ", shortStringBuffer.value) print("victimBuffer contents: ", victimBuffer.value) # The following block should print to the terminal. # Note that the strings are correctly # identified due to the null terminator (\x00) # # Memory contents before the memory corruption # Full Memory contents b'SHORT_STRING_CONTENTS\x00DoNotWriteHere_DoNotWriteHere_DoNotWriteHere_DoNotWriteHere_DoNotWriteHere_\x00\x00' # shortStringBuffer size=21, contents: b'SHORT_STRING_CONTENTS' # victimBuffer size=75, contents: b'DoNotWriteHere_DoNotWriteHere_DoNotWriteHere_DoNotWriteHere_DoNotWriteHere_' # Write a very long string to a small buffer of size lenShortString = 22 cmd.Parse(["python", "--shortString="+("OkToWrite"*lenShortString)[:lenShortString]+"CORRUPTED_"*3]) print("\n\nMemory contents after the memory corruption") print("Full Memory contents", memoryContents.raw) print("shortStringBuffer contents: ", shortStringBuffer.value) print("victimBuffer contents: ", victimBuffer.value) # The following block should print to the terminal. # # Memory contents after the memory corruption # Full Memory contents b'OkToWriteOkToWriteOkToCORRUPTED_CORRUPTED_CORRUPTED_\x00oNotWriteHere_DoNotWriteHere_DoNotWriteHere_\x00\x00' # shortStringBuffer size=52, contents: b'OkToWriteOkToWriteOkToCORRUPTED_CORRUPTED_CORRUPTED_' # victimBuffer size=30, contents: b'CORRUPTED_CORRUPTED_CORRUPTED_' # # Note that shortStringBuffer invaded the victimBuffer since the # string being written was bigger than the shortStringBuffer. # # Since no bounds checks were performed, the adjacent memory got # overwritten and both buffers are now corrupted. # # We also have a memory leak of the final block in the memory # 'oNotWriteHere_DoNotWriteHere_DoNotWriteHere_\x00\x00', caused # by the null terminator written at the middle of the victimBuffer. If you find a segmentation violation, be sure to wait for the stacktrace provided by Cppyy and try to find the root cause of the issue. If you have multiple cores, the number of stacktraces will correspond to the number of threads being executed by Cppyy. To limit them, define the environment variable `OPENBLAS_NUM_THREADS=1`. Operators ######### Cppyy may fail to map C++ operators due to the implementation style used by |ns3|. This happens for the fundamental type `Time`. To provide the expected behavior, we redefine these operators from the Python side during the setup of the |ns3| bindings module (`ns-3-dev/bindings/python/ns__init__.py`). .. sourcecode:: python # Redefine Time operators cppyy.cppdef(""" using namespace ns3; bool Time_ge(Time& a, Time& b){ return a >= b;} bool Time_eq(Time& a, Time& b){ return a == b;} bool Time_ne(Time& a, Time& b){ return a != b;} bool Time_le(Time& a, Time& b){ return a <= b;} bool Time_gt(Time& a, Time& b){ return a > b;} bool Time_lt(Time& a, Time& b){ return a < b;} """) cppyy.gbl.ns3.Time.__ge__ = cppyy.gbl.Time_ge cppyy.gbl.ns3.Time.__eq__ = cppyy.gbl.Time_eq cppyy.gbl.ns3.Time.__ne__ = cppyy.gbl.Time_ne cppyy.gbl.ns3.Time.__le__ = cppyy.gbl.Time_le cppyy.gbl.ns3.Time.__gt__ = cppyy.gbl.Time_gt cppyy.gbl.ns3.Time.__lt__ = cppyy.gbl.Time_lt A different operator used by |ns3| is `operator Address()`, used to convert different types of Addresses into the generic type Address. This is not supported by Cppyy and requires explicit conversion. Some helpers have been added to handle the common cases. .. sourcecode:: python # Define ns.cppyy.gbl.addressFromIpv4Address and others cppyy.cppdef("""using namespace ns3; Address addressFromIpv4Address(Ipv4Address ip){ return Address(ip); }; Address addressFromInetSocketAddress(InetSocketAddress addr){ return Address(addr); }; Address addressFromPacketSocketAddress(PacketSocketAddress addr){ return Address(addr); }; """) # Expose addressFromIpv4Address as a member of the ns3 namespace (equivalent to ns) setattr(cppyy.gbl.ns3, "addressFromIpv4Address", cppyy.gbl.addressFromIpv4Address) setattr(cppyy.gbl.ns3, "addressFromInetSocketAddress", cppyy.gbl.addressFromInetSocketAddress) setattr(cppyy.gbl.ns3, "addressFromPacketSocketAddress", cppyy.gbl.addressFromPacketSocketAddress) 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. Tracing ======= Callback based tracing is not yet properly supported for Python, as new |ns3| 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 = ns.ofstream("wifi-ap.tr") # create the file ns.YansWifiPhyHelper.EnableAsciiAll(ascii) ns.Simulator.Run() ns.Simulator.Destroy() ascii.close() # close the file There is one caveat: you must not allow the file object to be garbage collected while |ns3| 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 **************************** Overview ======== The python bindings are generated into an 'ns' namespace. Examples: :: from ns import ns n1 = ns.network.Node() or :: from ns import* n1 = ns.network.Node() The best way to explore the bindings is to look at the various example programs provided in |ns3|; 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. Historical Information ********************** If you are a developer and need more background information on |ns3|'s Python bindings, please see the `Python Bindings wiki page `_. Please note, however, that some information on that page is stale.