Home

07 Mehrsprachige Projekte: Python C-Erweiterungen erstellen

lecture cpp python pybind11 scikit-build-core c-extension cibuildwheel uv pypi wheels

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

🎭 Der mehrsprachige Party-Trick
C++ Engine
find_intersection() 10x schneller
🔗
Python-Brücke
pybind11 Typkonvertierung
📊
Plotly/Dash
Interaktive UI Kein Aufwand
Performance
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 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:

  1. Was C-Erweiterungen sind und warum sie existieren
  2. pybind11 zum Erstellen der Python-C++-Brücke
  3. scikit-build-core als modernes Build-Backend
  4. uv build zum lokalen Erstellen von Wheels
  5. cibuildwheel für plattformübergreifende CI-Builds
  6. 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:

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.

🐍 CPython-Architektur
Ihr Python-Code
Python Parser
Konvertiert Text zu AST
Bytecode-Compiler
Kompiliert AST zu .pyc
Python VM
(Interpreter)
◄──►
C-Erweiterung
Modul (.so)
Ihr C-Code!
Betriebssystem / Hardware

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:

  1. Referenzzählung verwalten: Jedes PyObject* hat einen Referenzzähler. Vergessen Sie ihn zu erhöhen oder zu verringern, bekommen Sie Speicherlecks oder Abstürze.

  2. Fehler manuell behandeln: Jeder C-API-Aufruf kann fehlschlagen. Sie müssen Rückgabewerte prüfen und Fehler korrekt propagieren.

  3. Typen manuell konvertieren: PyArg_ParseTuple und Py_BuildValue verwenden Format-Strings, die leicht falsch zu machen sind.

  4. Viel Boilerplate schreiben: Methodentabellen, Moduldefinitionen, Initialisierungsfunktionen…

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


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:

  1. Funktionssignaturen zu introspizieren zur Kompilierzeit
  2. Typkonvertierungscode automatisch zu generieren
  3. 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):

Plattform-Wheels (...-cp312-cp312-win_amd64.whl):

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?

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:

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:

  1. Plattformspezifische Pfade und Flags
  2. Kein Standardweg zum Finden von Abhängigkeiten
  3. Begrenzte CMake-Integration
  4. 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:

  1. Ruft CMake auf, um den Build zu konfigurieren
  2. Kompiliert den C++-Code
  3. 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:

# 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:

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:

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:

  1. Erstellen Sie ein Konto auf pypi.org
  2. Generieren Sie ein API-Token
  3. 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++:

In Python belassen:


11. Zusammenfassung

11.1 Wichtige Erkenntnisse

  1. Python C-Erweiterungen sind native Code-Module, die der Python-Interpreter direkt lädt. Sie sind seit 1991 Teil von Python.

  2. pybind11 macht das Erstellen von Bindings einfach: schreibe normale C++-Funktionen, füge ein paar Zeilen Binding-Code hinzu, fertig.

  3. scikit-build-core ist das moderne Build-Backend: es behandelt CMake, Python-Erkennung und Wheel-Erstellung automatisch.

  4. uv build erstellt Wheels lokal. Das Wheel enthält vorkompilierte Binaries, die Nutzer ohne Compiler installieren können.

  5. cibuildwheel baut Wheels für alle Plattformen in CI. Ein Workflow, Wheels für Windows, macOS und Linux.

  6. uv publish lädt auf PyPI hoch. Mit Trusted Publishing brauchen Sie nicht einmal API-Tokens.

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


12. Reflexionsfragen

  1. Wann würden Sie C++ statt NumPy-Optimierung wählen? Berücksichtigen Sie Speicherlayout, Vektorisierung und Entwicklungszeit.

  2. Was sind die Kompromisse bei der Pflege von Python- und C++-Implementierungen? Denken Sie an Tests, Dokumentation und Konsistenz.

  3. Wie würden Sie entscheiden, welche Funktionen nach C++ portiert werden? Welche Profiling-Tools würden Sie verwenden?

  4. Was passiert, wenn ein Nutzer Ihr sdist anstelle eines Wheels installiert? Wie handhabt scikit-build-core dies?

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

In kommenden Vorlesungen werden wir erkunden:


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:

Python Packaging:

Build-Systeme:

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

Plattformstandards:

14.3 Tiefgehende technische Ressourcen

Python C API (zum Verständnis, nicht für täglichen Gebrauch):

pybind11 Fortgeschrittene Themen:

14.4 Videoressourcen und Tutorials

Konferenzvorträge:

Tutorial-Reihen:

14.5 Bücher und Vertiefende Leitfäden

Empfohlene Bücher:

Online-Leitfäden:

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:

pybind11 Beispiele:

cibuildwheel in Aktion:

14.8 Community und Support

Foren und Q&A:

GitHub Discussions:

Chat:

14.9 Aktuell bleiben

Das Python-Packaging-Ökosystem entwickelt sich schnell. Bleibe auf dem Laufenden:

Blogs und 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                             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk