As a very basic example of wrapping a class, let’s consider a minimal wrapper for
SessionReader
:
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}
py::class_<>
This pybind11
object defines a Python class in C++. It is templated, and in this case we have
the following template arguments:
SessionReader
is the class that will be wrapped;
std::shared_ptr<SessionReader>
indicates that this object should be stored
inside a shared (or smart) pointer, which we frequently use throughout the library,
as can be seen by the frequent use of SessionReaderSharedPtr
.
We then have two arguments:
m
is the module we are going to define the class within.
"SessionReader"
is the name of the class in Python.
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:
The function name as a string, e.g. "GetSessionName"
A function pointer that defines the C++ function that will be called
An optional return policy, which we need to define when the C++ function returns a reference.
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.
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.
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)
.
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.
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:
Any NumPy ndarray
created from an Array<OneD, >
(and vice versa) will share
their memory. Although this avoids expensive memory copies, it means that
changing the C++ array changes the contents of the NumPy array (and vice versa).
Many functions in Nektar++ return Arrays through argument parameters. In Python this is a very unnatural way to write functions. For example:
1# This is good 2x, y, z = exp.GetCoords() 3# This is bad 4x, y, z = np.zeros(10), np.zeros(10), np.zeros(10) 5exp.GetCoords(x,y,z) 6# After this point x, y and z are updated as a result of GetCoords().
Use thin wrappers to overcome this problem. For examples of how to do this,
particularly in returning tuples, consult the StdRegions/StdExpansion.cpp
wrapper
which contains numerous examples.
TwoD
and ThreeD
arrays are not supported.
More information on the memory management and how the memory is shared can be found in Section 23.
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:
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:
StdExpansion
is an abstract class, so it has no initialiser and is non-copyable.
We write the parent classes of StdQuadExp
in the call to class_
. Note
that this does not necessarily need to include the full hierarchy of C++
inheritance: in StdRegions
the inheritance graph for StdQuadExp
looks like
StdExpansion -> StdExpansion2D -> StdQuadExp
. In the above wrapper, we
omit the StdExpansion2D
reference entirely. However, in order to do skip classes
in the inheritance tree, we need to tell pybind11
about this by passing the
py::multiple_inheritance
argument to the object.
py::init<>
is used to show how to wrap a C++ constructor. This can accept
any arguments for which you have either written explicit wrappers or pybind11
already knows how to convert.
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.