07 Mehrsprachige Projekte: Python C-Erweiterungen erstellen
January 2026 (8302 Words, 47 Minutes)
1. Einführung: Der Party-Trick
“Was wäre, wenn Ihr C++-Code Ergebnisse sofort mit Plotly visualisieren könnte?”
Das ist der Party-Trick von mehrsprachigen Python-Projekten: Sie schreiben performancekritischen Code in C++, behalten aber alle Vorteile des Python-Ökosystems – Dash für Web-Apps, Plotly für interaktive Diagramme, NumPy für Datenmanipulation.
1.1 Das Beste aus beiden Welten
find_intersection()
10x schneller
wo es zählt Nahtlose
Integration Reiches Ökosystem
für Visualisierung
1.2 Unser Road Profile Viewer Beispiel
Erinnern Sie sich an den Road Profile Viewer aus früheren Vorlesungen? Er hat einen Schieberegler, der den Kamerawinkel in Echtzeit anpasst und berechnet, wo der Strahl das Straßenprofil schneidet.
Die Python-Implementierung funktioniert perfekt, aber:
- Die
find_intersection()-Funktion läuft bei jeder Schieberegler-Bewegung - Mit einem komplexen Straßenprofil wird dies zum Engpass
- Auf eingebetteten Systemen oder mobilen Geräten zählt jede Millisekunde
Die Lösung: Portiere find_intersection() nach C++, behalte aber Plotly für die Visualisierung.
1.3 Was Sie lernen werden
In dieser Vorlesung lernen Sie den kompletten Weg von reinem Python zur verteilbaren C-Erweiterung:
- Was C-Erweiterungen sind und warum sie existieren
- pybind11 zum Erstellen der Python-C++-Brücke
- scikit-build-core als modernes Build-Backend
- uv build zum lokalen Erstellen von Wheels
- cibuildwheel für plattformübergreifende CI-Builds
- uv publish um Ihr Paket mit der Welt zu teilen
2. Was ist eine Python C-Erweiterung?
2.1 Eine kurze Geschichte
Python unterstützt native Erweiterungen seit seiner allerersten Veröffentlichung 1991. Das ist kein nachträglicher Einfall – es ist ein Kernpunkt von Pythons Designphilosophie.
Guido van Rossum entwarf Python als einbettbar und erweiterbar:
- Einbettbar: Sie können Python in eine größere Anwendung einbetten
- Erweiterbar: Sie können Python mit in C/C++ geschriebenen Modulen erweitern
Deshalb sind so viele “Python”-Bibliotheken eigentlich dünne Wrapper um C-Code:
| Bibliothek | Python API | Kern-Implementierung |
|---|---|---|
| NumPy | np.array([1, 2, 3]) |
C (mit SIMD-Optimierungen) |
| pandas | df.groupby('col') |
Cython + C |
| PyTorch | torch.tensor([...]) |
C++ (ATen-Bibliothek) |
| OpenCV | cv2.imread('img.jpg') |
C++ |
| scikit-learn | model.fit(X, y) |
Cython + C |
2.2 Wie CPython funktioniert
Der Standard-Python-Interpreter heißt CPython, weil er in C geschrieben ist. Wenn Sie python script.py ausführen, führen Sie ein C-Programm aus, das Ihren Python-Code interpretiert.
Wichtige Erkenntnis: C-Erweiterungsmodule werden zu Maschinencode kompiliert (.so unter Linux/macOS, .pyd unter Windows) und direkt vom Python-Interpreter geladen. Sie umgehen die Bytecode-Interpretation komplett.
2.3 Die traditionelle C-API
Python bietet eine C-API zum Erstellen von Erweiterungsmodulen. So sieht eine einfache Funktion aus:
// traditional_module.c
#include <Python.h>
// Die eigentliche Funktionsimplementierung
static PyObject* calculate(PyObject* self, PyObject* args) {
double x;
// Parse das Python-Argument in ein C double
if (!PyArg_ParseTuple(args, "d", &x)) {
return NULL; // Fehler: falscher Argumenttyp
}
// Führe die Berechnung durch
double result = x * 2.0;
// Konvertiere C-Ergebnis zurück zu Python-Objekt
return PyFloat_FromDouble(result);
}
// Methodentabelle: bildet Python-Funktionsnamen auf C-Funktionen ab
static PyMethodDef ModuleMethods[] = {
{"calculate", calculate, METH_VARARGS, "Calculate x * 2"},
{NULL, NULL, 0, NULL} // Sentinel (Ende des Arrays)
};
// Moduldefinitionsstruktur
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"my_module", // Modulname
NULL, // Modul-Docstring
-1, // Größe des Interpreter-spezifischen Zustands (-1 = global)
ModuleMethods // Methodentabelle
};
// Modul-Initialisierungsfunktion (wird beim Import aufgerufen)
PyMODINIT_FUNC PyInit_my_module(void) {
return PyModule_Create(&moduledef);
}
Das sind 35 Zeilen Boilerplate für eine einzeilige Berechnung!
2.4 Warum die C-API mühsam ist
Das direkte Arbeiten mit Pythons C-API erfordert:
-
Referenzzählung verwalten: Jedes
PyObject*hat einen Referenzzähler. Vergessen Sie ihn zu erhöhen oder zu verringern, bekommen Sie Speicherlecks oder Abstürze. -
Fehler manuell behandeln: Jeder C-API-Aufruf kann fehlschlagen. Sie müssen Rückgabewerte prüfen und Fehler korrekt propagieren.
-
Typen manuell konvertieren:
PyArg_ParseTupleundPy_BuildValueverwenden Format-Strings, die leicht falsch zu machen sind. -
Viel Boilerplate schreiben: Methodentabellen, Moduldefinitionen, Initialisierungsfunktionen…
-
Mit dem GIL umgehen: Der Global Interpreter Lock erfordert sorgfältige Behandlung in Multithread-Code.
2.5 Evolution der Python/C++ Binding-Ansätze
Die Community entwickelte verschiedene Lösungen, um C/C++ Binding einfacher zu machen:
| Jahr | Technologie | Ansatz | Schlüsseleigenschaft |
|---|---|---|---|
| 1991 | Python C API | Direktes C | Maximale Kontrolle, maximaler Boilerplate |
| 1996 | SWIG | Codegenerator | Wrappt existierende C/C++ Header automatisch |
| 2002 | Boost.Python | C++ Templates | Typsicher, erfordert aber Boost-Bibliothek |
| 2007 | Cython | Python-ähnliche Sprache | Schreibe "Python mit Typen", kompiliert zu C |
| 2015 | pybind11 | Header-only C++ | Modernes C++, minimaler Overhead, am beliebtesten |
| 2022 | nanobind | Header-only C++ | pybind11s Nachfolger, noch kleinere Binaries |
Heutige Wahl: Wir verwenden pybind11 weil:
- Es ist die am weitesten verbreitete moderne Lösung
- Header-only (keine Bibliothek zum Linken)
- Exzellente NumPy-Integration
- Aktive Community und Dokumentation
3. pybind11: Der moderne Ansatz
3.1 Was ist pybind11?
pybind11 ist eine leichtgewichtige, header-only Bibliothek, die C++ Template-Metaprogrammierung verwendet, um Python-Bindings zu generieren. Sie exponiert C++-Typen und -Funktionen zu Python mit minimalem Boilerplate.
Vergleiche das C-API-Beispiel von vorhin:
// pybind11-Version - gleiche Funktionalität, 90% weniger Code
#include <pybind11/pybind11.h>
double calculate(double x) {
return x * 2.0;
}
PYBIND11_MODULE(my_module, m) {
m.def("calculate", &calculate, "Calculate x * 2");
}
Das sind 10 Zeilen statt 35, und es ist viel klarer, was passiert.
3.2 Wie pybind11 funktioniert
pybind11 verwendet C++ Templates um:
- Funktionssignaturen zu introspizieren zur Kompilierzeit
- Typkonvertierungscode automatisch zu generieren
- Python-kompatible Moduldefinitionen zu erstellen ohne manuelle Arbeit
┌───────────────────────────────────────────────────────────────────┐
│ pybind11 Magie │
├───────────────────────────────────────────────────────────────────┤
│ │
│ Ihr C++ Code pybind11 Python │
│ ────────────── ───────── ──────── │
│ │
│ double calculate( Template >>> import │
│ double x Magie my_mod │
│ ) { ↓ >>> my_mod │
│ return x * 2; Generiert: .calc(5) │
│ } - Typ-Checks 10.0 │
│ - Konvertierungen │
│ PYBIND11_MODULE(...) - Fehlerbehandlung │
│ - Python API │
│ │
└───────────────────────────────────────────────────────────────────┘
3.3 Automatische Typkonvertierungen
pybind11 konvertiert automatisch zwischen Python- und C++-Typen:
| Python-Typ | C++-Typ | Anmerkungen |
|---|---|---|
int |
int, long, int64_t |
Überlaufprüfung |
float |
float, double |
Automatische Konvertierung |
str |
std::string |
UTF-8-Kodierung |
list |
std::vector<T> |
Kopiert Elemente |
dict |
std::map<K, V> |
Kopiert Elemente |
tuple |
std::tuple<...> |
Feste Größe |
None |
std::optional<T> |
C++17 |
numpy.ndarray |
py::array_t<T> |
Zero-Copy möglich! |
3.4 NumPy-Integration
Das Killer-Feature für wissenschaftliches Python: pybind11 integriert sich nahtlos mit NumPy.
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
// Funktion die NumPy-Arrays nimmt und zurückgibt
py::array_t<double> double_elements(py::array_t<double> input) {
// Hole Buffer-Info (Shape, Strides, Pointer)
auto buf = input.request();
double* ptr = static_cast<double*>(buf.ptr);
size_t size = buf.size;
// Erstelle Ausgabe-Array
py::array_t<double> output(size);
auto out_buf = output.request();
double* out_ptr = static_cast<double*>(out_buf.ptr);
// Verarbeite Elemente
for (size_t i = 0; i < size; ++i) {
out_ptr[i] = ptr[i] * 2.0;
}
return output;
}
PYBIND11_MODULE(my_module, m) {
m.def("double_elements", &double_elements);
}
Verwendung in Python:
import numpy as np
import my_module
arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
result = my_module.double_elements(arr)
print(result) # [2. 4. 6. 8. 10.]
4. Python Wheels und Build-Backends verstehen
Bevor wir C-Erweiterungen bauen können, müssen wir verstehen, wie Python-Pakete verteilt werden. Das ist eines der wichtigsten Konzepte im Python-Packaging – und es beeinflusst direkt Ihre Werkzeugwahl.
4.1 Was ist ein Python Wheel?
Ein Wheel ist Pythons Standardformat für die Verteilung vorkompilierter Pakete. Der Name kommt von der Phrase “wheel of cheese” – eine spielerische Anspielung auf den “CheeseShop” (der ursprüngliche Name für PyPI).
┌─────────────────────────────────────────────────────────────────────────┐
│ WAS IST IN EINER WHEEL-DATEI? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ road_profile_viewer-0.1.0-cp312-cp312-win_amd64.whl │
│ └──────────────────────────────────────────────────┘ │
│ Einfach eine .zip-Datei mit speziellem Namen! │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ road_profile_viewer/ │ │
│ │ ├── __init__.py │ │
│ │ ├── geometry.py │ │
│ │ ├── visualization.py │ │
│ │ └── geometry_cpp.cp312-win_amd64.pyd ← Kompilierte C-Erw.! │ │
│ │ │ │
│ │ road_profile_viewer-0.1.0.dist-info/ │ │
│ │ ├── METADATA │ │
│ │ ├── WHEEL │ │
│ │ └── RECORD │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Installation = Entpacken nach site-packages. Das war's! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Der Wheel-Dateiname sagt Ihnen alles:
road_profile_viewer-0.1.0-cp312-cp312-win_amd64.whl
│ │ │ │ │
│ │ │ │ └── Plattform (Windows 64-bit)
│ │ │ └── Python ABI (cp312 = CPython 3.12)
│ │ └── Python-Version (3.12)
│ └── Paket-Version
└── Paketname
4.2 Pure Python vs. Plattform-Wheels
Es gibt zwei Arten von Wheels:
Pure Python Wheels (...-py3-none-any.whl):
- Enthalten nur
.py-Dateien - Funktionieren auf jeder Plattform mit Python
- Ein einzelnes Wheel dient allen
Plattform-Wheels (...-cp312-cp312-win_amd64.whl):
- Enthalten kompilierten Code (
.pyd,.so) - Spezifisch für Python-Version UND Plattform
- Benötigen ein Wheel pro Plattform/Versions-Kombination
| Pakettyp | Wheel-Namensmuster | Beispiel |
|---|---|---|
| Pure Python | *-py3-none-any.whl |
requests-2.31.0-py3-none-any.whl |
| C-Erweiterung (Windows) | *-cp312-cp312-win_amd64.whl |
numpy-1.26.0-cp312-cp312-win_amd64.whl |
| C-Erweiterung (macOS) | *-cp312-cp312-macosx_*.whl |
numpy-1.26.0-cp312-cp312-macosx_11_0_arm64.whl |
| C-Erweiterung (Linux) | *-cp312-cp312-manylinux*.whl |
numpy-1.26.0-cp312-cp312-manylinux_2_17_x86_64.whl |
4.3 Wheels vs. Source Distributions
Wenn Sie ein Paket bauen, erstellen Sie eigentlich zwei Dinge:
uv build
# Erstellt:
# dist/road_profile_viewer-0.1.0.tar.gz (Source Distribution)
# dist/road_profile_viewer-0.1.0-cp312-....whl (Wheel)
| Format | Endung | Enthält | Installation |
|---|---|---|---|
| Source Distribution (sdist) | .tar.gz |
Quellcode, pyproject.toml, CMakeLists.txt | Erfordert Kompilierung auf Nutzermaschine |
| Wheel | .whl |
Vorkompiliert, installationsbereit | Nur entpacken – keine Kompilierung nötig |
Warum beides?
- Wheels werden bevorzugt: schnelle Installation, kein Compiler nötig
- sdist ist der Fallback: wenn kein Wheel für Ihre Plattform existiert, baut pip/uv aus dem Quellcode
Deshalb bieten beliebte Pakete wie NumPy Dutzende von Wheels – eines für jede Python-Version und Plattformkombination.
4.4 Das Problem: C++ für Nutzer bauen
Jetzt verstehen Sie, warum wir Wheels brauchen. Aber wie erstellen wir sie?
Wenn jemand uv pip install ihr-paket ausführt, was passiert dann?
Für Pure Python Pakete: Einfach! Kopieren Sie die .py-Dateien in ein Wheel.
Für C-Erweiterungen: Sie müssen C/C++-Code kompilieren. Das erfordert:
- Einen C/C++-Compiler (GCC, Clang, MSVC)
- Python-Development-Header
- Korrekte Compiler-Flags für die Plattform
- Wissen, wohin die kompilierte
.so/.pydgehört
Die meisten Nutzer haben keine Compiler installiert. Sie erwarten, uv pip install auszuführen und dass es einfach funktioniert. Deshalb bauen Sie (der Paketautor) Wheels für alle Plattformen und laden sie auf PyPI hoch.
4.5 uv und Build-Backends: Der uv-First-Ansatz
Im gesamten Kurs haben wir uv als unseren Python-Paketmanager verwendet. uv verwendet standardmäßig das uv_build-Backend – aber es hat eine Einschränkung:
“Das uv Build-Backend unterstützt derzeit nur reinen Python-Code.” — uv-Dokumentation
Für C/C++-Erweiterungen empfiehlt uv explizit die Verwendung von scikit-build-core. Sie können sogar ein Projekt direkt damit erstellen:
# uvs eingebaute Unterstützung für C/C++-Projekte!
uv init --build-backend scikit-build-core my-cpp-extension
# Das erstellt:
# my-cpp-extension/
# ├── pyproject.toml (konfiguriert für scikit-build-core)
# ├── CMakeLists.txt (CMake-Build-Konfiguration)
# └── src/
# └── main.cpp (Beispiel C++-Code)
Das ist unser uv-first-Ansatz: Wir verwenden uv für alles, und uv sagt uns, scikit-build-core für C-Erweiterungen zu verwenden.
4.6 Warum scikit-build-core?
uv unterstützt mehrere Build-Backends:
| Backend | Anwendungsfall | uv-Befehl |
|---|---|---|
| uv_build | Pure Python Pakete (Standard) | uv init --lib |
| hatchling | Pure Python mit Build-Hooks | uv init --build-backend hatchling |
| scikit-build-core | C/C++/Fortran/Cython-Erweiterungen | uv init --build-backend scikit-build-core |
| maturin | Rust-Erweiterungen | uv init --build-backend maturin |
scikit-build-core verwendet unter der Haube CMake, das Sie bereits in Teil 1 gelernt haben:
| Feature | Alter setuptools-Ansatz | scikit-build-core |
|---|---|---|
| Build-System | Eigener Python-Code | CMake (Industriestandard) |
| Dependency-Handling | Manuelle Pfade | find_package() |
| Cross-Platform | Fragil, manuelle Flags | CMake übernimmt es |
| uv-Integration | Funktioniert | First-Class-Support |
| IDE-Support | Eingeschränkt | Voller CMake/clangd-Support |
4.7 Der traditionelle Ansatz: setuptools
Der alte Weg verwendete setuptools mit eigenen Build-Befehlen:
# setup.py (der ALTE Weg)
from setuptools import setup, Extension
ext = Extension(
'my_module',
sources=['src/module.cpp'],
include_dirs=['/path/to/pybind11/include'],
extra_compile_args=['-std=c++17'],
)
setup(
name='my-package',
ext_modules=[ext],
)
Probleme mit diesem Ansatz:
- Plattformspezifische Pfade und Flags
- Kein Standardweg zum Finden von Abhängigkeiten
- Begrenzte CMake-Integration
- Komplexe Cross-Compilation
4.8 PEP 517: Die Standard-Build-Schnittstelle
PEP 517 führte eine Standardschnittstelle für Build-Backends ein. Anstatt setup.py deklarieren Sie Ihr Build-System in pyproject.toml:
[build-system]
requires = ["scikit-build-core", "pybind11"]
build-backend = "scikit_build_core.build"
Jetzt kann jedes Tool (pip, uv, build) Ihr Paket mit dem deklarierten Backend bauen. Deshalb kann uv nahtlos mit scikit-build-core arbeiten – beide sprechen dieselbe PEP-517-Sprache.
4.9 Minimales scikit-build-core Setup
Sie brauchen nur zwei Dateien, um C++ zu Ihrem Python-Paket hinzuzufügen:
pyproject.toml:
[project]
name = "my-package"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["scikit-build-core>=0.10", "pybind11>=2.13"]
build-backend = "scikit_build_core.build"
CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(my_package LANGUAGES CXX)
find_package(pybind11 CONFIG REQUIRED)
pybind11_add_module(my_ext src/bindings.cpp)
install(TARGETS my_ext LIBRARY DESTINATION .)
Das war’s! Wenn Sie uv build ausführen, tut scikit-build-core:
- Ruft CMake auf, um den Build zu konfigurieren
- Kompiliert den C++-Code
- Packt alles in ein Wheel
5. Hands-On: find_intersection nach C++ portieren
Portieren wir die find_intersection()-Funktion aus unserem Road Profile Viewer nach C++.
5.1 Das Python-Original
# geometry.py - Die originale Python-Implementierung
def find_intersection(
x_road: NDArray[np.float64],
y_road: NDArray[np.float64],
angle_degrees: float,
camera_x: float = 0,
camera_y: float = 1.5,
) -> tuple[float | None, float | None, float | None]:
"""Find the intersection point between camera ray and road profile."""
angle_rad = -np.deg2rad(angle_degrees)
# Handle vertical ray
if np.abs(np.cos(angle_rad)) < 1e-10:
return None, None, None
slope = np.tan(angle_rad)
# Check each segment of the road for intersection
for i in range(len(x_road) - 1):
x1, y1 = x_road[i], y_road[i]
x2, y2 = x_road[i + 1], y_road[i + 1]
# Skip if segment is behind camera
if x2 <= camera_x:
continue
# Calculate ray y-values at segment endpoints
ray_y1 = camera_y + slope * (x1 - camera_x)
ray_y2 = camera_y + slope * (x2 - camera_x)
# Check for sign change (intersection)
diff1 = ray_y1 - y1
diff2 = ray_y2 - y2
if diff1 * diff2 <= 0:
# Linear interpolation
if abs(diff2 - diff1) < 1e-10:
t = 0
else:
t = diff1 / (diff1 - diff2)
x_intersect = x1 + t * (x2 - x1)
y_intersect = y1 + t * (y2 - y1)
distance = np.sqrt(
(x_intersect - camera_x) ** 2 +
(y_intersect - camera_y) ** 2
)
return x_intersect, y_intersect, distance
return None, None, None
Diese Funktion wird bei jeder Schieberegler-Bewegung aufgerufen. Mit einem Straßenprofil von 1000+ Punkten läuft diese Schleife hunderte Male pro Sekunde.
5.2 Der C++ Header
// cpp/geometry.hpp
#ifndef ROAD_PROFILE_VIEWER_GEOMETRY_HPP
#define ROAD_PROFILE_VIEWER_GEOMETRY_HPP
#include <cmath>
#include <numbers>
#include <optional>
#include <vector>
namespace road_profile_viewer {
/// Result of an intersection calculation
struct IntersectionResult {
double x;
double y;
double distance;
};
/// Find intersection between camera ray and road profile
///
/// @param x_road X-coordinates of road profile points
/// @param y_road Y-coordinates of road profile points
/// @param angle_degrees Camera angle in degrees from horizontal
/// @param camera_x X-position of camera
/// @param camera_y Y-position of camera
/// @return Intersection result or nullopt if no intersection
std::optional<IntersectionResult> find_intersection(
const std::vector<double>& x_road,
const std::vector<double>& y_road,
double angle_degrees,
double camera_x = 0.0,
double camera_y = 1.5
);
} // namespace road_profile_viewer
#endif // ROAD_PROFILE_VIEWER_GEOMETRY_HPP
5.3 Die C++ Implementierung
// cpp/geometry.cpp
#include "geometry.hpp"
namespace road_profile_viewer {
namespace {
constexpr double kEpsilon = 1e-10;
inline double deg_to_rad(double degrees) {
return degrees * std::numbers::pi / 180.0;
}
} // namespace
std::optional<IntersectionResult> find_intersection(
const std::vector<double>& x_road,
const std::vector<double>& y_road,
double angle_degrees,
double camera_x,
double camera_y
) {
const double angle_rad = -deg_to_rad(angle_degrees);
// Handle vertical ray (undefined slope)
if (std::abs(std::cos(angle_rad)) < kEpsilon) {
return std::nullopt;
}
const double slope = std::tan(angle_rad);
// Check each road segment for intersection
for (size_t i = 0; i + 1 < x_road.size(); ++i) {
const double x1 = x_road[i];
const double y1 = y_road[i];
const double x2 = x_road[i + 1];
const double y2 = y_road[i + 1];
// Skip segments behind camera
if (x2 <= camera_x) {
continue;
}
// Calculate ray y-values at segment endpoints
const double ray_y1 = camera_y + slope * (x1 - camera_x);
const double ray_y2 = camera_y + slope * (x2 - camera_x);
const double diff1 = ray_y1 - y1;
const double diff2 = ray_y2 - y2;
// Check for sign change (intersection)
if (diff1 * diff2 <= 0.0) {
double t = 0.0;
if (std::abs(diff2 - diff1) >= kEpsilon) {
t = diff1 / (diff1 - diff2);
}
const double x_intersect = x1 + t * (x2 - x1);
const double y_intersect = y1 + t * (y2 - y1);
const double distance = std::hypot(
x_intersect - camera_x,
y_intersect - camera_y
);
return IntersectionResult{
.x = x_intersect,
.y = y_intersect,
.distance = distance
};
}
}
return std::nullopt;
}
} // namespace road_profile_viewer
5.4 Die pybind11 Bindings
// cpp/bindings.cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include "geometry.hpp"
namespace py = pybind11;
namespace rpv = road_profile_viewer;
PYBIND11_MODULE(geometry_cpp, m) {
m.doc() = "C++ geometry functions for Road Profile Viewer";
m.def("find_intersection",
[](py::array_t<double> x_road,
py::array_t<double> y_road,
double angle_degrees,
double camera_x,
double camera_y) {
// Convert NumPy arrays to std::vector
auto x_buf = x_road.request();
auto y_buf = y_road.request();
std::vector<double> x_vec(
static_cast<double*>(x_buf.ptr),
static_cast<double*>(x_buf.ptr) + x_buf.size
);
std::vector<double> y_vec(
static_cast<double*>(y_buf.ptr),
static_cast<double*>(y_buf.ptr) + y_buf.size
);
// Call C++ function
auto result = rpv::find_intersection(
x_vec, y_vec, angle_degrees, camera_x, camera_y
);
// Convert result back to Python
if (result.has_value()) {
return py::make_tuple(
result->x,
result->y,
result->distance
);
}
return py::make_tuple(py::none(), py::none(), py::none());
},
py::arg("x_road"),
py::arg("y_road"),
py::arg("angle_degrees"),
py::arg("camera_x") = 0.0,
py::arg("camera_y") = 1.5,
R"doc(
Find intersection between camera ray and road profile.
Args:
x_road: NumPy array of road x-coordinates
y_road: NumPy array of road y-coordinates
angle_degrees: Camera angle in degrees
camera_x: X-position of camera (default: 0.0)
camera_y: Y-position of camera (default: 1.5)
Returns:
Tuple of (x, y, distance) or (None, None, None) if no intersection
)doc"
);
}
5.5 Projektstruktur
Hier ist das vollständige Projektlayout:
road-profile-viewer/
├── src/
│ └── road_profile_viewer/
│ ├── __init__.py
│ ├── geometry.py # Python-Implementierung (Fallback)
│ ├── visualization.py # Dash/Plotly UI
│ └── main.py
├── cpp/
│ ├── geometry.hpp # C++ Header
│ ├── geometry.cpp # C++ Implementierung
│ └── bindings.cpp # pybind11 Bindings
├── tests/
│ └── test_geometry.py
├── CMakeLists.txt # Build-Konfiguration
├── pyproject.toml # Projekt-Metadaten + Build-Backend
└── README.md
5.6 Die vollständige pyproject.toml
[project]
name = "road-profile-viewer"
version = "0.1.0"
description = "Interactive 2D road profile viewer with C++ acceleration"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"dash>=2.14.0",
"plotly>=5.18.0",
"numpy>=1.26.0",
]
[project.scripts]
road-profile-viewer = "road_profile_viewer:main"
[build-system]
requires = ["scikit-build-core>=0.10", "pybind11>=2.13"]
build-backend = "scikit_build_core.build"
[tool.scikit-build]
wheel.packages = ["src/road_profile_viewer"]
cmake.build-type = "Release"
[dependency-groups]
dev = [
"pytest>=8.0",
"ruff>=0.8",
"pyright>=1.1",
]
5.7 Die vollständige CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(road_profile_viewer LANGUAGES CXX)
# Require C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find pybind11 (provided by build-system.requires)
find_package(pybind11 CONFIG REQUIRED)
# Create the Python module
pybind11_add_module(geometry_cpp
cpp/bindings.cpp
cpp/geometry.cpp
)
# Include directory for header
target_include_directories(geometry_cpp PRIVATE cpp)
# Install into the Python package directory
install(TARGETS geometry_cpp LIBRARY DESTINATION road_profile_viewer)
6. Der uv Build Workflow
6.1 Ihr Paket bauen
Mit konfiguriertem scikit-build-core ist das Bauen einfach:
# Baue sowohl sdist als auch wheel
uv build
# Ausgabe:
# Building road-profile-viewer@0.1.0
# ...CMake-Konfigurationsausgabe...
# ...Kompilierungsausgabe...
# Successfully built dist/road_profile_viewer-0.1.0.tar.gz
# Successfully built dist/road_profile_viewer-0.1.0-cp312-cp312-win_amd64.whl
6.2 Was ist im dist/-Verzeichnis?
dist/
├── road_profile_viewer-0.1.0.tar.gz # Source Distribution
└── road_profile_viewer-0.1.0-cp312-cp312-win_amd64.whl # Binary Wheel
Source Distribution (sdist): Enthält Quellcode. Der Empfänger benötigt einen Compiler zur Installation.
Wheel: Enthält vorkompilierte Binaries. Einfach entpacken und ausführen. Der Name kodiert die Kompatibilität:
road_profile_viewer-0.1.0-cp312-cp312-win_amd64.whl
│ │ │ │ │
│ │ │ │ └── Plattform (Windows x64)
│ │ │ └── Python ABI (CPython 3.12)
│ │ └── Python-Version (CPython 3.12)
│ └── Paket-Version
└── Paketname
6.3 Ihr Wheel testen
# Installiere das gerade gebaute Wheel
uv pip install dist/road_profile_viewer-0.1.0-cp312-cp312-win_amd64.whl
# Teste das C++-Modul
python -c "from road_profile_viewer.geometry_cpp import find_intersection; print('C++ module loaded!')"
6.4 Der Build-Flow
┌──────────────────────────────────────────────────────────────────────────┐
│ uv build Workflow │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ pyproject.toml │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ scikit-build- │ │
│ │ core erkennt │ ← Liest build-system.requires │
│ │ C++-Code │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ CMake │ ← Konfiguriert Build, findet pybind11 │
│ │ Konfiguration │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ C++ Kompilierung│ ← Erstellt geometry_cpp.pyd (Windows) │
│ │ │ oder geometry_cpp.so (Linux/macOS) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Wheel-Erstellung│ ← Bündelt Python + kompilierte Erweiterung │
│ └────────┬────────┘ │
│ ▼ │
│ dist/package-version-platform.whl │
│ │
└──────────────────────────────────────────────────────────────────────────┘
7. Das Python-Integrationsmuster
7.1 Das Fallback-Muster
Die beste Praxis ist, einen Python-Fallback bereitzustellen. Das ermöglicht Ihrem Paket zu funktionieren, auch wenn:
- Das C++-Modul nicht kompiliert werden konnte
- Der Nutzer auf einer nicht unterstützten Plattform ist
- Sie debuggen und C++ umgehen wollen
# src/road_profile_viewer/geometry.py
# Versuche C++-Implementierung zu importieren
try:
from road_profile_viewer.geometry_cpp import (
find_intersection as _cpp_find_intersection,
)
_USE_CPP = True
except ImportError:
_USE_CPP = False
import numpy as np
from numpy.typing import NDArray
def find_intersection(
x_road: NDArray[np.float64],
y_road: NDArray[np.float64],
angle_degrees: float,
camera_x: float = 0,
camera_y: float = 1.5,
) -> tuple[float | None, float | None, float | None]:
"""Find intersection between camera ray and road profile.
Uses C++ implementation if available, falls back to Python.
"""
if _USE_CPP:
return _cpp_find_intersection(
x_road, y_road, angle_degrees, camera_x, camera_y
)
# Python-Implementierung folgt...
angle_rad = -np.deg2rad(angle_degrees)
if np.abs(np.cos(angle_rad)) < 1e-10:
return None, None, None
slope = np.tan(angle_rad)
for i in range(len(x_road) - 1):
x1, y1 = x_road[i], y_road[i]
x2, y2 = x_road[i + 1], y_road[i + 1]
if x2 <= camera_x:
continue
ray_y1 = camera_y + slope * (x1 - camera_x)
ray_y2 = camera_y + slope * (x2 - camera_x)
diff1 = ray_y1 - y1
diff2 = ray_y2 - y2
if diff1 * diff2 <= 0:
if abs(diff2 - diff1) < 1e-10:
t = 0
else:
t = diff1 / (diff1 - diff2)
x_intersect = x1 + t * (x2 - x1)
y_intersect = y1 + t * (y2 - y1)
distance = np.sqrt(
(x_intersect - camera_x) ** 2 +
(y_intersect - camera_y) ** 2
)
return x_intersect, y_intersect, distance
return None, None, None
# Optional: zeige welche Implementierung verwendet wird
def get_backend() -> str:
"""Return the active backend: 'cpp' or 'python'."""
return "cpp" if _USE_CPP else "python"
7.2 Warum Fallbacks wichtig sind
| Szenario | Ohne Fallback | Mit Fallback |
|---|---|---|
| Nutzer hat keinen Compiler | Installation schlägt fehl | Funktioniert (langsamer) |
| Nicht unterstützte Plattform | ImportError Absturz | Funktioniert (langsamer) |
| Debugging | Schwer durch C++ zu steppen | Python direkt verwenden |
| Entwicklung | Muss nach jeder Änderung neu bauen | Python editieren, sofort testen |
7.3 Der Party-Trick verwirklicht
Jetzt muss sich Ihr Visualisierungscode überhaupt nicht ändern:
# visualization.py - Unverändert!
from road_profile_viewer.geometry import find_intersection
@app.callback(...)
def update_plot(angle):
# Dies verwendet automatisch C++ wenn verfügbar!
x, y, distance = find_intersection(x_road, y_road, angle)
# Plotly-Visualisierung - unverändert
fig = go.Figure(...)
return fig
Der Party-Trick: Ihr C++-Code ist 10x schneller, aber Sie verwenden immer noch Plotly, Dash und das gesamte Python-Ökosystem nahtlos.
8. cibuildwheel: Plattformübergreifende Wheels in CI
8.1 Das Problem
Wenn Sie uv build lokal ausführen, bekommen Sie ein Wheel nur für Ihre Plattform.
Aber Ihre Nutzer sind auf:
- Windows (x64, ARM)
- macOS (Intel, Apple Silicon)
- Linux (x64, ARM, verschiedene glibc-Versionen)
Jede Plattform braucht ihr eigenes Wheel. Alle manuell zu bauen ist unpraktikabel.
8.2 Die Lösung: cibuildwheel
cibuildwheel ist ein Tool, das Wheels für alle Plattformen mit GitHub Actions (oder anderem CI) baut. Es:
- Läuft auf Linux-, macOS- und Windows-Runnern
- Verwendet Docker für Linux, um portable “manylinux”-Wheels zu erstellen
- Behandelt die Python-Versionsmatrix automatisch
- Lädt Wheels als Artefakte hoch
8.3 GitHub Actions Workflow
Erstelle .github/workflows/build-wheels.yml:
name: Build Wheels
on:
push:
tags:
- 'v*' # Trigger bei Versions-Tags
workflow_dispatch: # Manueller Trigger
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Build wheels
uses: pypa/cibuildwheel@v2.21
env:
# Baue für Python 3.12+
CIBW_BUILD: "cp312-* cp313-*"
# Überspringe 32-bit und musl Linux
CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux*"
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}
path: ./wheelhouse/*.whl
build_sdist:
name: Build source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Build sdist
run: uv build --sdist
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz
8.4 Was wird gebaut?
Nach dem Workflow-Lauf haben Sie Wheels für:
wheelhouse/
├── road_profile_viewer-0.1.0-cp312-cp312-manylinux_2_17_x86_64.whl
├── road_profile_viewer-0.1.0-cp312-cp312-macosx_11_0_arm64.whl
├── road_profile_viewer-0.1.0-cp312-cp312-macosx_10_14_x86_64.whl
├── road_profile_viewer-0.1.0-cp312-cp312-win_amd64.whl
├── road_profile_viewer-0.1.0-cp313-cp313-manylinux_2_17_x86_64.whl
├── road_profile_viewer-0.1.0-cp313-cp313-macosx_11_0_arm64.whl
├── road_profile_viewer-0.1.0-cp313-cp313-macosx_10_14_x86_64.whl
└── road_profile_viewer-0.1.0-cp313-cp313-win_amd64.whl
manylinux: Ein standardisiertes Linux-Wheel-Format, das auf den meisten Linux-Distributionen funktioniert.
macosx_11_0_arm64: Apple Silicon (M1/M2/M3).
macosx_10_14_x86_64: Intel Macs.
9. Veröffentlichen mit uv
9.1 Der Python Package Index (PyPI)
PyPI ist das offizielle Repository für Python-Pakete. Wenn Sie pip install something ausführen, wird von PyPI heruntergeladen.
Um Ihr Paket zu veröffentlichen:
- Erstellen Sie ein Konto auf pypi.org
- Generieren Sie ein API-Token
- Laden Sie Ihre Wheels hoch
9.2 Zuerst Test PyPI
Testen Sie immer zuerst auf Test PyPI, bevor Sie auf Produktion veröffentlichen:
# Upload zu Test PyPI
uv publish --publish-url https://test.pypi.org/legacy/
# Teste Installation
pip install -i https://test.pypi.org/simple/ road-profile-viewer
9.3 Produktions-Veröffentlichung
Einmal getestet, veröffentliche auf dem echten PyPI:
# Veröffentliche alle Wheels und sdist
uv publish
Das lädt alles in dist/ auf PyPI hoch.
9.4 Trusted Publishing (Empfohlen)
Anstatt API-Tokens verwende Trusted Publishing mit GitHub Actions. Dies nutzt OpenID Connect (OIDC) zur Authentifizierung – keine Secrets zu verwalten!
Fügen Sie zu Ihrem Workflow hinzu:
publish:
name: Publish to PyPI
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
permissions:
id-token: write # Erforderlich für Trusted Publishing
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Konfigurieren Sie Trusted Publishing in Ihren PyPI-Kontoeinstellungen, um Ihr GitHub-Repository zu verknüpfen.
9.5 Die komplette CI/CD-Pipeline
┌──────────────────────────────────────────────────────────────────────────┐
│ Release Pipeline │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Entwickler erstellt Tag: git tag v0.1.0 && git push --tags │
│ │ │
│ ▼ │
│ 2. GitHub Actions wird durch Tag-Push ausgelöst │
│ │ │
│ ▼ │
│ 3. cibuildwheel baut Wheels für alle Plattformen (parallel) │
│ ├── ubuntu-latest → manylinux Wheels │
│ ├── macos-latest → macOS Wheels (Intel + ARM) │
│ └── windows-latest → Windows Wheels │
│ │ │
│ ▼ │
│ 4. uv build --sdist erstellt Source Distribution │
│ │ │
│ ▼ │
│ 5. Alle Artefakte werden gesammelt │
│ │ │
│ ▼ │
│ 6. pypa/gh-action-pypi-publish lädt auf PyPI hoch │
│ (mit Trusted Publishing - keine Tokens!) │
│ │ │
│ ▼ │
│ 7. Nutzer können jetzt: pip install road-profile-viewer │
│ │
└──────────────────────────────────────────────────────────────────────────┘
10. Leistungsvergleich
10.1 Benchmark-Setup
Messen wir den Leistungsunterschied:
# benchmark.py
import time
import numpy as np
from road_profile_viewer.geometry import find_intersection, get_backend
# Generiere ein komplexes Straßenprofil
np.random.seed(42)
x_road = np.linspace(0, 100, 1000)
y_road = np.sin(x_road / 10) + np.random.random(1000) * 0.1
def benchmark(iterations=1000):
start = time.perf_counter()
for angle in np.linspace(5, 85, iterations):
find_intersection(x_road, y_road, angle, 0, 2.0)
elapsed = time.perf_counter() - start
return elapsed * 1000 # Konvertiere zu ms
print(f"Backend: {get_backend()}")
print(f"Time for 1000 calls: {benchmark():.1f} ms")
10.2 Ergebnisse
| Implementierung | Zeit (1000 Aufrufe) | Speedup |
|---|---|---|
| Python (NumPy) | ~150 ms | 1x (Baseline) |
| C++ via pybind11 | ~15 ms | 10x schneller |
10.3 Wann C++ verwenden
Gute Kandidaten für C++:
- Enge Schleifen über Daten (wie unsere Schnittstellenprüfung)
- Mathematische Berechnungen ohne NumPy-Äquivalente
- Echtzeit-Verarbeitung (Audio, Video, Sensoren)
- Speicherbeschränkte Umgebungen
In Python belassen:
- I/O-Operationen (Datei, Netzwerk)
- Bereits optimierte NumPy-Operationen
- Code, der sich häufig ändert
- Glue-Code und Konfiguration
11. Zusammenfassung
11.1 Wichtige Erkenntnisse
-
Python C-Erweiterungen sind native Code-Module, die der Python-Interpreter direkt lädt. Sie sind seit 1991 Teil von Python.
-
pybind11 macht das Erstellen von Bindings einfach: schreibe normale C++-Funktionen, füge ein paar Zeilen Binding-Code hinzu, fertig.
-
scikit-build-core ist das moderne Build-Backend: es behandelt CMake, Python-Erkennung und Wheel-Erstellung automatisch.
-
uv build erstellt Wheels lokal. Das Wheel enthält vorkompilierte Binaries, die Nutzer ohne Compiler installieren können.
-
cibuildwheel baut Wheels für alle Plattformen in CI. Ein Workflow, Wheels für Windows, macOS und Linux.
-
uv publish lädt auf PyPI hoch. Mit Trusted Publishing brauchen Sie nicht einmal API-Tokens.
-
Das Fallback-Muster hält Ihr Paket überall funktionsfähig: versuchen Sie C++, fallen Sie auf Python zurück.
11.2 Die komplette Toolchain
┌───────────────────────────────────────────────────────────────────────┐
│ DIE C-ERWEITERUNGS-TOOLCHAIN │
├───────────────────────────────────────────────────────────────────────┤
│ │
│ pybind11 scikit-build-core uv │
│ ───────── ───────────────── ── │
│ C++ → Python CMake → Wheel Build → Publish │
│ Bindings Packaging Workflow │
│ │
│ cibuildwheel │
│ ──────────── │
│ Cross-Platform │
│ CI Builds │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ C++ │───►│ CMake │───►│ Wheel │───►│ PyPI │ │
│ │ Code │ │ Build │ │ .whl │ │ Nutzer │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘
11.3 Der Party-Trick
Sie haben jetzt C++ Performance mit Python-Komfort:
- C++ berechnet die Strahl-Straßen-Schnittmenge in Mikrosekunden
- Python orchestriert die Dash-Callbacks und Plotly-Rendering
- Nutzer wissen nie den Unterschied – sie sehen einfach eine schnelle, interaktive App
12. Reflexionsfragen
-
Wann würden Sie C++ statt NumPy-Optimierung wählen? Berücksichtigen Sie Speicherlayout, Vektorisierung und Entwicklungszeit.
-
Was sind die Kompromisse bei der Pflege von Python- und C++-Implementierungen? Denken Sie an Tests, Dokumentation und Konsistenz.
-
Wie würden Sie entscheiden, welche Funktionen nach C++ portiert werden? Welche Profiling-Tools würden Sie verwenden?
-
Was passiert, wenn ein Nutzer Ihr sdist anstelle eines Wheels installiert? Wie handhabt scikit-build-core dies?
-
Warum ist Trusted Publishing besser als API-Tokens? Berücksichtige Sicherheit und Wartung.
13. Was kommt als Nächstes
Dies schließt die Reihe zu Mehrsprachigen Projekten ab. Sie haben gelernt:
- Teil 1: C++ Grundlagen, CMake, Build-Prozess
- Teil 2: Code-Qualität (clang-format, clang-tidy), Testing (Google Test), Coverage
- Teil 3: Erstellen und Verteilen von C-Erweiterungen mit scikit-build-core und uv
In kommenden Vorlesungen werden wir erkunden:
- Software-Architekturmuster: Strukturierung großer Systeme
- Deployment-Strategien: Docker, Cloud-Plattformen, Embedded-Systeme
14. Weiterführende Literatur und Referenzen
Dieser Abschnitt bietet umfassende Ressourcen zur Vertiefung Ihres Verständnisses von Python C-Erweiterungen, Build-Systemen und den in dieser Vorlesung behandelten Tools.
14.1 Offizielle Dokumentation
Kernwerkzeuge:
- pybind11 Dokumentation — Der vollständige Leitfaden zu pybind11, einschließlich fortgeschrittener Features wie Smart Holders, Custom Type Casters und Python-Embedding
- scikit-build-core Dokumentation — Build-Backend-Referenz mit Konfigurationsoptionen und Migrationsanleitungen
- cibuildwheel Dokumentation — Umfassender CI-Wheel-Building-Leitfaden mit plattformspezifischen Konfigurationen
- uv Dokumentation — Modernes Python-Paketmanagement, Building und Publishing
Python Packaging:
- Python Packaging User Guide — Die maßgebliche Quelle für Python-Packaging-Best-Practices
- PyPA Build — PEP 517 Build-Frontend-Dokumentation
- PyPI Help — Pakete veröffentlichen, API-Tokens und Trusted Publishing Setup
Build-Systeme:
- CMake Dokumentation — Offizielles CMake-Referenzhandbuch
- CMake Tutorial — Schritt-für-Schritt-Einführung in CMake
- Modern CMake — Best Practices für moderne CMake-Nutzung
14.2 Spezifikationen und Standards
Das Verständnis der zugrundeliegenden Standards hilft Ihnen, Probleme zu beheben und fundierte architektonische Entscheidungen zu treffen:
Python Enhancement Proposals (PEPs):
- PEP 517 — Ein build-system-unabhängiges Format für Quellbäume (warum scikit-build-core funktioniert)
- PEP 518 — Spezifizierung minimaler Build-System-Anforderungen (die
[build-system]-Tabelle) - PEP 621 — Speicherung von Projektmetadaten in pyproject.toml
- PEP 660 — Editable Installs für PEP 517 Backends
- PEP 427 — Die Wheel-Binärpaketformat-Spezifikation
Plattformstandards:
- manylinux Policy — Linux-Wheel-Kompatibilitätsstandards und Container-Images
- Python Stable ABI — Erstellen von Wheels, die über Python-Versionen hinweg kompatibel sind
14.3 Tiefgehende technische Ressourcen
Python C API (zum Verständnis, nicht für täglichen Gebrauch):
- Python C API Reference — Vollständige Low-Level C API Dokumentation
- Extending Python with C — Offizielles Tutorial zu C-Erweiterungen (traditioneller Ansatz)
- Python Developer’s Guide: C API — Interna für Contributors
pybind11 Fortgeschrittene Themen:
- pybind11 FAQ — Häufige Probleme und Lösungen
- pybind11 GitHub Issues — Suche nach spezifischen Problemen
- pybind11 Discussions — Community Q&A
14.4 Videoressourcen und Tutorials
Konferenzvorträge:
- CppCon 2017: “pybind11 — Seamless operability between C++11 and Python” — Wenzel Jakob stellt pybind11-Designprinzipien vor
- PyData 2016: “Experiences with Cython, Pybind11, and CFFI” — Praktischer Vergleich von Binding-Ansätzen
- PyCon 2019: “Publishing (Perfect) Python Packages on PyPI” — Mark Smith über modernes Packaging
- SciPy 2023: “Building and Distributing Compiled Python Extensions” — Henry Schreiner über scikit-build-core
Tutorial-Reihen:
- Real Python: Python Bindings Overview — Umfassender Vergleich von ctypes, CFFI, pybind11, Cython
- pybind11 Video Tutorial Series — Schritt-für-Schritt pybind11-Einführung
14.5 Bücher und Vertiefende Leitfäden
Empfohlene Bücher:
- “High Performance Python” von Micha Gorelick & Ian Ozsvald (O’Reilly, 2. Auflage)
- Kapitel 7 behandelt Cython und C-Erweiterungen ausführlich
- Praktische Performance-Optimierungstechniken
- “Fluent Python” von Luciano Ramalho (O’Reilly, 2. Auflage)
- Kapitel 24: “Class Metaprogramming” erklärt, wie Python-Typen intern funktionieren
- Nützlich zum Verständnis dessen, was pybind11 automatisiert
- “Modern CMake for C++” von Rafał Świdziński (Packt, 2022)
- Umfassende moderne CMake-Praktiken
- Direkt anwendbar auf scikit-build-core-Projekte
Online-Leitfäden:
- Scientific Python Library Development Guide — Best Practices aus der NumPy/SciPy-Community
- An Introduction to Building Python Extensions — Offizieller Leitfaden
14.6 Alternative Ansätze
Während diese Vorlesung sich auf pybind11 + scikit-build-core konzentrierte, existieren andere Ansätze:
Binding-Generatoren:
| Tool | Am besten für | Dokumentation |
|---|---|---|
| pybind11 | Moderne C++11+ Projekte, NumPy-Integration | pybind11.readthedocs.io |
| nanobind | Kleinere Binaries, schnellere Kompilierung, C++17+ | nanobind.readthedocs.io |
| Cython | Python-ähnliche Syntax, schrittweise Optimierung | cython.readthedocs.io |
| CFFI | Wrappen existierender C-Bibliotheken | cffi.readthedocs.io |
| SWIG | Multi-Language-Bindings aus einer Definition | swig.org |
| maturin | Rust → Python Bindings | maturin.rs |
Build-Backends:
| Backend | Anwendungsfall | Dokumentation |
|---|---|---|
| scikit-build-core | CMake-basierte C/C++ Projekte (empfohlen) | scikit-build-core.readthedocs.io |
| meson-python | Meson-Build-System-Projekte | meson-python.readthedocs.io |
| setuptools | Einfache Erweiterungen, Legacy-Projekte | setuptools.pypa.io |
14.7 Praxisbeispiele
Lerne von produktionsreifen Open-Source-Projekten, die diese Techniken verwenden:
pybind11 + scikit-build-core:
- scikit-image — Bildverarbeitungsbibliothek mit C++-Erweiterungen
- Open3D — 3D-Datenverarbeitung mit pybind11
- awkward-array — Verschachtelte Datenstrukturen mit C++-Backend
pybind11 Beispiele:
- pybind11 Examples Repository — Offizielle Test-Suite zeigt viele Muster
- cmake_example — Minimales CMake + pybind11 Template
- scikit-build-core Examples — Verschiedene Konfigurationsbeispiele
cibuildwheel in Aktion:
- NumPy — Der Goldstandard für Wheel-Building
- Pillow — Komplexe Abhängigkeiten elegant gehandhabt
- cryptography — Sicherheitskritische Builds
14.8 Community und Support
Foren und Q&A:
- Python Packaging Discourse — Offizielle Python-Packaging-Diskussionen
- Stack Overflow: pybind11 — Community Q&A
- Stack Overflow: scikit-build — Build-System-Fragen
GitHub Discussions:
Chat:
- Scientific Python Discord — Echtzeit-Hilfe von der Community
- PyPA Discord — Python Packaging Authority Community
14.9 Aktuell bleiben
Das Python-Packaging-Ökosystem entwickelt sich schnell. Bleibe auf dem Laufenden:
Blogs und News:
- PyPI Blog — Offizielle Ankündigungen zu PyPI-Features
- Astral Blog — Updates zu uv, ruff und modernem Tooling
- Scientific Python Blog — NumPy/SciPy Ökosystem-News
Changelogs:
14.10 Schnellreferenzkarte
Halten Sie dies für Ihre Projekte bereit:
┌─────────────────────────────────────────────────────────────────────┐
│ PYTHON C-ERWEITERUNG SCHNELLREFERENZ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ PROJEKT ERSTELLEN: │
│ uv init my-extension │
│ # pyproject.toml bearbeiten: build-backend = "scikit_build_core.build"│
│ # CMakeLists.txt hinzufügen mit pybind11_add_module() │
│ │
│ BUILD: │
│ uv build # Erstellt dist/*.whl │
│ uv pip install dist/*.whl # Lokal installieren │
│ │
│ TEST: │
│ uv run pytest # Python-Tests ausführen │
│ python -c "from pkg import func; print(func())" │
│ │
│ VERÖFFENTLICHEN: │
│ uv publish --publish-url https://test.pypi.org/legacy/ # Test │
│ uv publish # Prod │
│ │
│ CI (cibuildwheel): │
│ uses: pypa/cibuildwheel@v2.21 │
│ # Baut Wheels für Linux, macOS, Windows automatisch │
│ │
│ SCHLÜSSELDATEIEN: │
│ pyproject.toml - Projektmetadaten + Build-Konfiguration │
│ CMakeLists.txt - C++ Build-Konfiguration │
│ src/pkg/module.cpp - pybind11 Bindings │
│ .github/workflows/ - CI/CD Pipelines │
│ │
│ NÜTZLICHE LINKS: │
│ pybind11: pybind11.readthedocs.io │
│ scikit-build-core: scikit-build-core.readthedocs.io │
│ cibuildwheel: cibuildwheel.pypa.io │
│ uv: docs.astral.sh/uv │
│ │
└─────────────────────────────────────────────────────────────────────┘