Today we'll take a look at how you can create bindings for your own project.
The Qt Company is pleased to announce that Qt for Python will also include Shiboken, your primary binding tool.
Read the material below and you will get an idea of how to create Python bindings for a simple C++ library.
Hopefully this encourages you to do the same with your own custom libraries.
As with any Qt project, we are happy to provide articles on Shiboken, thereby improving its understanding for everyone.
Sample library
The main purposes of this post will be our use of a slightly nonsensical custom library called Universe. It provides two classes: Icecream (ice cream) and Truck (truck).
Icecreams is characterized by taste and the Truck serves as a delivery vehicle for Icecream to children in the neighborhood. Pretty simple.
We would like to use these classes inside Python. The use case is to add additional ice cream flavors or to check if the ice cream delivery was successful. In simple terms, we want to provide Python bindings for Icecream and Truck so that we can use them in our own Python script.
We'll skip some parts for brevity, but you can check out the repository for the full source code. Link to the repository pyside-setup/examples/samplebinding .
C++ library
First, let's take a look at the Icecream header file
class Icecream { public: Icecream(const std::string &flavor); virtual Icecream *clone(); virtual ~Icecream(); virtual const std::string getFlavor(); private: std::string m_flavor; };
and header file Truck
class Truck { public: Truck(bool leaveOnDestruction = false); Truck(const Truck &other); Truck& operator=(const Truck &other); ~Truck(); void addIcecreamFlavor(Icecream *icecream); void printAvailableFlavors() const; bool deliver() const; void arrive() const; void leave() const; void setLeaveOnDestruction(bool value); void setArrivalMessage(const std::string &message); private: void clearFlavors(); bool m_leaveOnDestruction = false; std::string m_arrivalMessage = "A new icecream truck has arrived!\n"; std::vector m_flavors; };
Most APIs should be fairly easy to understand, but we'll summarize the important parts:
- Icecream is a polymorphic type and is meant to be overridden
- getFlavor() will return the flavor depending on the actual derived type
- Truck saves a vector of Icecream objects it owns, which can be added via addIcecreamFlavor()
- Truck arrival message can be configured with setArrivalMessage()
- deliver() will tell us if the "ice cream" delivery was successful or not
Shiboken type system
To inform shiboken about the API, we need bindings, for this we make a header file that includes the types we are interested in:
#ifndef BINDINGS_H #define BINDINGS_H #include "icecream.h" #include "truck.h" #endif // BINDINGS_H
In addition, shiboken also requires an XML typesystem file that defines the relationship between C++ and Python types:
<?xml version="1.0"?> <typesystem package="Universe"> <primitive-type name="bool"/> <primitive-type name="std::string"/> <object-type name="Icecream"> <modify-function signature="clone()"> <modify-argument index="0"> <define-ownership owner="c++"/> </modify-argument> </modify-function> </object-type> <value-type name="Truck"> <modify-function signature="addIcecreamFlavor(Icecream*)"> <modify-argument index="1"> <define-ownership owner="c++"/> </modify-argument> </modify-function> </value-type> </typesystem>
The first important thing to notice is that we declare "bool" and "std::string" as primitive types. Some of the C++ methods use them as parameter/return types and so the shiboken needs to know about them. It can then generate the appropriate conversion code between C++ and Python.
Most C++ primitive types are handled by shiboken without requiring any additional code. We then declare the above two classes. One of them as "object-type" (object-type), and the other as "value-type" (value-object).
The main difference is that "object-type" are passed in the generated code as pointers, while "value-type" are copied (value semantics).
By setting the class names in the type system file, shiboken will automatically attempt to create bindings for all methods declared in the classes, so there is no need to manually specify all method names...
If you don't want to change the feature in any way, then that brings us to the next topic, Ownership Rules.
Shiboken cannot magically find out who is responsible for freeing C++ objects placed in Python code.
There can be many cases: Python must deallocate C++ memory when the number of references to a Python object becomes zero, or Python must never delete a C++ object, assuming it will be deleted at some point inside the C++ library. Or perhaps it's the parent of another object (like QWidgets, for example). In our case, the clone() method is only called inside the C++ library, and we assume that the C++ code takes care of freeing the memory of the cloned object.
As far as addIcecreamFlavor() is concerned, we know that the Truck resides in the Icecream object and will be removed immediately after the Truck is destroyed. So again, ownership is set to “c++.”
If we didn't specify ownership rules, then C++ objects would be deleted when the corresponding Python names went out of scope.
Assembly
To build a custom Universe library and then generate bindings for it, we provide a well-documented, mostly generic CMakeLists.txt file that you can reuse for your own libraries.
It basically boils down to calling "cmake." to set up the project and then building with the tool chain of your choice (we recommend the "(N) Makefiles" generator).
As a result of creating a project, you get two shared libraries: libuniverse. (so/dylib/dll) and Universe. (so/pyd).
The first is a custom C++ library and the last is a Python module that can be imported from a Python script (document, test driver).
Of course, there are also intermediate files created by shiboken (.h/.cpp files created to create Python bindings). Don't worry about them if you don't need to troubleshoot or if for some reason it fails to compile or doesn't behave as it should. Then you can submit a bug report to Qt Company!
And finally, we get to the Python part.
Using the Python module
In the next little script, we will use the Universe module, inherit from Icecream, implement virtual methods, instantiate objects, and much more.
from Universe import Icecream, Truck class VanillaChocolateIcecream(Icecream): def __init__(self, flavor=""): super(VanillaChocolateIcecream, self).__init__(flavor) def clone(self): return VanillaChocolateIcecream(self.getFlavor()) def getFlavor(self): return "vanilla sprinked with chocolate" class VanillaChocolateCherryIcecream(VanillaChocolateIcecream): def __init__(self, flavor=""): super(VanillaChocolateIcecream, self).__init__(flavor) def clone(self): return VanillaChocolateCherryIcecream(self.getFlavor()) def getFlavor(self): base_flavor = super(VanillaChocolateCherryIcecream, self).getFlavor() return base_flavor + " and a cherry" if __name__ == '__main__': leave_on_destruction = True truck = Truck(leave_on_destruction) flavors = ["vanilla", "chocolate", "strawberry"] for f in flavors: icecream = Icecream(f) truck.addIcecreamFlavor(icecream) truck.addIcecreamFlavor(VanillaChocolateIcecream()) truck.addIcecreamFlavor(VanillaChocolateCherryIcecream()) truck.arrive() truck.printAvailableFlavors() result = truck.deliver() if result: print("All the kids got some icecream!") else: print("Aww, someone didn't get the flavor they wanted...") if not result: special_truck = Truck(truck) del truck print("") special_truck.setArrivalMessage("A new SPECIAL icecream truck has arrived!\n") special_truck.arrive() special_truck.addIcecreamFlavor(Icecream("SPECIAL *magical* icecream")) special_truck.printAvailableFlavors() special_truck.deliver() print("Now everyone got the flavor they wanted!") special_truck.leave()
After importing the classes from our module, we create two derived (secondary) Icecream types that have been customized with flavors.
Then we create a truck (truck), add to it some regular variants (varieties) of Icecreams (ice cream) and two special ones.
We try to send (deliver) Icecream (ice cream).
If the delivery fails, we create a new truck with the old options copied over and a new magic option that will surely satisfy all customers.
The above script briefly shows the use of C++ type inference, overriding virtual methods, creating and destroying objects, etc.