4.2. Creating a Python Extension

PyMØD is primarily Python bindings for libMØD made using Boost.Python. The following shows a template for creating a Python extension with C++ functions (e.g., to extend libMØD). The complete source code for the example can be found here.

4.2.1. Library Source and Headers

For this examples the following C++ source with header should be exposed in the Python module. Header:

#ifndef AWESOMELIB_HPP
#define AWESOMELIB_HPP

#include <mod/graph/Graph.hpp>

namespace awesome {

std::shared_ptr<mod::graph::Graph> doStuff();

} // namespace awesome

#endif // AWESOMELIB_HPP

Source:

#include "stuff.hpp"

namespace awesome {

std::shared_ptr<mod::graph::Graph> doStuff() {
	auto g = mod::graph::Graph::fromSMILES("CCO");
	g->setName("Ethanol");
	return g;
}

} // namespace awesome

4.2.2. Exposing the Interface

See the Boost.Python documentation for instructions how to expose C++ code. Below is the code for creating our simple Python extension.

#include <boost/python.hpp>

#include "stuff.hpp"

#include <mod/Config.hpp>

namespace py = boost::python;

namespace {
	// this can be used to make sure the extension and mod is using the same shared library
	uintptr_t magicLibraryValue() {
		return (uintptr_t)&mod::getConfig();
	}
}

BOOST_PYTHON_MODULE(awesome) {
	py::def("magicLibraryValue", &magicLibraryValue);

	py::def("doStuff", &awesome::doStuff);
}

The example exposes a bit of extra functionality for a sanity check. Python will dlopen libMØD twice As both the PyMØD module and our extension requires it. The library uses static variables and strange things might happen if multiple instances of these exist. The MØD wrapper script changes the dlopen flags (setting RTLD_GLOBAL and RTLD_NOW) which should prevent multiple instances of the library. The function magicLibraryValue can be used to check that this is true (see the test script below).

4.2.3. Building With CMake

We can use CMake to build the example:

cmake_minimum_required(VERSION 3.10)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake)

project(PyModTestProject CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# MØD
# -------------------------------------------------------------------------
find_package(mod REQUIRED)


# Boost.Python
# -------------------------------------------------------------------------
set(v 1.64.0)
foreach(PY 3 34 35 36 37 38 39)
    set(lib "python${PY}")
    find_package(Boost ${v} QUIET COMPONENTS ${lib})
    if(Boost_FOUND)
        find_package(Boost ${v} COMPONENTS ${lib})
        set(PYTHON_TARGET ${lib})
        break()
    endif()
endforeach()
if(NOT Boost_FOUND)
    find_package(Boost ${v} REQUIRED COMPONENTS python3)
    message(FATAL_ERROR "Could not find Boost.Python for Python 3. Tried 'python' wih suffixes 3, 34, 35, 36, 37, 38, and 39.")
endif()


# Python 3
# -------------------------------------------------------------------------
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)


# Artefacts
# -------------------------------------------------------------------------

add_library(awesome MODULE
        pyModule.cpp
        stuff.cpp)
set_target_properties(awesome PROPERTIES PREFIX "") # so it doesn't get the "lib" prefix
target_link_libraries(awesome PUBLIC mod::libmod Boost::${PYTHON_TARGET} Python3::Python)
target_compile_options(awesome PRIVATE -Wall -Wextra -pedantic
        -Wno-unused-parameter
        -Wno-comment
        -Wno-unused-local-typedefs)

4.2.4. Testing the Extension

After configuring and building there should be a shared library awesome.so which contains the extension. Executing the following script using the wrapper script in the same folder as the shared libray should now work:

import awesome

# sanity check for multiple copies of libMØD
modValue = mod.magicLibraryValue()
ourValue = awesome.magicLibraryValue()
if modValue != ourValue:
	print("mod =", modValue)
	print("our =", ourValue)
	raise Exception("Magic values differ! I.e., more than one instance of libMØD has been loaded.")
# end if check

g = awesome.doStuff()
print("Got a graph:", g.name)
g.print()

Command to execute:

mod -f test.py