Embedding the Python Interpreter in a Qt/C++ Application

on

A couple of years back, I was developing an application for Ubuntu. For various reasons, I wanted to develop the GUI in Qt/C++, but implement all logic in Python.

The default Python that came with Ubuntu at the time could not run my scripts, so I had to ship a compatible Python version along with my application. Basically, I had to embed the Python interpreter into my program, by linking to it, and then load it at runtime to run my Python scripts.

In this tutorial, I’ll show you how to embed the Python interpreter in your own Qt/C++ applications. We’ll be creating an application that minifies CSS stylesheet files. Only the GUI will be in Qt, while the CSS minification will be handled by Python using rCSSmin—a CSS minifier written in Python.

We will statically link the Python interpreter to our application and distribute the Python standard library in an archive, which will be loaded at runtime. That’s the only external file we’ll be shipping with our application, since the rCSSmin module will be stored in our program’s executable using Qt’s resource system. Pretty cool stuff, right?

This tutorial was written for Ubuntu 23.04, nevertheless, much of the information should be useful for other Linux distributions, Mac, or Windows.

Prepare the Development Environment

Install the development tools (typically come pre-installed).

sudo apt install build-essential

Install Qt Creator and the Qt SDK.

sudo apt install qtcreator qt6-base-dev

Install the Python static library and other development files.

sudo apt install python3-dev

Pack Python’s Standard Library

rCSSmin is a stand-alone application, however, even the simplest Python modules depend on Python’s standard library to run. Since Python’s standard library is very extensive, it’s not very efficient to pack and distribute the whole library with our Qt application.

For that reason, we’ll create a script that determines rCSSmin dependencies, compiles them, and conveniently packs them in a zip archive. To further save space, we’ll compile rCSSmin to bytecode and store it in a binary file.

Create a file named compile.py with the following contents:

#!/usr/bin/env python3

import os
import sys
from modulefinder import ModuleFinder
import zipfile
import py_compile
import marshal
from random import randint

RUN_DIR = os.path.dirname(os.path.realpath(__file__))
STD_LIB = os.path.join(sys.exec_prefix, 'lib', 'python%d.%d' % (sys.version_info.major, sys.version_info.minor))
STD_LIB_OUT = os.path.join(RUN_DIR, 'libpy%d%d.zip' % (sys.version_info.major, sys.version_info.minor))
DEP_OUT = os.path.join(RUN_DIR, 'dep.txt')
TMP_FILE = os.path.join(RUN_DIR, '~tmp%d' % randint(0,1000000))

def create_pyc(src_file):
    if not os.path.isfile(src_file):
        raise Exception('File "%s" does not exist.' % src_file)

    if os.path.exists(TMP_FILE):
        raise Exception('Path "%s" exists.' % TMP_FILE)

    py_compile.compile(src_file, cfile=TMP_FILE, doraise=True)

    with open(TMP_FILE, 'rb') as f:
        pyc = f.read()

    if os.path.isfile(TMP_FILE):
        os.remove(TMP_FILE)

    return pyc

def create_code_obj(src_module, dst_file):
    """Compiles the source module to a Python code object and serializes
    using the marshal format"""

    if not os.path.isfile(src_module):
        raise Exception('File "%s" does not exist.' % src_module)

    with open(src_module, 'r') as f:
        code_str = f.read()

    print('Writing code object to "%s"...' % dst_file)

    with open(dst_file, 'wb') as f:
        marshal.dump(compile(code_str, '<string>', 'exec', optimize=2), f)

def zip_std_lib(src_module, dst_file):
    """Compiles the Python standard library modules used by the source module
    and outputs to zip file."""

    finder = ModuleFinder()
    finder.run_script(src_module)

    modules = set()

    print('Writing dependencies to "%s"...' % DEP_OUT)

    with open(DEP_OUT, 'w') as f:
        for name, mod in finder.modules.items():
            print('%s: ' % name, end='', file=f)
            print(mod.__file__, file=f)

            if mod.__file__ is None:
                continue

            path = os.path.realpath(mod.__file__)

            if not path.startswith(os.path.normpath(STD_LIB)):
                continue

            while(os.path.dirname(path) != os.path.normpath(STD_LIB)):
                path = os.path.dirname(path)

            if os.path.isfile(path):
                modules.add(path)
            elif os.path.isdir(path):
                for root, dirs, files in os.walk(path):
                    for i in files:
                        modules.add(os.path.join(root, i))

        print('-' * 50, file=f)
        print('### Modules NOT imported ###', file=f)
        print('\n'.join(finder.badmodules.keys()), file=f)

    modules = sorted([i for i in modules if i.endswith(('.py','.pyc')) and not os.path.dirname(i).endswith('__pycache__')])

    print('Writing standard library to "%s"...' % dst_file)

    with zipfile.ZipFile(dst_file, 'w', compression=zipfile.ZIP_DEFLATED) as z:
        for i in modules:
            root, ext = os.path.splitext(i)

            if ext == '.py':
                arcname = os.path.relpath(root, STD_LIB) + '.pyc'
                pyc = create_pyc(i)
            else:
                arcname = os.path.relpath(i, STD_LIB)
                with open(i, 'rb') as f:
                    pyc = f.read()

            z.writestr(arcname, pyc)

if __name__ == "__main__":
    if len(sys.argv) > 1:
        src_module = os.path.join(RUN_DIR, sys.argv[1])
    else:
        raise Exception('Source module not specified.')

    if not os.path.isfile(src_module):
        raise Exception('File "%s" does not exist.' % src_module)

    zip_std_lib(src_module, STD_LIB_OUT)
    create_code_obj(src_module, os.path.join(RUN_DIR, os.path.basename(src_module) + '.codeobj'))

If you haven’t done so already, download rCSSmin source code and uncompress it. To use the above script, execute:

chmod u+x compile.py
./compile.py PATH/TO/rcssmin.py

The script will output three files:

  1. libpy311.zip: A subset of the Python standard library in a zip archive (The suffix number might be different, depending on your Python version).
  2. rcssmin.py.codeobj: The rCSSmin code object serialized using Marshal.
  3. dep.txt: A list of rCSSmin dependencies as determined by the script.

Create a Qt Application

Run Qt Creator and create a new Qt Widgets Application. Name the project embedPython and choose qmake as the build system.

Create new Qt Project

If a kit is auto-detected, select it, and click on Next. If it’s not auto-detected and you get a message “No suitable kits found”, like in the screenshot below, click on options to locate qmake manually.

Kit Selection

Go to KitsQt VersionsAdd and select your qmake path. It should be /usr/bin/qmake6 in Ubuntu.

Select qmake path

Then switch to the Kits tab, scroll down, and select your Qt version as in the screenshot.

Select Qt version

Click OK and the kit should be available now.

Kit Selection

After your project is created, in your project’s root directory, create a new sub-directory named res and copy rcssmin.py.codeobj.

In Qt Creator, add a new Qt Resource File named embedPython.qrc. Open the resource file to add a new prefix / and the file rcssmin.py.codeobj under that prefix.

Modify embedPython.pro and add the following lines at the end:

QMAKE_CXXFLAGS += -no-pie
QMAKE_LFLAGS += -no-pie
INCLUDEPATH += /usr/include/python3.11
LIBS += -L/usr/lib/x86_64-linux-gnu
LIBS += -Wl,-Bstatic -lpython3.11 -Wl,-Bdynamic
LIBS += -lz -lexpat -ldl -lutil

Python 3.11 is the default version shipped with Ubuntu 23.04. If you are on a different platform, you should find out which Python version you have installed, and then modify embedPython.pro accordingly.

find /usr/include/ -maxdepth 1 -name python\*
/usr/include/python3.11

Notice that I had to add the -no-pie link option to statically link with the Python library in Ubuntu. If your project compiles successfully in your platform without lines #1 and #2, feel free to remove them.

Open mainwindow.ui in the form editor. Add two plainTextEdit widgets and a pushButton, as seen in the screenshot below.

Qt Form Editor

In your project, create a new C++ class named PyRun. We will use PyRun to initialize the Python interpreter, load the rCSSmin module using Qt’s resource system, and execute the cssmin function inside the rCSSmin module.

Modify pyrun.h.

#ifndef PYRUN_H
#define PYRUN_H

#include <Python.h>
#include <marshal.h>

#include <QString>

class PyRun
{
public:
    PyRun(QString);
    ~PyRun();
    QString cssmin(QString);

private:
    bool hasError();
    PyObject* importModule(const QByteArray&, const QString&);
    PyObject* callFunction(PyObject*, QString, PyObject*);
    QString ObjectToString(PyObject*);
};

#endif // PYRUN_H

Modify pyrun.cpp.

#include "pyrun.h"

#include <QString>
#include <QStringList>
#include <QDir>
#include <QFileInfo>
#include <QDebug>

PyRun::PyRun(QString execFile)
{
    QString pythonStdLib = "libpy311.zip";
    QString pythonStdLibPath = QDir::toNativeSeparators(QFileInfo(QFileInfo(execFile).absoluteDir(), pythonStdLib).canonicalFilePath());
    QString rcssminPath = "://res/rcssmin.py.codeobj";

    PyConfig config;
    PyConfig_InitPythonConfig(&config);

    // Path of our executable
    PyConfig_SetBytesString(&config, &config.program_name, execFile.toStdString().c_str());

    PyConfig_Read(&config);

    // Set module search path
    config.module_search_paths_set = 1;
    PyWideStringList_Append(&config.module_search_paths, pythonStdLibPath.toStdWString().c_str());

    Py_NoSiteFlag = 1;

    // Initialize the Python interpreter
    Py_InitializeFromConfig(&config);

    qDebug() << "Python interpreter version:" << QString(Py_GetVersion());
    qDebug() << "Python standard library path:" << QString::fromWCharArray(Py_GetPath());

    QFile f(rcssminPath);

    if(f.open(QIODevice::ReadOnly))
    {
        QByteArray codeObj = f.readAll();
        f.close();
        importModule(codeObj, "rcssmin");
    }
}

PyRun::~PyRun()
{
    Py_Finalize();
}

PyObject* PyRun::importModule(const QByteArray& codeObj, const QString& moduleName)
{
    PyObject *poModule = NULL;

    // Get reference to main module
    PyObject *mainModule = PyImport_AddModule("__main__");

    // De-serialize Python code object
    PyObject *poCodeObj = PyMarshal_ReadObjectFromString((char*)codeObj.data(), codeObj.size());

    if(!hasError())
    {
        // Load module from code object
        poModule = PyImport_ExecCodeModule(moduleName.toUtf8().data(), poCodeObj);

        if(!hasError())
        {
            // Add module to main module as moduleName
            PyModule_AddObject(mainModule, moduleName.toUtf8().data(), poModule);
        }

        // Release object reference (Python cannot track references automatically in C++!)
        Py_XDECREF(poCodeObj);
    }

    return poModule;
}

PyObject *PyRun::callFunction(PyObject *poModule, QString funcName, PyObject *poArgs)
{
    PyObject *poRet = NULL;

    // Get reference to function funcName in module poModule
    PyObject *poFunc = PyObject_GetAttrString(poModule, funcName.toUtf8().data());

    if(!hasError())
    {
        // Call function with arguments poArgs
        poRet = PyObject_CallObject(poFunc, poArgs);

        if(hasError())
        {
            poRet = NULL;
        }

        // Release reference to function
        Py_XDECREF(poFunc);
    }

    // Release reference to arguments
    Py_XDECREF(poArgs);

    return poRet;
}

QString PyRun::cssmin(QString css)
{
    QString res;

    // Get reference to rcssmin module
    PyObject *poModule = PyImport_AddModule("rcssmin");

    if(!hasError())
    {
        PyObject *poRet = callFunction(poModule, "cssmin", Py_BuildValue("(s)", css.toUtf8().data()));
        res = ObjectToString(poRet);
    }

    return res;
}

QString PyRun::ObjectToString(PyObject *poVal)
{
    QString val;

    if(poVal != NULL)
    {
        if(PyUnicode_Check(poVal))
        {
            // Convert Python Unicode object to UTF8 and return pointer to buffer
            const char *str = PyUnicode_AsUTF8(poVal);

            if(!hasError())
            {
                val = QString(str);
            }
        }

        // Release reference to object
        Py_XDECREF(poVal);
    }

    return val;
}

bool PyRun::hasError()
{
    bool error = false;

    if(PyErr_Occurred())
    {
        // Output error to stderr and clear error indicator
        PyErr_Print();
        error = true;
    }

    return error;
}

Our MainWindow class handles the pushButton click action, parses the text input with cssmin, and displays the result in the plainTextEdit widget.

Modify mainwindow.h.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include "pyrun.h"

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(PyRun *pyRun, QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();

private:
    Ui::MainWindow *ui;
    PyRun *pyRun;
};

#endif // MAINWINDOW_H

Modify mainwindow.cpp.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(PyRun *pyRun, QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    this->pyRun = pyRun;
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_pushButton_clicked()
{
    ui->plainTextEdit_2->setPlainText(pyRun->cssmin(ui->plainTextEdit->toPlainText()));
}

Note that you might have to modify the UI widget names to match the respective names in your form editor.

Finally, modify main.cpp.

#include "pyrun.h"
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    PyRun *pyRun = new PyRun(a.applicationFilePath());
    MainWindow w(pyRun);
    w.show();

    return a.exec();
}

Click on BuildBuild All Projects to build the application.

Running our Qt Application

Before running our Qt application for the first time, make sure that you copy libpy311.zip in the same directory as the embedPython executable. Then, click on BuildRun and the application should launch.

Copy-paste the CSS code below to the “Input” plainTextEdit widget.

h1, h2, h3, h4, h5, h6 {
  margin-top: .5rem;
}

.container {
  padding-top: 1rem;
  padding-bottom: 1rem;
}

pre {
  margin-top: 0 !important;
  margin-bottom: 1rem !important;
}

blockquote {
  font-style: italic;
}

a {
  color: inherit;
  outline: 0;
}

After clicking on Minify, you should see the minified text in the “Output” plainTextEdit.

embedPython

As mentioned earlier, the Python interpreter is statically linked to our application. To verify, we can use the ldd command, which prints a list of all shared libraries required by a program.

If the Python interpreter is indeed statically linked to our application, libpython3.11.so should not be in the ldd list:

ldd embedPython
...
linux-vdso.so.1 (0x00007ffeadcd9000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f75d487a000)
libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f75d484f000)
libQt6Widgets.so.6 => /lib/x86_64-linux-gnu/libQt6Widgets.so.6 (0x00007f75d4000000)
libQt6Gui.so.6 => /lib/x86_64-linux-gnu/libQt6Gui.so.6 (0x00007f75d3800000)
libQt6Core.so.6 => /lib/x86_64-linux-gnu/libQt6Core.so.6 (0x00007f75d3200000)
...

You can find all source code used in this tutorial in my GitHub repository.