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.
Quiz
Testen Sie Ihr Rust-Wissen!
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.

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:

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.

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.

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.

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.
Ersetzt Rust bald C++?
Das Rennen der Programmiersprachen
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.

Rust Transition Service
Sicher in die Zukunft
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.