Generate Python Bindings for C++ code using Shiboken

This will be a guide on how to generate Python bindings for your C++ library using Shiboken. Shiboken is a tool specifically created to build PySide, so it supports Qt code perfectly fine.

The steps described here require the in-progress merge request that adds the necessary code to Extra CMake Modules. I hope that it gets merged soon (I’ll update the post). I’ll use KUnitConversion as an example, because it’s a small library.

We’ll start adding the building instructions. This part is mostly boilerplate code as it’s the same for any library (except for the obvious thing of changing the library name):

 1set(bindings_library "KUnitConversion")
 2
 3set(wrapped_header ${CMAKE_SOURCE_DIR}/python/bindings.h)
 4set(typesystem_file ${CMAKE_SOURCE_DIR}/python/bindings.xml)
 5
 6set(generated_sources
 7    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_module_wrapper.cpp
 8    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_wrapper.cpp
 9    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_converter_wrapper.cpp
10    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_unit_wrapper.cpp
11    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_unitcategory_wrapper.cpp
12    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_updatejob_wrapper.cpp
13    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_value_wrapper.cpp)
14
15set(qt_libs Qt6::Core)
16
17ecm_generate_python_bindings(
18    PACKAGE_NAME ${bindings_library}
19    VERSION ${KF_VERSION}
20    WRAPPED_HEADER ${wrapped_header}
21    TYPESYSTEM ${typesystem_file}
22    GENERATED_SOURCES ${generated_sources}
23    INCLUDE_DIRS ${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_INCLUDEDIR_KF}/KUnitConversion
24    QT_LIBS ${qt_libs}
25    QT_VERSION ${REQUIRED_QT_VERSION}
26    HOMEPAGE_URL "https://invent.kde.org/frameworks/kunitconversion"
27    ISSUES_URL "https://bugs.kde.org/describecomponents.cgi?product=frameworks-kunitconversion"
28)
29
30target_link_libraries(${bindings_library} PRIVATE KF6UnitConversion)
31install(TARGETS ${bindings_library} LIBRARY DESTINATION "${KDE_INSTALL_LIBDIR}/python-kf6")

Let’s see what each part does.

 1set(bindings_library "KUnitConversion")
 2
 3set(wrapped_header ${CMAKE_SOURCE_DIR}/python/bindings.h)
 4set(typesystem_file ${CMAKE_SOURCE_DIR}/python/bindings.xml)
 5
 6set(generated_sources
 7    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_module_wrapper.cpp
 8    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_wrapper.cpp
 9    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_converter_wrapper.cpp
10    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_unit_wrapper.cpp
11    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_unitcategory_wrapper.cpp
12    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_updatejob_wrapper.cpp
13    ${CMAKE_CURRENT_BINARY_DIR}/KUnitConversion/kunitconversion_value_wrapper.cpp)
14
15set(qt_libs Qt6::Core)

The first line just defines the name of the Python library we’ll build later. Then we set the header file that includes all the necessary headers of the library. We’ll see that file later. The generated sources list is a bit more complicated, as you need to guess the names of the files generated by Shiboken. Fortunately, you can probably guess the pattern from the example above: the first two files are always the same (the name of the library + _module_wrapper or _wrapper) and the rest is the list of classes defined in your XML file (more about it later). The qt_libs variable just contains a list of the Qt modules that our library requires. You should list all of them, even if one depends on another, because otherwise Shiboken won’t be able to find the include directories.

17ecm_generate_python_bindings(
18    PACKAGE_NAME ${bindings_library}
19    VERSION ${KF_VERSION}
20    WRAPPED_HEADER ${wrapped_header}
21    TYPESYSTEM ${typesystem_file}
22    GENERATED_SOURCES ${generated_sources}
23    INCLUDE_DIRS ${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_INCLUDEDIR_KF}/KUnitConversion
24    QT_LIBS ${qt_libs}
25    QT_VERSION ${REQUIRED_QT_VERSION}
26    HOMEPAGE_URL "https://invent.kde.org/frameworks/kunitconversion"
27    ISSUES_URL "https://bugs.kde.org/describecomponents.cgi?product=frameworks-kunitconversion"
28)

This is the magic part. The ecm_generate_python_bindings function takes care of running Shiboken with all the required arguments, building the Python library and a wheel file to publish it on the Python Package Index (pypi.org). It has the following arguments:

30target_link_libraries(${bindings_library} PRIVATE KF6UnitConversion)
31install(TARGETS ${bindings_library} LIBRARY DESTINATION "${KDE_INSTALL_LIBDIR}/python-kf6")

The last part links the C++ library with the Python bindings and installs it. Now let’s take a look at the header file:

1#pragma once
2
3// Make "signals:", "slots:" visible as access specifiers
4#define QT_ANNOTATE_ACCESS_SPECIFIER(a) __attribute__((annotate(#a)))
5
6#include <KUnitConversion/Converter>
7#include <KUnitConversion/Unit>
8#include <KUnitConversion/UnitCategory>
9#include <KUnitConversion/Value>

Nothing exciting there, just the list of includes (and some Qt thing that I don’t understand).

The last file you need is the typesystem definition, where you tell Shiboken which things (classes, structs, enums, namespaces…) you want to include in your bindings and how it should interpret them. You can delete or rename functions, change the return type, modify the input parameters and many other things. You may want to take a look at the documentation because the list of posible options is very large.

 1<?xml version="1.0"?>
 2<typesystem package="KUnitConversion">
 3    <load-typesystem name="typesystem_core.xml" generate="no" />
 4
 5    <namespace-type name="KUnitConversion">
 6        <enum-type name="CategoryId" />
 7        <object-type name="Converter" />
 8        <object-type name="Unit" />
 9        <object-type name="UnitCategory" />
10        <enum-type name="UnitId" />
11        <object-type name="UpdateJob" />
12        <object-type name="Value" />
13    </namespace-type>
14</typesystem>

You need to load the typesystems of the Qt libraries that you are using so Shiboken can understand what your code is referring to. They come included with PySide.

That’s all you need to generate the Python bindings for your library. The last step is building the project as you usually do.