21.2 Basic class wrapping

As a very basic example of wrapping a class, let’s consider a minimal wrapper for SessionReader:


Listing 21.2: Basic class wrapping with pybind11
1void export_SessionReader(py::module &m) 
2{ 
3  py::class_<SessionReader, std::shared_ptr<SessionReader>>( 
4      m, "SessionReader") 
5 
6    .def_static("CreateInstance", SessionReader_CreateInstance) 
7 
8    .def("GetSessionName", &SessionReader::GetSessionName, 
9         py::return_value_policy::copy) 
10 
11    .def("InitSession", &SessionReader::InitSession, 
12         py::arg("filenames") = py::list()) 
13 
14    .def("Finalise", &SessionReader::Finalise) 
15    ; 
16}

21.2.1 py::class_<>

This pybind11 object defines a Python class in C++. It is templated, and in this case we have the following template arguments:

We then have two arguments:

21.2.2 Wrapping member functions

We then call the .def function on the class_<>, which allows us to define member functions on our class. This is equivalent to def-ing a function in Python. .def has two required parameters, and one optional parameter:

pybind11 is very smart and can convert many Python objects to their equivalent C++ function arguments, and C++ return types of the function to their respective Python object. Many times therefore, one only needs to define the .def() call.

However, there are some instances where we need to do some additional conversion, mask some C++ complexity from the Python interface, or deal with functions that return references. We describe ways to deal with this below.

Thin wrappers

Instead of defining a function pointer to a member of the C++ class, we can define a function pointer to a separate function that defines some extra functionality. This is called a thin wrapper.

As an example, consider the CreateInstance function. In C++ we pass this function the command line arguments in the usual argc, argv format. In Python, command line arguments are defined as a list of strings inside sys.argv. However, pybind11 does not know how to convert this list to argc, argv, so we need some additional code.


Listing 21.3: Thin wrapper example
1SessionReaderSharedPtr SessionReader_CreateInstance(py::list &ns) 
2{ 
3    // ... some code here that converts a Python list to the standard 
4    // c/c++ (int argc, char **argv) format for command line arguments. 
5    // Then use this to construct a SessionReader and return it. 
6    SessionReaderSharedPtr sr = SessionReader::CreateInstance(argc, argv); 
7    return sr; 
8}

In Python, we can then simply call session = SessionReader.CreateInstance(sys.argv).

Dealing with references

When dealing with functions in C++ that return references, e.g. const NekDouble &GetFactor() we need to supply an additional argument to .def(), since Python immutable types such as strings and integers cannot be passed by reference. There are also lifetime implications; i.e. if the C++ object being referenced is destroyed whilst still being used in Python, this would cause the program to abort.

To address this, we can define a return value policy which allows pybind11 to take various actions based on the intent of the function. For a full list of options, consult the pybind11] guide on return value policies. However in this case we opt to use copy as highlighted above, which will create a copy of the const reference and return this to Python, thereby avoiding any lifetime issues.

Dealing with Array<OneD, >

The LibUtilities/Python/BasicUtils/SharedArray.cpp file contains a number of functions that allow for the automatic conversion of Nektar++ Array<OneD, > storage to and from NumPy ndarray objects. This means that you can wrap functions that take these as parameters and return arrays very easily. However bear in mind the following caveats:

More information on the memory management and how the memory is shared can be found in Section 23.

21.2.3 Inheritance

Nektar++ makes heavy use of inheritance, which can be translated to Python quite easily using pybind11. For a good example of how to do this, you can examine the StdRegions wrapper for StdExpansion and its elements such as StdQuadExp. In a cut-down form, these look like the following:


Listing 21.4: Inheritance with pybind11
1void export_StdExpansion(py::module &m) 
2{ 
3    py::class_<StdExpansion, std::shared_ptr<StdExpansion>>(m, "StdExpansion"); 
4} 
5void export_StdQuadExp(py::module &m) 
6{ 
7    py::class_<StdQuadExp, StdExpansion, std::shared_ptr<StdQuadExp>>( 
8        m, "StdQuadExp", py::multiple_inheritance()) 
9        .def(py::init<const LibUtilities::BasisKey &, 
10                      const LibUtilities::BasisKey &>()); 
11}

Note the following:

21.2.4 Wrapping enums

Most Nektar++ enumerators come in the form:

1enum MyEnum { 
2    eItemOne, 
3    eItemTwo, 
4    SIZE_MyEnum 
5}; 
6static const char *MyEnumMap[] = { 
7    "ItemOne" 
8    "ItemTwo" 
9    "ItemThree" 
10};

To wrap this, you can use the NEKPY_WRAP_ENUM macro defined in NekPyConfig.hpp, which in this case can be used as NEKPY_WRAP_ENUM(m, MyEnum, MyEnumMap). Note that if instead of const char * the map is defined as a const std::string, you can use the NEKPY_WRAP_ENUM_STRING macro.