Instead of depending on the Cython module, this recipe now depends on locating the Boost libraries on the system, in combination with the Python development headers and library.
The Python development headers and library are searched for with the following:
find_package(PythonInterp REQUIRED)
find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
Note how we first searched for the interpreter and then for the development headers and libraries. Moreover, the search for PythonLibs asks for the exact same major and minor versions for the development headers and libraries as were found for the interpreter. This is necessary for ensuring that consistent versions of interpreter and libraries are used throughout the project. However, this command combination will not guarantee that an exactly matching version of the two will be found.
When locating the Boost.Python component, we have met the difficulty that the name of the component that we try to locate depends both on the Boost version and our Python environment. Depending on the Boost version, the component can be called python, python2, python3, python27, python36, python37, and so on. We have solved this problem by searching from specific to more generic names and only failing if no match can be located:
list(
APPEND _components
python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}
python${PYTHON_VERSION_MAJOR}
python
)
set(_boost_component_found "")
foreach(_component IN ITEMS ${_components})
find_package(Boost COMPONENTS ${_component})
if(Boost_FOUND)
set(_boost_component_found ${_component})
break()
endif()
endforeach()
if(_boost_component_found STREQUAL "")
message(FATAL_ERROR "No matching Boost.Python component found")
endif()
Discovery and usage of the Boost libraries can be tweaked by setting additional CMake variables. For example, CMake offers the following options:
- Boost_USE_STATIC_LIBS can be set to ON to force the use of the static version of the Boost libraries.
- Boost_USE_MULTITHREADED can be set to ON to ensure that the multithreaded version is picked up and used.
- Boost_USE_STATIC_RUNTIME can be set to ON such that our targets will use the variant of Boost that links the C++ runtime statically.
Another new aspect introduced by this recipe is the use of the MODULE option to the add_library command. We already know from Recipe 3, Building and linking shared and static libraries, in Chapter 1, From a Simple Executable to Libraries, that CMake accepts the following options as valid second argument to add_library:
- STATIC, to create static libraries; that is, archives of object files for use when linking other targets, such as executables
- SHARED, to create shared libraries; that is, libraries that can be linked dynamically and loaded at runtime
- OBJECT, to create object libraries; that is, object files without archiving them into a static library, nor linking them into a shared object
The MODULE option introduced here will generate a plugin library; that is, a Dynamic Shared Object (DSO) that is not linked dynamically into any executable, but can still be loaded at runtime. Since we are extending Python with our own functionality written in C++, the Python interpreter will need to be able to load our library at runtime. This can be achieved by using the MODULE option to add_library and by preventing the addition of any prefix (for example, lib on Unix systems) to the name of our library target. The latter operation is carried out by setting the appropriate target property, like so:
set_target_properties(account
PROPERTIES
PREFIX ""
)
One aspect of all recipes that demonstrate the interfacing of Python and C++ is that we need to describe to the Python code how to hook up to the C++ layer and to list the symbols which should be visible to Python. We also have the possibility to (re)name these symbols. In the previous recipe, we did this in a separate account.pyx file. When using Boost.Python, we describe the interface directly in the C++ code, ideally close to the definition of the class or function we wish to interface:
BOOST_PYTHON_MODULE(account) {
py::class_<Account>("Account")
.def("deposit", &Account::deposit)
.def("withdraw", &Account::withdraw)
.def("get_balance", &Account::get_balance);
}
The BOOST_PYTHON_MODULE template is included from <boost/python.hpp> and is responsible for creating the Python interface. The module will expose an Account Python class that maps to the C++ class. In this case, we do not have to explicitly declare a constructor and destructor – these are created for us and called automatically when the Python object is created:
myaccount = Account()
The destructor is called when the object goes out of scope and is collected by the Python garbage collection. Also, observe how BOOST_PYTHON_MODULE exposes the deposit, withdraw, and get_balance functions, and maps them to the corresponding C++ class methods.
This way, the compiled module can be found by Python when placed in PYTHONPATH. In this recipe, we have achieved a relatively clean separation between the Python and C++ layers. The Python code is not restricted in functionality, does not require type annotation or rewriting of names, and remains pythonic:
from account import Account
account1 = Account()
account1.deposit(100.0)
account1.deposit(100.0)
account2 = Account()
account2.deposit(200.0)
account2.deposit(200.0)
account1.withdraw(50.0)
assert account1.get_balance() == 150.0
assert account2.get_balance() == 400.0