Have you ever wanted to be able to call a C++ Qt library from Python? It turns out it’s pretty easy to do. This tutorial explains how to do it in Mac OS X, but it’s slightly out of date. I’d like to provide the world with a way to make it work with Ubuntu 14.04 out of the box. My instructions are based on that tutorial and the PySide Binding Generation Tutorial. Let’s check it out. Note: This mechanism seems to work for wrapping Qt 4 libraries. I don’t think PySide works with Qt 5, at least in Ubuntu 14.04. Keep that in mind. Everything we do will be with Qt 4.
We’re going to create a library called libdougtest. It’s going to contain a QObject subclass called TestClass. We will create a Python wrapper for it called libDougTestPy, which we will eventually be able to refer to as DougTestPy to follow Python’s library naming conventions. Let’s get started.
Install some prerequisites
There are many things you need installed:
sudo apt-get install build-essential qt-sdk python-pyside* libshiboken-dev shiboken pyside-tools libpyside-dev
I got a warning saying I should install a Phonon backend when I did this, so I also installed the package phonon-backend-gstreamer just in case.
Create a project directory
We’re going to start out by creating a directory for this entire project. Let’s call it dougtest_project:
mkdir dougtest_project cd dougtest_project
All work we do will be inside of this directory. The reason I’m naming it dougtest_project instead of just dougtest is because I’m going to create another directory inside of it called dougtest for the C++ library itself.
Create libdougtest
Now, let’s create the good old C++ library that we want to wrap. This is pretty easy, since you just follow the standard Qt way of creating a library. For simplicity, I’ve provided a barebones library containing a single class. We are going to create a directory inside of dougtest_project called dougtest which will be the home for this library:
mkdir dougtest
First of all, create the project file:
dougtest/dougtest.pro:
QT += core TARGET = dougtest TEMPLATE = lib HEADERS += testclass.h SOURCES += testclass.cpp
Now, create the header and source files:
dougtest/testclass.h:
#ifndef TESTCLASS_H #define TESTCLASS_H #include <QObject> class TestClass : public QObject { Q_OBJECT public: TestClass(QObject *parent = 0); virtual ~TestClass(); void doSomething(); private: int value; }; #endif
dougtest/testclass.cpp:
#include "testclass.h" TestClass::TestClass(QObject *parent) : QObject(parent), value(0) { } TestClass::~TestClass() { } void TestClass::doSomething() { value++; qDebug("Testing: %d", value); }
This concludes the creation of the library. Let’s make sure it compiles:
cd dougtest qmake-qt4 make
If you’re successful, you should see libdougtest.so.1.0.0 with several symlinks. Change back to the main directory:
cd ..
OK, we have successfully created the library that we’re going to wrap. We haven’t seen anything special yet; the fun is coming now.
Create files for shiboken
Next, we’re going to create some files that shiboken will use during the wrapping process. shiboken is a binding generator for creating Python bindings for C++ libraries, which is exactly what we want.
mkdir data
Create the following two files:
data/global.h:
#undef QT_NO_STL #undef QT_NO_STL_WCHAR #ifndef NULL #define NULL 0 #endif #include <QtCore/QtCore> #include <pyside_global.h> #include <testclass.h>
data/typesystem.xml:
<?xml version="1.0"?> <typesystem package="DougTestPy"> <load-typesystem name="typesystem_core.xml" generate="no"/> <object-type name="TestClass"/> </typesystem>
These files specify the classes to wrap, and will be passed as parameters to shiboken, as you will see shortly.
Create wrapped library project file
Next, we need to create a Qt project that will generate the wrapper library. Create the directory DougTestPy (inside of the main dougtest_project directory):
mkdir DougTestPy
Create the file DougTestPy/DougTestPy.pro:
TEMPLATE = lib QT += core # Need to have access to header files from the library being wrapped. INCLUDEPATH += ../dougtest # PySide include paths QMAKE_CXXFLAGS += $$system(pkg-config --cflags pyside) INCLUDEPATH += $$system(pkg-config --variable=includedir pyside)/QtCore # PySide dependencies LIBS += $$system(pkg-config --libs pyside) # Link against the library being wrapped LIBS += -L../bin/ -ldougtest # Generate the wrapper library in the bin directory TARGET = ../bin/DougTestPy # Here are all the sources that go with it SOURCES += \ DougTestPy/dougtestpy_module_wrapper.cpp \ DougTestPy/testclass_wrapper.cpp
This script may be confusing because it refers to source files that we haven’t created yet. This is the naming scheme that shiboken will use when we use it to create the wrapper code.
Create the build script and run it
We are finally ready to create a shell script that will call shiboken and do everything that needs to be done. I hope this isn’t too complicated. It’s very possible that a Makefile would work too, but this is what I have working.
Create the file build.sh inside the main dougtest_project directory:
#!/bin/sh # Absolute path to this script, e.g. /home/user/bin/foo.sh SCRIPT=$(readlink -f "$0") # Absolute path this script is in, e.g. /home/user/bin SCRIPTPATH=$(dirname "$SCRIPT") WRAPPER_NAME="DougTestPy" WRAPPED_NAME="dougtest" QT_INC=$(pkg-config --variable=includedir QtCore)/.. QTCORE_INC=$(pkg-config --variable=includedir QtCore) PYSIDE_INC=$(pkg-config --variable=includedir pyside) QTTYPESYSTEM=$(pkg-config --variable=typesystemdir pyside) # I'm assuming the wrapped library is built in-place; if not, # change WRAPPED_BIN_DIR to the location of the final binaries. WRAPPED_SRC_DIR="${SCRIPTPATH}/${WRAPPED_NAME}" WRAPPED_BIN_DIR="${SCRIPTPATH}/${WRAPPED_NAME}" cd $SCRIPTPATH # Clean up the old build to ensure we start fresh. If we don't do this, # old stale stuff can be left behind, which can cause crashes in the # library after you make changes. rm -rf ./bin ./${WRAPPER_NAME}/Makefile ./${WRAPPER_NAME}/${WRAPPER_NAME} # Copy the latest C++ library build into the bin directory. mkdir -p bin cp -R ${WRAPPED_BIN_DIR}/lib${WRAPPED_NAME}* ./bin/ # Now work inside the wrapper directory cd $WRAPPER_NAME # Create the wrappers for the library shiboken ../data/global.h \ --include-paths=$WRAPPED_SRC_DIR:$QT_INC:$QTCORE_INC:$PYSIDE_INC \ --typesystem-paths=../data:$QTTYPESYSTEM \ --output-directory=. \ --avoid-protected-hack \ ../data/typesystem.xml if [ $? -ne 0 ]; then echo "shiboken returned error. Something is shibroken :-(" exit 1 fi # Build the python wrapper library qmake-qt4 if [ $? -ne 0 ]; then echo "qmake-qt4 returned error." exit 1 fi make if [ $? -ne 0 ]; then echo "make returned error." exit 1 fi # Back into the directory containing the script cd .. # Make meaningful symlink so Python can use the generated library rm -rf ./bin/${WRAPPER_NAME}.so ln -s lib${WRAPPER_NAME}.so ./bin/${WRAPPER_NAME}.so
OK, you’re ready to run this build script:
sh build.sh
Assuming everything succeeds, you should end up with a bin directory with the following contents:
- DougTestPy.so (a symlink to libDougTestPy.so)
- libDougTestPy.so, libDougTestPy.so.1, libDougTestPy.so.1.0, and libDougTestPy.so.1.0.0
- libdougtest.so, libdougtest.so.1, libdougtest.so.1.0, and libdougtest.so.1.0.0
The symlink has to be created because the build process generates a library that begins with “lib”, but Python expects the library to not be prefixed with “lib”. Maybe there is a way to fix it so that qmake doesn’t add the “lib” prefix, but I couldn’t figure out how after a quick search, and this did the trick, so I let it go.
Test the library
We’re ready to test it out. Create the file test.py in the main dougtest_project directory:
import sys sys.path.append("bin") from PySide.QtCore import * from DougTestPy import * # Create a TestClass instance tc = TestClass() print "Created test class" # Test the TestClass tc.doSomething() print "Called test class function" tc.doSomething() print "Called test class function again"
Now, run it. Note that I had to add the “bin” directory to Python’s path in order to use the library. We’re not done with library trickery yet though: libDougTestPy.so depends on libdougtest.so. So libdougtest.so needs to be in the system library path. The easiest way to do this temporarily is to add the bin directory to LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=bin/
For a more permanent solution, you could put libdougtest.so* into /usr/local/lib and run “sudo ldconfig” to eliminate the need for that hack.
Now, run the test script:
python test.py
You should get this output:
Created test class Testing: 1 Called test class function Testing: 2 Called test class function again
Conclusion
I hope this was helpful. It’s really not that bad; it’s just that it requires a lot of manual setup. It would be nice if there was something that could automatically generate all the junk we had to make by hand. Maybe such a tool already exists and I’m wasting my time. Either way, I think this is interesting. Thanks again to the people who wrote the tutorials I linked above.
Note that if you use libraries other than QtCore, you will need to add them into the appropriate places in data/global.h, data/typesystem.xml, DougTestPy/DougTestPy.pro, and build.sh. Just see where the word “core” or “Core” appears, and add your extra libraries such as QtGui and QtXml in the same format.
Awesome! thanks!