Brückenbauer

Wie Sie Rust in «C/C++»-Projekten einbauen können

Schnell geht man davon aus: Rust und C oder gar C++ sind nicht vereinbar. Unser Rust-Experte Oliver With erläutert in diesem Artikel, wie es dennoch möglich ist – und wie damit ein Projekt zukunftsfähig und schlank sein kann.

02.06.2025Text: Urs Häfliger0 Kommentare
Header Blogserie Rust Brueckenbauer C zu Rust

Weil die Vorteile von Rust gegenüber C/C++ überzeugen, möchte ein Unternehmen sein Software-Produkt zu Rust migrieren. Oft stellt sich dabei die Frage, wie die Migration graduell vonstattengehen kann. Denn es gibt häufig ein paar Randbedingungen:

  • Ein komplettes Neuschreiben des Codes ist nicht machbar.
  • Das Team muss sich erst mal an Rust gewöhnen.
  • Unser Produkt wird als ein einziges Binary kompiliert.

Damit bleibt die Option eine Rust-Library in ein bestehendes C/C++ mittels Foreign Function Interface (FFI) zu integrieren. So können wir Rust graduell in unser Produkt einführen.

Ziel: Eine Rust-Library im «C++»-Look

Wir haben ein bestehendes «C++»-Projekt, das aus verschiedenen Libraries besteht. Eine dieser Libraries wollen wir nun in Rust schreiben und in C++ verwenden.

  • Die Rust-Library soll sich gegen aussen wie eine «C++»-Library verhalten.
  • Die Rust-Library lässt sich einfach in das bestehende CMake-Projekt einbinden, sodass Rust-spezifische Eigenheiten gekapselt bleiben.
  • Die Rust-Library wollen wir später in einem puren Rust-Kontext wiederverwenden. Wir wollen also keinen FFI-Code in der Rust-Library.
Rust Prgramming Language Quiz
Quiz

Testen Sie Ihr Rust-Wissen!

Rust erobert die Herzen der Entwickler – vielleicht auch Ihres. Wie fit sind Sie aber auf Rust? Sehr? Dann stellen Sie Ihr Wissen auf die Probe!
Zum Quiz

Ansatz: C-ABI als Brückenbauer

Wir entwickeln eine pure Rust-Library mit ihren Tests und packen diese Library in eine Wrapper-Library, um sie von C++ aus verwenden zu können.

Bild1 Blogserie Rust Brueckenbauer
Abbildung 1: Ein C++ Projekt besteht aus einer Reihe von Libraries. Neu kommt eine Rust-Library dazu, die ein C++ Interface zur Verfügung stellt.

Damit C++ überhaupt Rust-Funktionen verwenden kann und umgekehrt, nützen wir das C Application Binary Interface (C-ABI). Weil nur die C-ABI stabil ist und deshalb von beiden Sprachen verwendbar ist. Weder die C++ ABI noch die Rust ABI sind stabil.

Das Vorgehen, wie Rust und C++ kommunizieren, kann man anhand einer Brückenanalogie illustrieren:

Bild2 Blogserie Rust Brueckenbauer
Abbildung 2: Von der puren C++ Welt (ganz links) werden Funktionsaufrufe in die C-ABI verpackt und über die Brücke gesendet. Auf der Rust-Seite werden die C-Funktionsaufrufe in die pure Rust-Welt übersetzt.

Die C-ABI bildet die Brücke. Vom «C++»-Wrapper werden Funktionsaufrufe in C-Funktionsaufrufe übersetzt so über die Brücke gesendet. Auf der Rust-Seite werden die Funktionsaufrufe an die entsprechenden Rust-Funktionen weitergeleitet.

Implementation der C-ABI-Brücke

Auf der «C++»-Seite stellen wir die Rust-Library als «C++»-Headerfile einer Klasse zur Verfügung. Dabei achten wir darauf, dass mit Objekten dieser Klasse nur sinnvolle Operationen ausgeführt werden können. Deshalb ist der Copy-Constructor und der Copy-Assignment-Constructor in dieser Klasse gelöscht, weil diese Operationen auf der Rust-Seite nicht unterstützt sind. Diese Klasse hat nur einen Pointer (eines opaken Typs) auf das eigentliche Objekt (ein unique_ptr auf das Objekt ist auch möglich, wobei man den Deleter aber selbst implementieren muss). Zusätzlich kann ein Objekt nur über eine Fabrikfunktion (New) instanziiert werden.

Bild3 Blogserie Rust Brueckenbauer
Abbildung 3: «C++»- Wrapper für die Rust-Library. Die Konstruktoren sind so angepasst, dass die Operationen für den entsprechenden Typ in Rust sinnvoll sind. Als einzigen Member enthält die Klasse einen Pointer auf den entsprechenden Typen in Rust.

Für die Implementation dieser Klasse wird die Signatur der Brückenfunktion (im extern “C” Block) benötig. Die Fabrikfunktion “New” reicht ihre Argumente (mit ein paar Konversionen) über die Brücke (person_new(..)). Der Rückgabewert von person_new() ist ein Pointer auf das Objekt, das in Rust kreiert wurde. Mit diesem Pointer als Member-Variable wird das C++ Objekt konstruiert.

Damit die Wrapper-Klasse die Lebensdauer des zugrundeliegenden Rust-Typs sinnvoll verwalten kann, müssen wir den Destruktor von Hand implementieren. Dafür wird ein Funktionsaufruf über die Brücke benötigt.

Bild4 Blogserie Rust Brueckenbauer
Abbildung 4: Die Brückenfunktionen sind auf der Rust-Seite implementiert und werden hier in der Definition der C++ Wrapper-Klasse verwendet.

Auf der Rust-Seite der Brücke sind die C-ABI-Brückenfunktionen definiert. Die Brückenfunktionen sind normaler Rust-Code, wobei aber die Funktionssignatur der C-ABI genügt. Im Fall der Funktion “person_new()” werden wiederum die Argumente konvertiert und dann die Fabrikfunktion des Rust-Structs aufgerufen. Diese Fabrikfunktion (Person::new(..)) stammt aus der puren Rust-Welt. Der Rust-Struct wird auf dem Heap angelegt und ein Raw-Pointer darauf zurück in die «C++»-Welt gereicht.

Bild5 Blogserie Rust Brueckenbauer
Abbildung 5: Implementierung der Brückenfunktion in Rust. Das Macro "#[no_mangle]” und die Keywords "extern "C" erreichen, dass diese Funktion eine C-ABI kompatible Signatur hat.

Damit sind C++ und Rust über die C-ABI-Brücke verbunden. Analog zu obigem Vorgehen werden die anderen Funktionen, die mit dem Rust-Objekt interagieren, implementiert.

Mit diesem Ansatz können wir die gesetzten Ziele einer sauberen Kapselung des Rust-Codes und der nahtlosen Integration in ein «C++»-Projekts erreichen.

Eine Rennstrecke mit zwei Motorradfahren im Vordergrund, die sich ein Duell liefern, und weiteren Fahrern im Hintergrund.
Ersetzt Rust bald C++?

Das Rennen der Programmiersprachen

C++ dominiert seit Jahrzehnten die Programmierwelt, doch Rust ist auf dem Vormarsch. Erfahren Sie in diesem Blogbeitrag, wer gewinnen könnte.
Mehr erfahren

Alternative zur C-ABI-Brücke: cbindgen

Das geschilderte Vorgehen strebt eine strikte Kapselung der Rust- und der «C++»-Welt an. Der Nachteil ist, dass wir die C-Funktionssignaturen in der «C++»-Implementation von Hand schreiben müssen.

Wenn eine weniger strikte Kapselung des Codes akzeptabel ist, können auch direkt Rust-Funktionen und Rust-Structs zur Verwendung in C++ zur Verfügung gestellt werden. Die Funktionssignaturen können in diesem Fall mit dem Tool cbindgen generiert werden.

Alternative – oder: einfach bei C bleiben

Wenn Rust in ein C-Projekt eingebunden werden soll, entfällt die Möglichkeit, eine Wrapper-Klasse zu erstellen. Das Vorgehen wird vereinfacht, weil man direkt die von Rust importierten Brückenfunktionen verwenden kann. Das funktioniert technisch gesehen problemlos. Der Zustand wird aber nicht mehr automatisch von der Wrapper-Klasse verwaltet, sondern muss vom verwendenden Code manuell verwaltet werden.

Bild6 Blogserie Rust Brueckenbauer
Abbildung 6: In einem C-Projekt können die von Rust importierten Brückenfunktionen direkt verwendet werden.
Icon Rakete animiert rot
Rust Transition Service

Sicher in die Zukunft

Die Softwareentwicklung verändert sich rasant. Unser Rust Transition Service unterstützt Ihr Unternehmen dabei, Ihre Software sicher, effizient und zukunftsfähig zu gestalten.
Jetzt entdecken

Build-System: Auf die Codezeile kommt’s an

Wir erwarten von der Rust-Library auch, dass sie sich bezüglich des Builds leicht in ein C++ Projekt einfügen lässt.

Aus der Sicht des Top-Level-Projekts wird die Rust-Library wie jede andere statische Library hinzugefügt mit zwei Zeilen CMake-Code:

add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/crates/person-cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE person_bridge_cpp)

Zur Integration einer Rust-Library in CMake hilft das Tool «Corrosion». Im Prinzip ist Corrosion eine Sammlung von Scripts, die das Build-Tool von Rust aufrufen und die produzierten Artefakte für CMake verwendbar macht.

Diese Funktion erledigt die Hauptarbeit: Das Rust-Projekt wird gebaut und in CMake importiert:

corrosion_import_crate(MANIFEST_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Cargo.toml)

Mit diesen zwei Zeilen wird die Wrapper Library erstellt und die mit Corrosion gebaute Rust-Library dazu gelinkt:

add_library(${PROJECT_NAME} STATIC src/lib.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE person_bridge_rs)

Das C++ Header-File wird den Konsumenten zur Verfügung gestellt:

target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

Abschluss: Höherer Aufwand für schlankere Architektur

Um eine Rust-Library in ein C/C++ Projekt einzubinden, gibt es diverse Ansätze. Mit dem geschilderten Vorgehen erreichen wir unsere Ziele einer nahtlosen Integration eines Rust-Projekts. Die beiden Teile bleiben aber voneinander getrennt und kommunizieren nur über wohldefinierte Interfaces. Dies erfordert zwar ein wenig mehr Aufwand, dient aber einer schlanken Architektur, die sich längerfristig auszahlt.

Der Experte

Oliver With

Oliver With ist Spezialist für Embedded Software. Als Senior-Entwickler ist er überzeugt, dass eng zusammenarbeitende Teams komplexe Probleme am besten lösen. Kreativität in der Lösungsfindung vereint er mit Qualität in der Entwicklung, um erfolgreiche Produkte zu schaffen. Er ist Rust-Enthusiast, weil es mit Rust erstmals eine Sprache gibt, die Sicherheit, Performance, Akzeptanz in der Industrie und Ergonomie für Entwickler kombiniert. 

Unser Wissen im Abo

Einfach in der Handhabung

Rust: Werkzeuge für ein sicheres Dependency Management

Rust
Vorbereitet für den Change

Erfolgreiche Einführung von Rust in Ihrem Team

Rust
Konzept

Wie Sie in Rust ein leichtgewichtiges Error-Handling erreichen

Rust

Attention!

Sorry, so far we got only content in English for this section.