Python meets C/C++, Teil 1: Python um C/C++ erweitern oder darin einbetten
Seite 2: Verschiedene Techniken und Frameworks
Die Palette an vorzustellenden Techniken beginnt mit den Low-Level-Werkzeugen und schließt mit den High-Level-Frameworks ab. Als Grundlage für Experimente soll eine Shared Library beziehungsweise eine Dynamic Link Library (DLL) als C/C++-Bibliothek zum Einsatz kommen. Zum einen lassen sich C/C++-Bibliotheken aus Python direkt mit dem Standardmodul ctypes
aufrufen. Zum anderen bietet Python eine native API zur Kommunikation von Python mit C/C++ an. Darüber hinaus lassen sich die Schnittstellen direkt aus dem Simplified Wrapper and Interface Generator SWIG oder aus pybind11 erzeugen. Während SWIG es erlaubt, Bindungen nicht nur für C/C++ zu erzeugen, glänzt pybind11 vor allem darin, Modernes C++ komfortabel einzubinden.
Da beim Erweitern und Einbetten der Teufel sprichwörtlich im Detail steckt, stellen die Artikel hier alle Beispiele mithilfe des GCC-Compiler auf Linux und mit dem cl.exe-Compiler auf Windows vor.
Jetzt ist es an der Zeit, eine einfache Shared Library/DLL für die ersten Schritte zu erzeugen.
Shared Library und Dynamic Link Library
Shared Libaries lassen sich alternativ auch als Shared Objects bezeichnen und entsprechen den Dynamic Link Libraries (DLL) unter Windows. Die Headerdatei in Listing 2 und die dazugehörige Sourcedatei in Listing 3 sind die Grundlage einer denkbar einfachen Shared Library und damit auch für die ersten Schritte, um Python mit C/C++-Funktionalität zu erweitern.
#include <stdio.h>
void helloWorld();
Listing 2: helloWorld.h
#include "helloWorld.h"
void helloWorld() {
printf("Hello World\n");
}
Listing 3: helloWorld.c
Um die Shared Library mithilfe des Compilers GCC zu erzeugen und sie zu verwenden, sind drei Schritte notwendig. Zunächst gilt es, mit einem Befehl positionsunabhängigen Code zu erzeugen: gcc -c -fpic helloWorld.c
. Anschließend erzeugt folgender Code die Shared Library: gcc -shared -o libhelloWorld.so helloWorld.o
, und im letzten Schritt macht man dem Linker und der Laufzeit folgendermaßen die dazugehörigen Pfade bekannt: gcc -LPathToSharedLib -Wl,-rpath=PathToSharedLib -o helloWorldShared main.c -lhelloWorld
.
Sind die drei Schritte erfolgreich umgesetzt, lässt sich die Funktionalität der Shared Library wie in Abbildung 2 direkt verwenden.
Zum Erzeugen der Dynamic Link Library (DLL) unter Windows sind ein paar besondere Schritte notwendig. Zuerst gilt es, mit dem Befehl __declspec()
zu spezifizieren, welche Symbole zu exportieren oder zu importierten sind. Darüber hinaus gilt es mit __stdcall
die Aufrufkonvention festzulegen. Neben der DLL erzeugt der cl.exe-Compiler eine sogenannte Importbibliothek, die auf .lib
endet. Die Importbibliothek enthält Rümpfe für jede Funktion, die die DLL exportiert.
Das Anpassen des Sourcecodes veranschaulichen Listing 4 und 5, die die angepassten Dateien enthalten.
#include <stdio.h>
#if defined(BUILDING_MYLIB)
#define MYLIB_API __declspec(dllexport) __stdcall
#else
#define MYLIB_API __declspec(dllimport) __stdcall
#endif
void MYLIB_API helloWorld();
Listing 4: helloWorldWindows.h
#include "helloWorldWindows.h"
void MYLIB_API helloWorld() {
printf("Hello World\n");
}
Listing 5: helloWorldWindows.c
Als Nächstes geht es mit folgendem Befehl an das Erzeugen der DLL (.dll
) und der Importbibliothek (.lib
): cl.exe /DBUILDING_MYLIB helloWorldWindows.c /LD
. Abschließend lässt sich das Linken des Executables folgendermaßen umsetzen: cl.exe main.c /link helloWorldWindows.lib
.
Nun ist es geschafft. Wie aber lässt sich die Shared Library aus Python verwenden? Die einfachste Antwort liefert Python direkt mit dem Modul ctypes
.
Direkte Kommunikation per Modul ctypes
Mit dem Modul ctypes
lassen sich die Shared Library 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. In der offiziellen Python-Dokumentation lautet ihre Funktionsweise im Wortlaut wie folgt: "ctypes is a foreign function library for Python. It provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python." Das hört sich vielversprechend an. Abbildung 3 zeigt, wie die Shared Library libhelloWorld.so
sich direkt laden und die Funktion "helloWorld.helloWorld()" ausführen lässt.
Der Aufruf unter Linux ist fast buchstäblich nach Windows übersetzbar, lediglich die Konventionen zum Aufruf unter Linux und Windows unterscheiden sich. So verwendet Linux die cdecl
-Aufrufkonvention für die Shared Libraries (ctypes.cdll.LoadLibrary
), Windows hingegen setzt die stdcall
-Aufrufkonvention für DLLs ein (ctypes.windll.LoadLibrary
).
Selbstverständlich ist es möglich, mit dem ctypes
-Modul direkt auf Systembibliotheken wie die libc
-Bibliothek zuzugreifen. Bevor Abbildung 5 und 6 die ersten Gehversuche unter Linux und Windows vorstellen, ist ein wenig Theorie notwendig. In der Regel kann die Python-Laufzeit die Typen der Argumente und den Rückgabetyp nicht selbst bestimmen, hierbei benötigt sie Hilfe. Als Beispiel kommen exemplarisch die C-Funktion printf
für die Spezifikation der Argumenttypen und die C-Funktion strchr
für die Spezifikation des Rückgabetyps infrage. Die Funktion strchr(str, cha)
gibt einen Zeiger auf das erste Vorkommen eines Zeichens cha
im C-String str
zurück.
Datentypen mit Ausnahme von integralen Datentypen, Strings und Bytes müssen spezifiziert werden: libc.printf(2020, "Hello", b"Hello")
. Weitere Datentypen wie double
und S-Strings sind zwingend anzugeben: libc.printf(ctypes.c_double(5.5), ctypes.c_char_p("Hello")
. Die Datentypen lassen sich mithilfe eines Funktionsprototypen direkt angeben. argtypes
(libc.printf.argtypes = [ctypes.c_double, ctypes.c_char_p]
) legt den Datentyp der Parameter fest wie folgt: libc.printf(5.5, "Hello")
. Standardmäßig nimmt Python int
als Rückgabetyp der Funktion an. Weitere Rückgabetypen lassen sich mit restype
spezifizieren (libc.strchr.restype = ctypes.c_char_p
) und im Anschluss verwenden: libc.strchr(b"Hello World", ord("W"))
.
Die folgende Tabelle zeigt die Unterschiede zwischen den fundamentalen C-Datentypen und den Python-Datentypen.
ctypes type | C type | Python type |
c_bool | _Bool | bool (1) |
c_char | char | 1-character bytes object |
c_wchar | wchar_t | 1-character string |
c_byte | char | int |
c_ubyte | unsigned char | int |
c_short | short | int |
c_ushort | unsigned short | int |
c_int | int | int |
c_uint | unsigned int | int |
c_long | long | int |
c_ulong | unsigned long | int |
c_longlong | __int64 or long long | int |
c_ulonglong | unsigned __int64 or unsigned long long | int |
c_size_t | size_t | int |
c_ssize_t | ssize_t or Py_ssize_t | int |
c_float | float | float |
c_double | double | float |
c_longdouble | long double | float |
c_char_p | char * (NUL terminated) | bytes object or None |
c_wchar_p | wchar_t * (NUL terminated) | string or None |
c_void_p | void * | int or None |
Abbildung 5 zeigt, dass sich mit ctypes
die C-Standard-Bibliothek libc
direkt verwenden lässt.
Richtig interessant wird die Ausgabe des Programmes erst mit der Funktion strchr
. So gibt der Aufruf libc.strchr(b"HelloWorld", ord("W"))
eine Ganzzahl zurück, falls der Ausgabetyp nicht spezifiziert ist. Wird hingegen der Rückgabetyp mittels libc.strchr.restype = ctypes.c_char_p
gesetzt, gibt die Funktion den gesuchten Substring zurück. Darüber hinaus lässt sich der zweite Datentyp der Funktion auch als Byte-Literal (ASCII-Zeichen) angeben: libc.strchr.(b"HelloWorld, b"W")
.
Bei Windows kommt das C-Pendant msvcrt
zum Einsatz. Es lässt sich mit dem Befehl ctypes.cdll.msvcrt
direkt ansprechen, wie Abbildung 6 vorführt. Insbesondere die letzte Zeile des Screenshots ist von Interesse: Sie zeigt, dass den libc.prinf
-Funktionen die typischen Attribute eines Python-Objekts zur Verfügung stehen.
ctypes
erlaubt es, existierende C-Bibliotheken direkt aus Python aufzurufen. Es ist ebenfalls möglich, Erweiterungsmodule direkt in C/C++ zu implementieren.