aboutsummaryrefslogtreecommitdiffstats
path: root/examples/samplebinding/doc/samplebinding.rst
blob: e96e99df4c2434e0d6e09cfdac4b8813763dc8b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
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, Truck) and with 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:

.. code-block:: xml

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


It can then be used in a more pythonic way:

.. code-block:: python

    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.

.. code-block:: python

    from Universe import Icecream, Truck


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

.. code-block:: python

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


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

.. code-block:: python

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


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

.. code-block:: python

    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.

.. code-block:: python

    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:

.. code-block:: bash

    cd ~/pyside-setup/examples/samplebinding

On Windows:

.. code-block:: bash

    cd C:\pyside-setup\examples\samplebinding

.. code-block:: bash

    mkdir build
    cd build
    cmake -H.. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release
    ninja
    ninja install
    cd ..

Use the Python module
+++++++++++++++++++++

The final example can then be run by:

.. code-block:: bash

    python main.py

In the ``main.py`` script, two types are derived from :code:`Icecream` for
different “flavors” after importing the classes from the :code:`Universe`
module. Then, a :code:`truck` is created to deliver some regular flavored
Icecreams and two special ones.

If the delivery fails, a new :code:`truck` is created with the old flavors
copied over, and a new *magical* flavor that will surely satisfy all customers.

Try running it to see if the ice creams are delivered.

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:

.. code-block:: bash

    set CC=cl

passing the compiler on the command line:

.. code-block:: bash

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

or by using the -G option:

.. code-block:: bash

    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:

.. code-block:: bash

    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:

#. 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.

#. 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.