Python meets C/C++, Teil 1: Python um C/C++ erweitern oder darin einbetten

Seite 2: Verschiedene Techniken und Frameworks

Inhaltsverzeichnis

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 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.

Die Shared Library verwenden (Abb. 2)

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.

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.

Die Shared Library unter Linux nutzen (Abb. 3)

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).

Die DLL unter Windows nutzen (Abb. 4)

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.

Verwendung der C-Standard-Bibliothek [code]libc[/code] aus Python unter Linux (Abb. 5)

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.

Verwendung der C-Standard-Bibliothek [code]msvcrt[/code] aus Python unter Windows (Abb. 6)

ctypes erlaubt es, existierende C-Bibliotheken direkt aus Python aufzurufen. Es ist ebenfalls möglich, Erweiterungsmodule direkt in C/C++ zu implementieren.