Sample Bindings Example#

This example showcases how to generate Python bindings for a non-Qt C++ library.

The example defines a CMake project that builds two libraries:

  • libuniverse - a sample library with two C++ classes.

  • Universe - the generated Python extension module that contains bindings to the library above.

The project file is structured in such a way that a user can copy-paste in into their own project, and be able to build it with a minimal amount of modifications.

Description#

The libuniverse library declares two classes: Icecream and Truck.

Icecream objects have a flavor, and an accessor for returning the flavor.

Truck instances store a vector of Icecream objects, and have various methods for adding new flavors, printing available flavors, delivering icecream, etc.

From a C++ perspective, Icecream instances are treated as object types (pointer semantics) because the class declares virtual methods.

In contrast Truck does not define virtual methods and is treated as a value type (copy semantics).

Because Truck is a value type and it stores a vector of Icecream pointers, the rule of five has to be taken into account (implement the copy constructor, assignment operator, move constructor, move assignment operator and destructor).

And due to Icecream objects being copyable, the type has to define an implementation of the clone() method, to avoid type slicing issues.

Both of these types and their methods will be exposed to Python by generating CPython code. The code is generated by shiboken and placed in separate .cpp files named after each C++ type. The code is then compiled and linked into a shared library. The shared library is a CPython extension module, which is loaded by the Python interpreter.

Beacuse the C++ language has different semantics to Python, shiboken needs help in figuring out how to generate the bindings code. This is done by specifying a special XML file called a typesystem file.

In the typesystem file you specify things like:

  • which C++ classes should have bindings (Icecream) and what kind of semantics (value / object)

  • Ownership rules (who deletes the C++ objects, C++ or Python)

  • Code injection (for various special cases that shiboken doesn’t know about)

  • Package name (name of package as imported from Python)

In this example we declare Icecream as an object type and Truck as a value type. The clone() and addIcecreamFlavor(Icecream*) need additional info about who owns the parameter objects when passing them across language boundaries (in this case C++ will delete the objects).

The Truck has getters and setters for the string arrivalMessage. In the type system file, we declare this to be a property in Python:

<property type="std::string" name="arrivalMessage" get="getArrivalMessage" set="setArrivalMessage"/>

It can then be used in a more pythonic way:

special_truck.arrivalMessage = "A new SPECIAL icecream truck has arrived!\n"

After shiboken generates the C++ code and CMake makes an extension module from the code, the types can be accessed in Python simply by importing them using the original C++ names.

from Universe import Icecream, Truck

Constructing C++ wrapped objects is the same as in Python

icecream = Icecream("vanilla")
truck = Truck()

And actual C++ constructors are mapped to the Python __init__ method.

class VanillaChocolateIcecream(Icecream):
    def __init__(self, flavor=""):
        super().__init__(flavor)

C++ methods can be accessed as regular Python methods using the C++ names

truck.addIcecreamFlavor(icecream)

Inheritance works as with regular Python classes, and virtual C++ methods can be overridden simply by definining a method with the same name as in the C++ class.

class VanillaChocolateIcecream(Icecream):
    # ...
    def getFlavor(self):
        return "vanilla sprinked with chocolate"

The main.py script demonstrates usages of these types.

The CMake project file contains many comments explaining all the build rules for those interested in the build process.

Building the project#

This example can only be built using CMake. The following requirements need to be met:

  • A PySide package is installed into the current active Python environment (system or virtualenv)

  • A new enough version of CMake (3.16+).

  • ninja

For Windows you will also need:

  • a Visual Studio environment to be active in your terminal

  • Correct visual studio architecture chosen (32 vs 64 bit)

  • Make sure that your Python intepreter and bindings project build configuration is the same (all Release, which is more likely, or all Debug).

The build uses the pyside_config.py file to configure the project using the current PySide/Shiboken installation.

Using CMake#

You can build and run this example by executing the following commands (slightly adapted to your file system layout) in a terminal:

macOS/Linux:

cd ~/pyside-setup/examples/samplebinding

On Windows:

cd C:\pyside-setup\examples\samplebinding
mkdir build
cd build
cmake -H.. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja
ninja install
cd ..

The final example can then be run by:

python main.py

Windows troubleshooting#

It is possible that CMake can pick up the wrong compiler for a different architecture, but it can be addressed explicitly by setting the CC environment variable:

set CC=cl

passing the compiler on the command line:

cmake -H.. -B. -DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe

or by using the -G option:

cmake -H.. -B. -G "Visual Studio 14 Win64"

If the -G "Visual Studio 14 Win64" option is used, a sln file will be generated, and can be used with MSBuild instead of ninja. The easiest way to both build and install in this case, is to use the cmake executable:

cmake --build . --target install --config Release

Note that using the "Ninja" generator is preferred to the MSBuild one, because the MSBuild one generates configs for both Debug and Release, and this might lead to building errors if you accidentally build the wrong config at least once.

Virtualenv Support#

If the python application is started from a terminal with an activated python virtual environment, that environment’s packages will be used for the python module import process. In this case, make sure that the bindings were built while the virtualenv was active, so that the build system picks up the correct python shared library and PySide6 / shiboken package.

Linux Shared Libraries Notes#

For this example’s purpose, we link against the absolute path of the dependent shared library libshiboken because the installation of the library is done via a wheel, and there is no clean solution to include symbolic links in a wheel package (so that passing -lshiboken to the linker would work).

Windows Notes#

The build config of the bindings (Debug or Release) should match the PySide build config, otherwise the application will not properly work.

In practice this means the only supported configurations are:

  1. release config build of the bindings + PySide setup.py without --debug flag + python.exe for the PySide build process + python39.dll for the linked in shared library.

  2. debug config build of the application + PySide setup.py with --debug flag + python_d.exe for the PySide build process + python39_d.dll for the linked in shared library.

This is necessary because all the shared libraries in question have to link to the same C++ runtime library (msvcrt.dll or msvcrtd.dll). To make the example as self-contained as possible, the shared libraries in use (pyside6.dll, shiboken6.dll) are hard-linked into the build folder of the application.

Download this example