Fließende Kommunikation zwischen Python und C/C++ Python erweitern und einbetten
15.03.2022Von Rainer Grimm *
Es gibt viele gute Gründe, Python entweder um C/C++ zu erweitern oder in C/C++ einzubetten – grundsätzlich verstehen sich Python und C/C++ nämlich blendend. Am Anfang steht aber die Qual der Wahl: Welche Technik oder welches Framework sollte zum Einsatz kommen? Wann ist es ratsam, Python zu erweitern, wann sollte man es eher in C++ einbetten?
Firma zum Thema
Das mit Abstand am häufigsten verwendete Python (CPython) ist bereits in C implementiert und dürfte vielen Entwicklern bereits geläufig sein. Doch wann ist es nötig, das verwendete Python zu erweitern? Wann ist es eher ratsam, statt in C eine einbettung in C++ vorzunehmen?
Bevor dieser Artikel in die Untiefen der Kommunikation zwischen Python und C/C++ abtaucht, gilt es zuerst, die elementare Frage zu klären: Was bedeutet es, Python um C/C++ zu erweitern beziehungsweise Python in C/C++ einzubetten? Vereinfacht gesagt verhalten sich die beiden Techniken spiegelbildlich zueinander.
Das Erweitern geschieht in den folgenden drei Schritten:
- Zunächst muss man die Werte von Python nach C/C++ konvertieren.
- Anschließend kann man die konvertierten Werte verwenden, um die C/C++-Funktionalität auszuführen.
- Zuletzt lassen sich die Ergebnisse von C/C++ nach Python konvertieren.
Das Einbetten hingegen funktioniert genau umgekehrt: Zuerst werden die Werte von C/C++ nach Python konvertiert, und am Ende steht das Konvertieren der Ergebnisse von C/C++ nach Python. Das Erweitern und Einbetten von Python ist kein Selbstzweck, sondern bietet eine Reihe von Vorteilen. Der Einfachheit halber beschränkt sich folgende Aufzählung auf drei Vorteile.
- Don’t repeat yourself (DRY).
- Optimierung des performanzkritischen Teils der Applikation.
Um den Umfang dieses Artikels nicht zu sprengen, werden sich alle Beispiele in diesem Dokument auf Linux beziehen. Die entsprechenden Schritte lassen sich aber auch auf Windows entsprechend durchführen.
Direkte Kommunikation aus Python mit Shared Libraries dank ctypes
Mit dem Modul ctypes lässt sich eine Shared Librarys oder DLL direkt und komfortabel aufrufen. ctypes wird in der Python-Dokumentation als eine Bibliothek für fremde Funktionen beschrieben. Sie stellt mit C kompatible Datentypen zur Verfügung und ermöglicht den Aufruf von Funktionen in den DLLs oder Shared Libraries. Außerdem dient sie dazu, diese Bibliotheken in reinen Python-Code zu verpacken.
Bild 1 zeigt, wie die Shared Library libhelloWorld.so sich direkt laden und die Funktion “helloWorld.helloWorld()” ausführen lässt.
Python direkt erweitern
Für das direkte Erweitern der Python-API und den unmittelbaren Zugriff auf die Python-Laufzeit bietet Python Funktionen, Makros und Variablen an. Die Python-API wird durch die Headerdatei Python.h angeboten und lässt sich nur unter CPython einsetzen. Eigene Erweiterungsmodule in C/C++ für Python zu schreiben ist eine durchaus anspruchsvolle Aufgabe – daher wird das folgende Kapitel in pragmatischer Manier die bekannte Shared Library helloWorld als Erweiterungsmodul implementieren, und folgende Listing stellt das neue Erweiterungsmodul vor.
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject* method_helloWorld(PyObject*, PyObject*);
static PyMethodDef HelloWorld[] = {
{"helloWorld", method_helloWorld, METH_VARARGS,
"Hello world program"},
NULL, NULL, 0, NULL}};
static struct PyModuleDef helloWorldModule = {
PyModuleDef_HEAD_INIT,
"helloWorld",
"Hello World message",
-1,
HelloWorld
};
static PyObject* method_helloWorld(PyObject* self, PyObject* args) {
printf("Hello Worldn");
Py_INCREF(Py_None);
return Py_None;
}
PyMODINIT_FUNC PyInit_helloWorld(void) {
return PyModule_Create(&helloWorldModule);
}
Zum Verständnis des Listings sind einige Erklärungen hilfreich:
- Python.h erlaubt den Zugriff auf die Python-API, sie muss die erste Headerdatei sein.
- static PyMethodDef HelloWorld[ ] definiert die Methodentabelle. Dabei besteht ein Eintrag{“helloWorld”, method_helloWorld, METH_VARARGS, “Hello”} aus dem Namen der Python-Methode helloWorld, dem Namen der korrespondierenden C-Funktion method_helloWorld, der Aufrufkonvention der C-Funktion METH_VARARGS und dem Dokumentationsstring der Python-Methode. Eine Methodentabelle kann mehr als einen Eintrag besitzen, der letzte Eintrag {NULL, NULL, 0, NULL} schließt die Tabelle ab.
- static struct PyModuleDef helloWorldModule steht für die Definition des Moduls. Das Modul benötigt einen Namen helloWorld sowie den Dokumentationsstring des Moduls Hello World message, und es referenziert die bereits definierte Methodentabelle HelloWorld.
- PyMODINIT_FUNC sorgt für die Initialisierung des Moduls.
Nun gilt es noch ein Paket zu bauen, das das Modul enthält. Mit dem Python-Modul disutils lässt sich diese Aufgabe für Linux problemlos umsetzen. Für das Erzeugen eines Pakets ist eine setup.py-Datei notwendig. Die Datei setup.py im folgenden Listing stellt eine minimale Konfigurationsdatei für das Paket helloWorld dar.
from distutils.core import setup, Extension
def main():
setup(name="helloWorld",
ext_modules=[Extension("helloWorld",
["helloWorldModule.c"])])
if __name__ == "__main__":
main()
Nachdem die Namen des Moduls disutils bekannt sind, lässt sich das Paket beschreiben. name steht für den Namen des Pakets. Neben dem Namen des Pakets ist auch der Inhalt des Moduls genau zu spezifizieren. Das erledigt das Attribut ext_modules. Das Paket besteht aus dem gleichnamigen Modul helloWorld, das wiederum aus der C-Datei helloWorldModule.c besteht. Ein Paket kann mehrere Module enthalten.
Mittels Aufruf python setup.py build_ext –inplace lässt sich das Paket bauen.
Bild 2 zeigt die lange Kommandozeile, die notwendig ist, um das Paket auf Linux zu erstellen.
Mit dem Flag –inplace wird in dem aktuellen Verzeichnis eine Shared Library erzeugt, die sich direkt verwenden lässt. Bild 3 stellt die Verwendung des Shared Library auf Linux vor.
Interfaces zu C/C++ mit SWIG erzeugen
Der Simplified Wrapper and Interface Generator (SWIG) erzeugt Interfaces, sodass C beziehungsweise C++ mit anderen Programmiersprachen kommunizieren können. SWIG spricht mehrere Sprachen wie beispielsweise C#, D, Java, JavaScript, Perl, Python, PHP oder Ruby. SWIG dient als Wrapper zwischen Python und C beziehungsweise C++.
Der leichteste Einstieg in SWIG ist es, ein Erweiterungsmodul zu implementieren, was hier mit einem einfachen helloWorld-Beispiel demonstriert wird. Das Erzeugen eines Erweiterungsmoduls mit SWIG beginnt mit einer einfachen SWIG-Interfacedatei, die als hello.i im folgenden Listing dargestellt ist.
%module helloWorld
%{
#define SWIG_FILE_WITH_INIT
#include "helloWorld.h"
%}
Die Datei hello.i enthält den Namen des Moduls helloWorld und spezifiziert mit SWIG_FILE_WITH_INIT, dass die zu erzeugende Erweiterung eine Python-Erweiterung ist. Letztere fügt den Initialisierungscode für das Modul hinzu. Außerdem umfasst die Datei die Headerdatei der C-Quellen helloWorld.h und eine Deklaration der C-Funktion namens extern void helloWorld(). Das Kommando swig erzeugt mithilfe der Interfacedatei die zwei Dateien helloWorld_wrap.c und helloWorld.py. Bild 4 zeigt den dabei ablaufenden Build-Prozess.
Während die Datei helloWorld_wrap.c der Low-Level-Wrapper ist, der mit der restlichen Applikation gelinkt wird, enthält die Python-Datei helloWorld.py den High-Level-Code, der sich aus Python importieren lässt. Das Modul benötigt nicht nur die dafür erforderlichen C- und Python-Wrapper, die durch swig erzeugt wurden, sondern auch die eigentliche C-Funktionalität.
Das nächste Listing zeigt die Headerdatei helloWorld.h zu der korrespondierenden Quellcodedatei helloWorld.c:
#include <stdio.h>
void helloWorld();
Die Quellcodedatei ist bewusst einfach gehalten.
#include "helloWorld.h"
void helloWorld() {
printf("Hello Worldn");
}
Jetzt gilt es nur noch, das Erweiterungsmodul helloWorld zu bauen und zu verwenden. Mit dem Python-Modul disutils geht das leicht von der Hand.
C++11 und Python mit pybind11 nahtlos verbinden
Der Name ist Programm, denn pybind11 unterstützt eine nahtlose Zusammenarbeit zwischen C++11 und Python. Nahtlos heißt in dem konkreten Fall, dass sich sowohl C als auch C++ um Python erweitern und andererseits C/C++ in Python einbinden lassen.
pybind11 ist vollständig in die Headerdateien implementiert. Damit steht die gesamte Funktionalität durch das Inkludieren von Headerdateien direkt zur Verfügung.Die Vielseitigkeit von pybind11 ist beeindruckend. So lassen sich Lambda-Ausdrücke verwenden, Funktionen können ihre Argumente per Value, Referenz oder Zeiger erhalten und lassen sich überladen. Klassen können Methoden und Attribute besitzen, lassen sich mehrfach ableiten und erlauben Virtualität. Mit der Standard Template Library (STL) und weiteren Bibliotheken von C++11 lässt pybind praktisch keine Wünsche offen.
Seminar-Tipp: C++11 und C++14
Zum Seminar: C++11 und C++14
Die Funktionalität von pybind11 lässt sich am einfachsten an einem Beispiel aufzeigen. Das folgende Listing stellt eine C++-Funktion vor, für die pybind11 eine Python-Bindung erzeugt:
#include <pybind11/pybind11.h>
int add(int i, int j) {
return i + j;
}
PYBIND11_MODULE(function, m) {
m.def("add", &add, "A simple add function");
}
Die C++/Python-Bindung besteht aus den folgenden Komponenten
- #include <pybind11/pybind11.h> erzeugt die C++11/Python-Bindung
- PYBIND11_MODULE wird durch die import-Anweisung aufgerufen
- function ist der Name des Erweiterungmoduls, durch das sich das Modul in Python importieren lässt
- m ist eine Variable vom Typ py::module_, die als Interface für die Bindung verwendet wird
- m.def macht Python mit der Funktion bekannt und bindet sich an das Erweiterungsmodul function. Dabei ist das erste Argument der Name der Python-Funktion, das zweite die Adresse der C-Funktion und das letzte Argument der Dokumentationsstring der Python-Funktion.
Das Bauen des Programms unter Linux ist mit python3-config fast schon ein Kinderspiel. Zunächst gilt es, die Headerdateien zu ermitteln python3 -m pybind11 -–includes , dann sind mit folgendem Befehl die Suffixe zu bestimmen: python3-config –extension-suffix . Daraus resultieren Kommandozeilenbefehle für das Bauen des Erweiterungsmoduls, und das Ergebnis sollte wie folgt aussehen:
c++ -O3 –Wall –std=c++11 -shared -fPIC $(python3 -m pybind11 –includes) function.cpp -o function$(python3-config –extension-suffix).
Mit den vorgestellten Kommandozeilenbefehlen lässt sich das Modul function erstellen, importieren und verwenden (siehe Bild 5).
Python in C/C++ einbetten
Die Anforderung, Python in C oder C++ einzubetten sind dieselben, die es bei der Erweiterung von Python um C/C++ zu meistern galt. Lediglich die Reihenfolge ist beim Einbetten auf den Kopf gestellt. Zunächst sind die Werte von C/C++ nach Python zu konvertieren. Anschließend gilt es, die konvertierten Werte zum Ausführen der Python-Funktionalität zu verwenden. Zuletzt lassen sich die Ergebnisse dann von Python nach C beziehungsweise C++ konvertieren.
Prinzipiell bieten sich zwei Möglichkeiten an, um Python-Code in C/C++ auszuführen. Zum einen lässt sich der Python-Code als String oder als Modul verpacken und vollständig ausführen. Darüber hinaus ist es möglich, explizit Code aus einem Python-Modul auszuführen. Diese zweite Variante ist deutlich anspruchsvoller und kann daher nicht in diesem Artikel vorgestellt werden.
Die größte Hürde beim Einbetten besteht darin, die richtigen Compiler- und Linkeroptionen zu bestimmen. Mit dem Executable python-config lassen sich die Optionen für den Compiler (python-config –clflags) sowie für den Linker (python-config –ldflags) auf Linux bestimmen.
Das direkte Ausführen eines Python-Strings oder Python-Moduls sollte nun leicht von der Hand gehen.
Python-Strings direkt ausführen
Das einfache C-Programm showTimeString.cpp im folgenden Listing dient als erstes Beispiel.
#include <Python.h>
int main(int argc, char* argv[]) {
Py_Initialize();
PyRun_SimpleString("import timen"
"print(time.ctime(time.time()))");
Py_Finalize();
}
Mit der Headerdatei <Python.h> stehen die Anweisungen in dem main-Programm zu Verfügung. Py_Initialize initialisiert den Python-Interpreter. PyRun_SimpleString führt den Python-String direkt aus, der zuerst das Modul time lädt, um anschließend die aktuelle Uhrzeit auszugeben. Py_Finalize fährt den Interpreter wieder runter.
Python-Module direkt ausführen
Ähnlich einfach ist das Ausführen eines Python-Moduls in dem folgenden C-Programm. Der Quellcode im folgenden Listing stellt das Python-Modul showTime.py vor.
import time
print(time.ctime(time.time()))
Ähnlich wie bei dem Python-String im vorherigen Listing importiert das Python-Modul auch das Modul time und stellt die aktuelle Uhrzeit dar. Deutlich komfortabler ist hingegen das C-Programm showTimeModule.cpp im Folgenden, das das Python-Modul lädt und ausführt.
#include <Python.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
Py_Initialize();
FILE* pyFile = fopen("showTime.py", "r");
if (pyFile) {
PyRun_SimpleFile(pyFile, "showTime.py");
fclose(pyFile);
}
Py_Finalize();
}
Das Programm öffnet in diesem Fall die Datei showTime.py und führt das Python-Modul mithilfe des Befehls PyRun_SimpleFile direkt aus (Bild 6).
Persönliches Fazit
Auf die Frage, welche Lösung für das Erweitern von Python um C/C++ zu empfehlen ist, gibt es keine einfache Antwort.
- Für das Erweitern um eine bestehende dynamische Bibliothek ist das Python-Modul ctypes die erste Wahl.
- Sollen C beziehungsweise C++ nicht nur mit Python, sondern zudem mit weiteren Skriptsprachen kommunizieren, spielt SWIG seine volle Stärke aus.
- Den Autor hat pybind11 am stärksten beeindruckt. Mit diesem Framework ist es möglich, C++-Funktionalität mit Python anzusprechen.
Dieser Beitrag stammt mit freundlicher Genehmigung des Autors aus dem Tagungsband des ESE Kongress 2021.