In this recipe, we have interfaced Python and C++ using a relatively compact CMakeLists.txt file, but we have achieved this by using the FindCython.cmake and UseCython.cmake modules, which have been placed under cmake-cython. These modules are included using the following code:
# directory contains UseCython.cmake and FindCython.cmake
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake-cython)
# this defines cython_add_module
include(UseCython)
FindCython.cmake is included in UseCython.cmake and locates and defines ${CYTHON_EXECUTABLE}. The latter module defines the cython_add_module and cython_add_standalone_executable functions, which can be used to create Python modules and standalone executables, respectively. Both modules have been downloaded from https://github.com/thewtex/cython-cmake-example/tree/master/cmake.
In this recipe, we use cython_add_module to create a Python module library. Note how we set the non-standard CYTHON_IS_CXX source file property to TRUE, so that the cython_add_module function will know to compile pyx as a C++ file:
# tells UseCython to compile this file as a c++ file
set_source_files_properties(account.pyx PROPERTIES CYTHON_IS_CXX TRUE)
# create python module
cython_add_module(account account.pyx account.cpp)
The Python module is created inside ${CMAKE_CURRENT_BINARY_DIR}, and in order for the Python test.py script to locate it, we pass the relevant path with a custom environment variable, which is used inside test.py to set the PATH variable. Note how the COMMAND is set to call the CMake executable itself to set the local environment right before executing the Python script. This affords us platform-independence and avoids polluting the environment with spurious variables:
add_test(
NAME
python_test
COMMAND
${CMAKE_COMMAND} -E env ACCOUNT_MODULE_PATH=$<TARGET_FILE_DIR:account>
${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py
)
We should also take a look at the account.pyx file, which is the interface file between Python and C++ and describes the C++ interface:
# describe the c++ interface
cdef extern from "account.hpp":
cdef cppclass Account:
Account() except +
void deposit(double)
void withdraw(double)
double get_balance()
You can see except + in the Account class constructor. This directive allows Cython to handle exceptions raised by the C++ code.
The account.pyx interface file also describes the Python interface:
# describe the python interface
cdef class pyAccount:
cdef Account *thisptr
def __cinit__(self):
self.thisptr = new Account()
def __dealloc__(self):
del self.thisptr
def deposit(self, amount):
self.thisptr.deposit(amount)
def withdraw(self, amount):
self.thisptr.withdraw(amount)
def get_balance(self):
return self.thisptr.get_balance()
We can see how the cinit constructor, the __dealloc__ destructor, and the deposit and withdraw methods, are matched with the corresponding C++ implementation counterparts.
To summarize, we have found a mechanism to couple Python and C++ by introducing a dependency on the Cython module. This module can preferably be installed by pip into a virtual environment or Pipenv, or by using Anaconda.