Create a Python Binding for C++ #
Background #
It becomes necessary at times to expand the FreeCAD API further by exposing functions that are available in the source code in c++ to the python. This process is generally referred to as creating a binding.
FreeCAD uses a custom XML-based system to create the Python wrapper for a C++ class. To wrap a C++ class for use in Python, two files must be manually created, and two files are automatically generated by the CMake build system (in addition to the C++ header and implementation files for the class).
You must create:
- YourClassPy.xml
- YourClassPyImp.cpp
Edit the appropriate CMakeLists.txt file to add references to these two files. From the XML file, the build system will then create:
- YourClassPy.cpp
- YourClassPy.h
Class Description XML File #
The XML file YourClassPy.xml provides information about the functions and attributes that the Python class implements, as well as the user documentation for those items that displays in the FreeCAD Python console.
For this example, we will look at the wrapper for the Base::Axis C++ class. The XML description file begins with:
<GenerateModel xmlns:xsi##"http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation##"generateMetaModel_Module.xsd">
<PythonExport
Father##"PyObjectBase"
Name##"AxisPy"
Twin##"Axis"
TwinPointer##"Axis"
Include##"Base/Axis.h"
FatherInclude##"Base/PyObjectBase.h"
Namespace##"Base"
Constructor##"true"
Delete##"true"
FatherNamespace##"Base">
<Documentation>
<Author Licence##"LGPL" Name##"Juergen Riegel" EMail##"FreeCAD@juergen-riegel.net" />
<UserDocu>User documentation here
<!-- Lengthy comments removed. See source file. -->
</UserDocu>
<DeveloperDocu>Developer documentation here</DeveloperDocu>
</Documentation>
Following this preamble, a list of methods and attributes is given. The format of a method is:
<Methode Name##"move">
<Documentation>
<UserDocu>
move(Vector)
Move the axis base along the vector
</UserDocu>
</Documentation>
</Methode>
The format of an attribute is:
<Attribute Name##"Direction" ReadOnly##"false">
<Documentation>
<UserDocu>Direction vector of the Axis</UserDocu>
</Documentation>
<Parameter Name##"Direction" Type##"Object" />
</Attribute>
For an attribute, if “ReadOnly” is false, you will provide both a getter and a setter function. If it is true, only a getter is allowed. In this case we will be required to provide two functions in the implementation C++ file:
Py::Object AxisPy::getDirection(void) const
and:
void AxisPy::setDirection(Py::Object arg)
Implementation C++ File #
The implementation C++ file YourClassPyImp.cpp
provides the “glue” that connects the C++ and Python structures together, effectively translating from one language to the other. The FreeCAD C++-to-Python system provides a number of C++ classes that map to their corresponding Python type. The most fundamental of these is the incode Py::Object
class - rarely created directly, this class provides the base of the inheritance tree, and is used as the return type for any function that is returning Python data.
Include Files #
Your C++ implementation file will include the following files:
#include "PreCompiled.h"
#include "YourClass.h"
// Inclusion of the generated files (generated out of [YourClassPy.xml)
#include "YourClassPy.h"
#include "YourClassPy.cpp"
Of course, you may include whatever other C++ headers your code requires to function as well.
Constructor #
Your C++ implementation must contain the definition of the PyInit function: for example, for the Axis class wrapper, this is
int AxisPy::PyInit(PyObject* args, PyObject* /*kwd*/)
//Within this function you will most likely need to parse incoming arguments to the constructor: the most important function for this purpose is the Python-provided incode|PyArg_ParseTuple. It takes in the
//passed argument list, a descriptor for the expected arguments that it should parse, and type information and storage locations for the parsed results. For example:
PyObject* d;
if (PyArg_ParseTuple(args, "O!O", &(Base::VectorPy::Type), &o,
&(Base::VectorPy::Type), &d)) {
// NOTE: The first parameter defines the base (origin) and the second the direction.
*getAxisPtr() ## Base::Axis(static_cast<Base::VectorPy*>(o)->value(),
static_cast<Base::VectorPy*>(d)->value());
return 0;
}
For a complete list of format specifiers see [https://docs.python.org/3/c-api/arg.html Python C API documentation]. Note that several related functions are also defined which allow the use of keywords, etc. The complete set is:
- PyArg_Parse (PyObject *, const char *, …);
- PyArg_ParseTuple (PyObject *, const char *, …);
- PyArg_ParseTupleAndKeywords (PyObject *, PyObject *, const char *, char **, …);
- PyArg_VaParse (PyObject *, const char *, va_list);
- PyArg_VaParseTupleAndKeywords (PyObject *, PyObject *, const char *, char **, va_list);
Another Explanation #
The basic structure of a program to expose functionality to Python is something like this:
- get the Py object parameters and convert them to c++ variables using PyArg_ParseTuple(),
- use various c++ routines from OpenCascade and/or FreeCAD to produce the desired result,
- convert the c++ result into Py object using routines like PyLong_AsLong(), Py::asObject(), etc,
- return the Py object.
There are two source files required to implement a new Python binding. Assuming we wanted to expose some methods from https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/Part/TopoShape.cpp, we would need to make:
- TopoShapePy.xml - definitions of the functions for be exposed in XML format. This file is used to generate header files for our next file…
- TopoShapePyImp.cpp - the actual C++ code that bridges from Python to C++.
These 2 files need to be added to ../Mod/yourModule/App/CMakeLists.txt. See https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/Part/App/AppPartPy.cpp and FreeCAD/src/Mod/Part/App/CMakeLists.txt for examples.
You can extend the Python version of your module by adding to ../Mod/yourModule/App/AppmyModulePy.cpp (see https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/Part/App/AppPartPy.cpp and FreeCAD/src/Mod/Part/AppPartPy.cpp]). The additions are accessed in Python by “import myModule”.
Note:
There is a convention for return values from our C++/Python connections:
- xxxxxPyImp routines return a PyObject* pointer (see TopoShapePyImp.cpp)
- AppmyModulePy.cpp routines return a Py::Object (see https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/Part/App/AppPartPy.cpp and FreeCAD/src/Mod/Part/AppPartPy.cpp.
See also #
- [https://forum.freecadweb.org/viewtopic.php?p##314796#p314617]
- [https://forum.freecadweb.org/viewtopic.php?p##560639#p560639] Forum discussion: Adding Commands in Python to C++ Workbench
- [https://forum.freecadweb.org/viewtopic.php?f##10&t##70750] Another forum thread
- (./Workbench creation.md)
- [https://github.com/FreeCAD/FreeCAD/commit/20b86e55b8dd1873f4c19e036d047528c9ff7f4e] Commit 20b86e5, exposing OCC’s precision methods to Python