Bild von Teil 1 - Interoperabilität zwischen C++ und Rust: Grundlagen

05. Nov 2025

Teil 1 - Interoperabilität zwischen C++ und Rust: Grundlagen

Dieser Artikel stellt den ersten Beitrag zu einer fortlaufenden Serie über die Migration und Integration von C++ in Rust dar.

Viele Unternehmen stehen derzeit vor derselben Frage: Soll eine bestehende C++-Codebasis durch eine sichere Rust-Implementierung teilweise oder langfristig vollständig ersetzt werden? Gerade Hersteller mit hohen Sicherheits- oder Compliance-Anforderungen (z.B. im Kontext des EU Cyber Resilience Act) geraten zunehmend unter Druck und sind bereit gezielt in Massnahmen zur Erhöhung der Softwaresicherheit zu investieren. Rust bietet Sicherheitsgarantien zur Compile-Zeit, die andere Sprachen wie C++ nur durch Disziplin und Erfahrung der Programmierer erreichen können. Durch ein konservatives Ownership- und Borrowing-Modell schränkt Rust die Programmierfreiheit bewusst ein – mit dem Ziel, ganze Klassen von Fehlern wie ungültige Speicherzugriffe und Race-Conditions bereits beim Kompilieren auszuschliessen. Gerade diese Fehler treten in C++ häufig nur sporadisch zur Laufzeit auf und sind schwer reproduzierbar. Indem Rust solche Probleme frühzeitig verhindert, steigert es nachweisbar die Zuverlässigkeit und somit auch das Vertrauen in das Produkt.

Die Migration von C++ nach Rust ist jedoch auch mit Unbekannten verbunden: Lässt sich der C++-Code mit einem Tool zuverlässig auf Rust übersetzen? Gibt es verwendete C++-Konstrukte, die gar keine direkte Entsprechung in Rust haben? Oder verhalten sich scheinbar äquivalente Objekte in C++ und Rust zur Laufzeit tatsächlich identisch? Solche Unbekannten erschweren eine realistische Aufwandsschätzung für eine Migration und können sogar zum Scheitern des Vorhabens führen.

Eine praxistaugliche Alternative ist deshalb die schrittweise Migration, bei welcher nur isolierte Komponenten nach Rust portiert werden und über C-Schnittstellen mit dem Legacy-C++-Code kommunizieren. Besteht das Programm aus gut abgrenzbaren Komponenten, bietet der Ansatz mehrere Vorteile:

  • Priorisierung: Sicherheitskritische, komplexe oder nicht aktiv gewartete Komponenten können bewusst später migriert werden, wenn Zeit und Ressourcen verfügbar sind.
  • Risiko-Minimierung: Das Risiko für ein Misslingen einer Migration sinkt und nach jeder erfolgreichen Teilmigration steigt die Planbarkeit weiterer Schritte.
  • Akzeptanz: Eine Teilmigration ist firmenpolitisch weitaus weniger heikel als eine Vollmigration.

Allerdings ist dieser Ansatz nicht kostenlos. Die C-Schnittstelle limitiert den Datenaustausch zwischen den Sprachen, was oft eine nicht triviale Umstrukturierung der Datenmodelle erfordert. Zudem reduziert sie die Entwicklungsgeschwindigkeit, da die Schnittstelle als sprachübergreifender Vertrag gilt, dessen Änderung mit erheblichem Abstimmungs- und Anpassungsaufwand verbunden ist.

Die fortlaufende Serie über die Migration und Integration von C++ in Rust führt unsere allgemeine Artikel-Serie mit der letzten Ausgabe  Rust – Moderne Softwareentwicklung mit Sicherheit und Performance weiter. Dieser erste Teil beleuchtet die technischen Grundlagen der Interoperabilität zwischen C++ und Rust und zeigt, wie sie als Mittel für eine schrittweise Migration genutzt werden kann.

Das Sanduhr-Modell

Der C++- und Rust-Code kommunizieren miteinander über eine C-kompatible Schnittstelle. In C++ entspricht diese Schnittstelle einer Teilmenge der eigenen Standard-Sprachkonstrukte, während sie in Rust über das Modul std::ffi (Foreign Function Interface) bereitgestellt wird. Dabei müssen Datentypen, die in einer Sprache definiert und in der anderen verwendet werden sollen, zunächst auf C-kompatiblen Grundtypen reduziert (über eine sogenannte FFI-Bridge) und in der anderen Sprache wieder zusammengebaut werden. Zum Beispiel muss ein std::vector in C++ als C-Array uminterpretiert werden, um schliesslich in Rust zu einem Vec wieder zusammengesetzt zu werden. Dieses Prinzip wird als Sanduhr-Modell (hourglass model) bezeichnet (wie im Bild unten dargestellt): Die C-Schnittstelle bildet den schmalen, gemeinsamen Kern ("Hals") zwischen den beiden Sprachen, während die beiden Aussenseiten aus den komplexeren Datentypen von Rust und C++ bestehen.

Mit der C-Schnittstelle des Sanduhr-Modells wird ein anwendungsspezifisches Application Binary Interface (ABI) definiert. Das ABI beschreibt, wie Funktionen und Datenstrukturen auf Binärebene zwischen Programmen ausgetauscht werden, welche in unterschiedlichen Sprachen kompiliert wurden. Es umfasst unter anderem die Aufrufkonventionen, die Speicheranordnung von Datenstrukturen sowie die Namenskonventionen (Name-Mangling). Über dieses ABI können Funktionen, die in einer Sprache implementiert wurden, von der anderen aufgerufen werden.

Im Gegensatz dazu beschreibt ein Application Programming Interface (API) die Schnittstelle auf Quellcode- und nicht auf Binärebene. Diese Unterscheidung wird deutlich, wenn man den Build-Prozess betrachtet (siehe Bild oben): Rust- und C++-Compiler übersetzen ihren jeweiligen Quellcode unabhängig voneinander in Objektdateien (.o). Erst im zweiten Schritt werden diese Objektdateien durch den Linker zu einem gemeinsamen Binärprogramm zusammengefügt – dieser Vorgang erfolgt ohne direkte Kontrolle durch einen der Compiler. Während die Verlinkung von Objektdateien innerhalb einer Sprache vom jeweiligen Compiler garantiert korrekt funktioniert, ist die Verknüpfung zwischen Rust- und C++-Objekten fehleranfälliger. Hier muss sichergestellt werden, dass beide Seiten das gleiche ABI einhalten – sonst kann es zu undefiniertem Verhalten, Speicherfehlern oder Abstürzen kommen.

Cargo ist das Build- und Packet-Verwaltungssystem von Rust – vergleichbar mit CMake für C++. Der Hauptzweck von Cargo ist das Kompilieren, Verwalten von Abhängigkeiten und Veröffentlichen von Rust-Projekten in Form von sogenannten Crates. Es kann aber auch Tools wie CMake integrieren, um C++-Quellcode zu kompilieren. So kann der ganze Rust- und C++-Build-Prozess durch einen einzigen Aufruf von Cargo orchestriert werden. Darin enthalten ist das Kompilieren von C++-Code (z.B. via CMake), das Kompilieren von Rust-Code und das Verlinken der Objektdateien.

Werkzeuge im Überblick

Eine C-kompatible Schnittstelle zwischen zwei Sprachen zu implementieren, ist möglich, aber ohne Hilfsmittel nur schwer zuverlässig und fehlerfrei umzusetzen. Dies liegt daran, dass die Speicheranordnung von C-Typen nicht vollständig standardisiert ist, sondern vom Compiler, Zielplattform und anderen Faktoren abhängt. So kann es vorkommen, dass vermeintlich identische C-kompatible Datenstrukturen in Rust und C++ unterschiedlich im Speicher angeordnet werden – mit potenziell fatalen Folgen. Um solche Fehler zu vermeiden, existieren verschiedene Werkzeuge, die das Definieren von Schnittstellen automatisieren und teilweise sogar deren Korrektheit statisch überprüfen. Für die Interoperabilität zwischen Rust und C++ stehen heute mehrere Ansätze zur Verfügung – je nachdem, ob der Fokus auf C-Kompatibilität oder einer idiomatischen Nutzung beider Sprachen liegt.

bindgen und cbindgen

Das bindgen-Crate generiert aus existierenden C-Header-Dateien (.h) automatisch die entsprechenden Rust-Funktions- und Typdefinitionen. In der Praxis wird C++-Code oft zunächst durch einen dünnen C-Wrapper gekapselt, aus dem bindgen die Rust-Schnittstelle ableitet. Darauf aufbauend kann eine sichere, idiomatische Rust-API erstellt werden, die ohne manuelles Übersetzen der Funktionssignaturen auskommt.

Das cbindgen-Crate funktioniert in die entgegengesetzte Richtung: Es erzeugt aus Rust-Code automatisch eine C-kompatible Header-Datei, die von C- oder C++-Projekten eingebunden werden kann. So lassen sich Rust-Bibliotheken direkt aus C++ heraus nutzen – etwa zur schrittweisen Integration von Rust-Komponenten in bestehende Systeme.

cxx

Das cxx-Crate verfolgt einen grundlegend anderen Ansatz. Anstatt eine bestehende C-Schnittstelle zu übersetzen, wird die gemeinsame Schnittstelle deklarativ in Rust definiert – mithilfe eines speziellen Makros. Aus dieser Beschreibung generiert cxx automatisch die passenden Bindings sowohl auf Rust- als auch auf C++-Seite. Dadurch ist sichergestellt, dass die Schnittstellen immer konsistent bleiben – auch wenn sie sich weiterentwickeln - und potenzielle Fehler werden bereits zur Kompilierzeit erkannt.

cxx versteht gängige Standardtypen beider Sprachen (z. B. String, Vec, UniquePtr, etc.) und kann diese sicher zwischen Rust und C++ abbilden. Zudem erzwingt cxx Regeln, die unbeabsichtigte Datenveränderungen durch die jeweils andere Sprache verhindern. So wird beispielsweise unterbunden, dass Datenstrukturen by value von C++ nach Rust übergeben werden, wenn diese durch das Move-Verhalten von Rust zu unerwarteten Modifikationen führen könnten. Darüber hinaus erlaubt cxx über die Verknüpfung mit C++-Template-Instanziierungen eine sichere Nutzung solcher generischen Typen auch auf Rust-Seite – ohne Speicher- oder Ownership-Verletzungen.

Fazit

Die Interoperabilität zwischen Rust und C++ bietet ein grosses Potenzial, bestehende Systeme schrittweise zu modernisieren und gleichzeitig sicherer zu gestalten. Die dafür notwendige Sprachschnittstelle ist jedoch fehleranfällig und sollte – wo immer möglich – automatisiert erzeugt werden, etwa mit Werkzeugen wie bindgen oder cxx, die im Fall von cxx sogar eine statische Überprüfung der Schnittstelle durchführen. Trotz dieser Unterstützung bleiben gewisse Einschränkungen beim Datenaustausch zwischen den Sprachen bestehen – etwa beim Zugriff auf Stack-Variablen oder bei der Nutzung asynchroner Funktionen. Wie sich diese Limitierungen im Detail auswirken und welche Lösungsansätze es gibt, beleuchten wir im nächsten Teil dieser Serie.


Schliessen
Stamp Icon-Print Icon-Clear
S
M
L
XL
XXL