As a very basic example of wrapping a class, let’s consider the SessionReader
wrapper.
1void export_SessionReader() 2{ 3 py::class_<SessionReader, 4 std::shared_ptr<SessionReader>, 5 boost::noncopyable>( 6 "SessionReader", py::no_init) 7 8 .def("CreateInstance", SessionReader_CreateInstance) 9 .staticmethod("CreateInstance") 10 11 .def("GetSessionName", &SessionReader::GetSessionName, 12 py::return_value_policy<py::copy_const_reference>()) 13 14 .def("Finalise", &SessionReader::Finalise) 15 ; 16}
py::class_<>
This Boost.Python
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
boost::noncopyable
indicates that Boost.Python
shouldn’t try to automatically
wrap the copy constructor of SessionReader
. We add this here because of compiler
errors due to subclasses used inside SessionReader
, but generally, this should be
used for abstract classes which can’t be copied.
We then have two arguments:
"SessionReader"
is the name of the class in Python.
py::no_init
indicates this object has no publically-accessible initialiser. This
is because for SessionReader
, we define a factory-type function called
CreateInstance
instead.
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.
Boost.Python
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, Boost.Python
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. For a full list of
options, consult the Boost.Python
guide. However a good rule of thumb is to use
copy_const_reference
as highlighted above, which will create a copy of the const reference
and return this.
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)
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 Boost.Python
. 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() 2{ 3 py::class_<StdExpansion, 4 std::shared_ptr<StdExpansion>, 5 boost::noncopyable>( 6 "StdExpansion", py::no_init); 7} 8void export_StdQuadExp() 9{ 10 py::class_<StdQuadExp, py::bases<StdExpansion>, 11 std::shared_ptr<StdQuadExp> >( 12 "StdQuadExp", py::init<const LibUtilities::BasisKey&, 13 const LibUtilities::BasisKey&>()); 14}
Note the following:
StdExpansion
is an abstract class, so it has no initialiser and is non-copyable.
We use py::bases<StdExpansion>
in the definition of StdQuadExp
to define
its parent class. 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 call entirely.
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 Boost.Python
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(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.