🔝 Retour au Sommaire
La section précédente a montré comment faire communiquer C++ et Rust via la FFI manuelle — des fonctions extern "C", des types C purs, des pointeurs bruts, et un unsafe omniprésent côté Rust. Cette approche fonctionne, mais elle souffre de trois défauts majeurs : la verbosité (chaque fonction déclarée trois fois), l'absence de vérification de type entre les deux langages (une signature C++ incorrectement reproduite en Rust compile sans erreur), et la limitation aux seuls types C à la frontière.
cxx résout ces trois problèmes simultanément. Développé par David Tolnay (l'un des contributeurs les plus prolifiques de l'écosystème Rust), cxx introduit un langage de description d'interface écrit directement en Rust, dans un bloc #[cxx::bridge]. À partir de cette description unique, cxx génère automatiquement le code C++ et Rust nécessaire des deux côtés de la frontière, avec des vérifications de type à la compilation qui garantissent la cohérence.
Le résultat : un code de glue minimal, des types riches traversant la frontière (String, Vec<T>, Box<T>, UniquePtr<T>), et une surface unsafe réduite au strict nécessaire — le tout géré par l'outil, pas par le développeur.
En FFI manuelle, les déclarations C++ et Rust sont indépendantes. Rien ne garantit qu'elles décrivent la même interface :
// C++ — déclare un int32_t
extern "C" int32_t engine_process(engine_t* e, const uint8_t* data, size_t len);// Rust — déclare un i64 par erreur → compile, crash silencieux à l'exécution
extern "C" {
fn engine_process(e: *mut Engine, data: *const u8, len: usize) -> i64;
}Ce type d'erreur est indétectable au compile-time. Le code compile des deux côtés, produit un binaire, et le bug se manifeste à l'exécution — ou pire, silencieusement, sous forme de corruption de données.
cxx adopte une approche différente. L'interface entre C++ et Rust est décrite une seule fois, dans un bloc #[cxx::bridge] en Rust. À partir de cette description, cxx génère :
- Des headers C++ (
.h) contenant les déclarations C++ correspondantes. - Des fichiers source C++ (
.cc) contenant le code de glue. - Du code Rust contenant les implémentations des wrappers.
Les deux côtés sont générés depuis la même source. Si un type ou une signature est modifié dans le bridge, les deux côtés sont régénérés en cohérence. Si une incompatibilité existe, elle est détectée au compile-time — pas à l'exécution.
#[cxx::bridge] (Rust) ← Source de vérité unique
│
├──→ Code Rust généré (wrappers safe, traits, impls)
│
└──→ Headers C++ générés (declarations, types, fonctions)
│
└──→ Compilé avec le code C++ existant
Le développeur n'écrit jamais de extern "C" manuellement, ni côté C++ ni côté Rust. Il n'écrit jamais de #[no_mangle]. Il ne manipule jamais de pointeurs bruts dans le bridge (bien qu'il puisse le faire en dehors). Toute la mécanique FFI est abstraite.
// include/engine.h — API C++ normale, pas de extern "C"
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <memory>
class Engine {
public:
explicit Engine(const std::string& config_path);
~Engine();
bool load_data(const std::vector<uint8_t>& data);
double compute() const;
std::string status() const;
int32_t error_code() const;
private:
std::string config_path_;
std::vector<uint8_t> buffer_;
double result_ = 0.0;
int32_t last_error_ = 0;
};
// Factory function
std::unique_ptr<Engine> create_engine(const std::string& config_path);// src/engine.cpp
#include "engine.h"
#include <numeric>
Engine::Engine(const std::string& config_path)
: config_path_(config_path) {}
Engine::~Engine() = default;
bool Engine::load_data(const std::vector<uint8_t>& data) {
if (data.empty()) {
last_error_ = -1;
return false;
}
buffer_ = data;
return true;
}
double Engine::compute() const {
if (buffer_.empty()) return 0.0;
double sum = std::accumulate(buffer_.begin(), buffer_.end(), 0.0);
return sum / static_cast<double>(buffer_.size());
}
std::string Engine::status() const {
return buffer_.empty() ? "idle" : "ready";
}
int32_t Engine::error_code() const {
return last_error_;
}
std::unique_ptr<Engine> create_engine(const std::string& config_path) {
return std::make_unique<Engine>(config_path);
}Point fondamental : ce code C++ n'a aucune connaissance de Rust. Pas de extern "C", pas de types C, pas de modifications. C'est une classe C++ ordinaire avec des std::string, des std::vector, un std::unique_ptr et des méthodes.
// src/bridge.rs
#[cxx::bridge]
mod ffi {
// ── Types C++ opaques visibles depuis Rust ──────────────
unsafe extern "C++" {
include!("myproject/include/engine.h");
type Engine;
// Factory function — retourne un unique_ptr
fn create_engine(config_path: &CxxString) -> UniquePtr<Engine>;
// Méthodes sur Engine
fn load_data(self: &Engine, data: &CxxVector<u8>) -> bool;
fn compute(self: &Engine) -> f64;
fn status(self: &Engine) -> String;
fn error_code(self: &Engine) -> i32;
}
}Analysons ce bloc ligne par ligne.
#[cxx::bridge] — l'attribut qui déclenche la génération de code. Le contenu du module ffi n'est pas du Rust normal — c'est un DSL (Domain Specific Language) interprété par le proc-macro cxx.
mod ffi { ... } — le module qui contiendra les types et fonctions générés. Après expansion du macro, ce module contient les wrappers safe autour des appels FFI.
unsafe extern "C++" { ... } — déclare des éléments qui vivent côté C++. Le unsafe indique que cxx fait confiance au développeur pour la justesse des déclarations C++ (les types, les signatures, le fait que Engine existe bien dans le header indiqué). Une fois cette confiance accordée, le code Rust généré est safe.
include!("myproject/include/engine.h") — indique à cxx quel header C++ contient les déclarations. Ce chemin apparaîtra dans le header C++ généré.
type Engine — déclare Engine comme un type C++ opaque. Rust ne connaît ni sa taille ni son layout — il ne peut le manipuler qu'à travers des UniquePtr<Engine>, &Engine ou Pin<&mut Engine>.
fn create_engine(...) -> UniquePtr<Engine> — la factory function. UniquePtr<T> est le type cxx correspondant à std::unique_ptr<T>. La propriété est transférée vers Rust.
fn load_data(self: &Engine, ...) -> bool — une méthode. Le self: &Engine indique que c'est une méthode sur Engine (pas une fonction libre). cxx génère automatiquement le code qui appelle engine.load_data(data) côté C++.
&CxxString et &CxxVector<u8> — les types cxx pour const std::string& et const std::vector<uint8_t>&. Ce ne sont pas des copies — ce sont des références directes vers les objets C++ en mémoire.
// src/main.rs
mod bridge;
use cxx::let_cxx_string;
fn main() {
// Créer une CxxString à partir d'une &str Rust
let_cxx_string!(config = "/etc/myapp/config.toml");
// create_engine retourne un UniquePtr<Engine>
let mut engine = ffi::create_engine(&config);
// Créer un CxxVector<u8>
let data: Vec<u8> = vec![10, 20, 30, 40, 50];
// load_data prend une &CxxVector<u8> — conversion depuis &Vec<u8>
// Note : cette conversion nécessite un petit adaptateur (voir section types)
let result = engine.compute();
println!("Status: {}", engine.status()); // String Rust standard
println!("Result: {}", result);
}Le code applicatif est entièrement safe. Pas de unsafe, pas de pointeurs bruts, pas de CString, pas de vérification manuelle de null.
cxx ne tente pas de convertir automatiquement tous les types entre C++ et Rust. Au lieu de cela, il définit un ensemble de types de pont (bridge types) qui ont une représentation mémoire identique des deux côtés, ou des types opaques qui ne sont manipulables que par référence.
Ces types ont une représentation mémoire connue des deux côtés et peuvent être passés par valeur :
| Type cxx | Côté C++ | Côté Rust | Sémantique |
|---|---|---|---|
i8, i16, i32, i64 |
int8_t, int16_t, int32_t, int64_t |
i8, i16, i32, i64 |
Scalaire, par valeur |
u8, u16, u32, u64 |
uint8_t, uint16_t, uint32_t, uint64_t |
u8, u16, u32, u64 |
Scalaire, par valeur |
f32, f64 |
float, double |
f32, f64 |
Scalaire, par valeur |
bool |
bool |
bool |
Scalaire, par valeur |
String |
rust::String |
String |
Retour Rust → C++ |
&str |
rust::Str |
&str |
Emprunt Rust → C++ |
CxxString |
std::string |
(opaque) | Référence C++ → Rust |
&[T] |
rust::Slice<const T> |
&[T] |
Emprunt Rust → C++ |
&mut [T] |
rust::Slice<T> |
&mut [T] |
Emprunt mutable |
Vec<T> |
rust::Vec<T> |
Vec<T> |
Conteneur Rust |
CxxVector<T> |
std::vector<T> |
(opaque) | Conteneur C++ |
UniquePtr<T> |
std::unique_ptr<T> |
(wrapper) | Propriété exclusive C++ |
SharedPtr<T> |
std::shared_ptr<T> |
(wrapper) | Propriété partagée C++ |
Box<T> |
rust::Box<T> |
Box<T> |
Propriété exclusive Rust |
Result<T> |
Retour avec exception | Result<T> |
Gestion d'erreurs |
Un type déclaré dans unsafe extern "C++" { type Engine; } est opaque pour Rust. Sa taille et son layout sont inconnus. Rust ne peut le manipuler que de trois manières :
UniquePtr<Engine>— propriété exclusive. Quand leUniquePtrest droppé, le destructeur C++ est appelé.SharedPtr<Engine>— propriété partagée via comptage de références atomique.&Engine/Pin<&mut Engine>— emprunts. La durée de vie est garantie par le borrow checker Rust sur le wrapper, pas sur l'objet C++ sous-jacent.
unsafe extern "C++" {
type Engine;
fn create_engine(path: &CxxString) -> UniquePtr<Engine>;
fn compute(self: &Engine) -> f64; // &self → const ref
fn load_data(self: Pin<&mut Engine>, data: &[u8]); // &mut self → mutable ref
}Le Pin<&mut Engine> est l'équivalent cxx d'une référence mutable C++. Pin garantit que l'objet ne sera pas déplacé en mémoire — une contrainte importante car les objets C++ peuvent contenir des pointeurs internes (self-referential structures) qui deviendraient invalides après un déplacement.
On peut aussi exposer des types Rust vers C++ :
#[cxx::bridge]
mod ffi {
// Type Rust opaque, visible depuis C++ comme rust::Box<Processor>
extern "Rust" {
type Processor;
fn create_processor(name: &str) -> Box<Processor>;
fn run(self: &Processor, input: &[u8]) -> Vec<u8>;
}
}Le code C++ généré peut alors créer et utiliser un Processor :
// C++ — code généré automatiquement par cxx
rust::Box<Processor> proc = create_processor("my_proc");
rust::Slice<const uint8_t> input(data, len);
rust::Vec<uint8_t> output = proc->run(input); cxx permet de définir des structs dont le layout est connu des deux côtés :
#[cxx::bridge]
mod ffi {
struct Measurement {
timestamp: i64,
value: f64,
sensor_id: u32,
}
unsafe extern "C++" {
include!("myproject/include/recorder.h");
type Recorder;
fn record(self: Pin<&mut Recorder>, m: &Measurement);
}
extern "Rust" {
fn generate_measurement() -> Measurement;
}
}La struct Measurement est utilisable identiquement des deux côtés — par valeur, par référence, dans des vecteurs. cxx génère la définition C++ correspondante :
// Généré par cxx
struct Measurement {
int64_t timestamp;
double value;
uint32_t sensor_id;
};Les champs autorisés dans les structs partagées sont les types scalaires, String, CxxString, Vec<T>, UniquePtr<T>, Box<T> et d'autres structs partagées.
#[cxx::bridge]
mod ffi {
enum LogLevel {
Debug,
Info,
Warning,
Error,
}
unsafe extern "C++" {
fn set_log_level(level: LogLevel);
}
}Les enums partagées sont représentées comme des enum class en C++ avec des valeurs numériques séquentielles.
Les chaînes sont le type le plus fréquent aux frontières et le plus sujet aux erreurs en FFI manuelle. cxx fournit une abstraction propre.
unsafe extern "C++" {
fn process_name(name: &str); // C++ reçoit rust::Str
fn store_name(name: String); // C++ reçoit rust::String (ownership transféré)
}Côté C++, rust::Str est une vue non-owning sur une chaîne UTF-8 (analogue à std::string_view), et rust::String est une chaîne owning (analogue à std::string, mais allouée par Rust).
// C++ — utilisation des types Rust
void process_name(rust::Str name) {
// name.data() retourne un const char*
// name.size() retourne la taille
// name.operator std::string() convertit en std::string
std::string cpp_name(name);
// ...
}unsafe extern "C++" {
fn get_name(self: &Engine) -> &CxxString; // Emprunt de la std::string C++
fn get_label(self: &Engine) -> String; // Copie dans une String Rust
}&CxxString est une référence directe vers la std::string C++ en mémoire — zéro copie. Pour obtenir une &str Rust :
let name: &CxxString = engine.get_name();
let rust_str: &str = name.to_str().expect("invalid UTF-8"); La conversion to_str() vérifie la validité UTF-8 (une std::string C++ peut contenir des séquences non-UTF-8). Si la chaîne est garantie UTF-8, to_str_unchecked() évite cette vérification.
Quand la signature retourne String (pas &CxxString), cxx génère du code qui copie la std::string C++ dans une String Rust. C'est plus simple mais implique une allocation et une copie.
unsafe extern "C++" {
fn create_engine(path: &CxxString) -> UniquePtr<Engine>;
}Le UniquePtr<Engine> côté Rust encapsule un std::unique_ptr<Engine> C++. Quand le UniquePtr Rust est droppé, le destructeur C++ de Engine est appelé. La propriété est transférée de manière déterministe, sans garbage collector.
{
let engine = ffi::create_engine(&config);
// engine possède l'objet C++
engine.compute();
} // engine droppé ici → std::unique_ptr<Engine> détruit → ~Engine() appeléextern "Rust" {
fn create_processor() -> Box<Processor>;
}Côté C++, rust::Box<Processor> possède l'objet Rust. Quand le Box C++ est détruit, le drop Rust est appelé.
unsafe extern "C++" {
fn get_shared_engine() -> SharedPtr<Engine>;
}Le compteur de références est partagé entre les deux langages. L'objet est détruit quand le dernier SharedPtr (côté C++ ou Rust) est relâché.
Les références &T et Pin<&mut T> dans le bridge sont des emprunts — elles ne possèdent pas l'objet. cxx ne peut pas encoder les lifetimes Rust complètes dans le bridge (les durées de vie des objets C++ ne sont pas vérifiables par le borrow checker), mais il garantit au minimum que les emprunts restent valides pendant la durée de l'appel de fonction.
unsafe extern "C++" {
// &Engine est valide pendant l'appel à compute()
fn compute(self: &Engine) -> f64;
// Retourner une référence est possible mais la durée de vie
// est liée au self (comme reference_internal en pybind11)
fn status(self: &Engine) -> &CxxString;
}cxx permet de déclarer qu'une fonction C++ peut échouer. Côté Rust, le retour est un Result<T, cxx::Exception> :
unsafe extern "C++" {
// La fonction C++ peut lever une exception
fn load_config(self: Pin<&mut Engine>, path: &CxxString) -> Result<()>;
fn compute(self: &Engine) -> Result<f64>;
}Côté C++, si Engine::load_config ou Engine::compute lève une std::exception, cxx l'intercepte, extrait le what() et le transforme en Err(cxx::Exception) côté Rust :
match engine.as_mut().unwrap().load_config(&path) {
Ok(()) => println!("Config loaded"),
Err(e) => eprintln!("Failed: {}", e), // e contient le what() de l'exception
}Le code C++ n'a aucune modification à subir — cxx génère automatiquement le try/catch dans le wrapper.
Dans l'autre sens, une fonction Rust qui retourne un Result est exposée en C++ comme une fonction qui peut lever une rust::Error :
extern "Rust" {
fn validate_data(data: &[u8]) -> Result<bool>;
}
// Implémentation Rust
fn validate_data(data: &[u8]) -> Result<bool, Box<dyn std::error::Error>> {
if data.is_empty() {
return Err("empty data".into());
}
Ok(data.iter().all(|&b| b > 0))
}Côté C++, l'appel peut lever une rust::Error :
try {
bool valid = validate_data(slice);
} catch (const rust::Error& e) {
std::cerr << "Validation failed: " << e.what() << std::endl;
}Ce mécanisme élimine complètement le code de conversion exception ↔ code d'erreur que la FFI manuelle impose (section 43.3.1.5).
Le bridge cxx est bidirectionnel. On peut exposer des fonctions et des types Rust vers C++ dans le même bloc bridge :
#[cxx::bridge]
mod ffi {
// ── Types et fonctions C++ utilisés depuis Rust ─────────
unsafe extern "C++" {
include!("myproject/include/engine.h");
type Engine;
fn create_engine(path: &CxxString) -> UniquePtr<Engine>;
fn compute(self: &Engine) -> f64;
}
// ── Types et fonctions Rust exposés vers C++ ────────────
extern "Rust" {
type Validator;
fn create_validator(strict: bool) -> Box<Validator>;
fn validate(self: &Validator, data: &[u8]) -> Result<bool>;
fn summary(self: &Validator) -> String;
}
}
// Implémentation Rust (en dehors du bloc bridge)
pub struct Validator {
strict: bool,
checks_performed: u64,
}
fn create_validator(strict: bool) -> Box<Validator> {
Box::new(Validator {
strict,
checks_performed: 0,
})
}
impl Validator {
// Les méthodes déclarées dans le bridge comme fn(self: &Validator, ...)
// sont implémentées comme des fonctions libres ou des méthodes classiques
}
fn validate(v: &Validator, data: &[u8]) -> Result<bool, Box<dyn std::error::Error>> {
if data.is_empty() {
return Err("no data".into());
}
if v.strict && data.len() < 10 {
return Err("data too short for strict mode".into());
}
Ok(true)
}
fn summary(v: &Validator) -> String {
format!("Validator(strict={}, checks={})", v.strict, v.checks_performed)
}Côté C++, le Validator Rust est utilisable naturellement :
#include "myproject/src/bridge.rs.h" // Header généré par cxx
void run_validation() {
rust::Box<Validator> v = create_validator(true);
uint8_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
rust::Slice<const uint8_t> slice(data, sizeof(data));
try {
bool ok = v->validate(slice);
std::cout << "Valid: " << ok << std::endl;
} catch (const rust::Error& e) {
std::cerr << "Validation error: " << e.what() << std::endl;
}
rust::String s = v->summary();
std::cout << std::string(s) << std::endl;
}# Cargo.toml
[package]
name = "myproject"
version = "0.1.0"
edition = "2021"
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"Le script de build compile le code C++ et le code de glue généré par cxx :
// build.rs
fn main() {
cxx_build::bridge("src/bridge.rs")
.file("src/engine.cpp") // Code C++ métier
.include("include") // Répertoire des headers
.std("c++20") // Standard C++
.flag_if_supported("-Wall")
.flag_if_supported("-Wextra")
.compile("myproject");
println!("cargo:rerun-if-changed=src/bridge.rs");
println!("cargo:rerun-if-changed=src/engine.cpp");
println!("cargo:rerun-if-changed=include/engine.h");
}cxx_build::bridge("src/bridge.rs") parse le bloc #[cxx::bridge], génère le code C++ de glue, et retourne un objet cc::Build standard (du crate cc) auquel on ajoute les fichiers source C++ et les options de compilation.
cargo build --release
# cxx génère le code de glue
# cc compile engine.cpp + le code de glue
# rustc compile le code Rust
# Le tout est lié en un binaire uniquePour les projets où CMake est le build system principal :
cmake_minimum_required(VERSION 3.24)
project(myproject LANGUAGES CXX)
include(FetchContent)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.5
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(Corrosion)
# Importer le crate Rust (qui contient le bridge cxx)
corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml)
# L'exécutable C++ lie avec la bibliothèque Rust
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE myproject) myproject/
├── CMakeLists.txt # Si CMake est le driver principal
├── include/
│ └── engine.h # Headers C++ (code métier)
├── src/
│ ├── engine.cpp # Implémentation C++ (code métier)
│ ├── main.cpp # Point d'entrée C++ (optionnel)
│ ├── bridge.rs # Bridge cxx (source de vérité)
│ ├── lib.rs # Module Rust principal
│ └── main.rs # Point d'entrée Rust (optionnel)
├── Cargo.toml
└── build.rs
Un projet peut contenir plusieurs blocs #[cxx::bridge] dans différents fichiers. Chaque bridge génère son propre ensemble de wrappers :
// src/lib.rs
mod engine_bridge; // src/engine_bridge.rs contient un #[cxx::bridge]
mod network_bridge; // src/network_bridge.rs contient un autre #[cxx::bridge]
mod storage_bridge; // src/storage_bridge.rs contient un troisième // build.rs
fn main() {
cxx_build::bridges(["src/engine_bridge.rs",
"src/network_bridge.rs",
"src/storage_bridge.rs"])
.file("src/engine.cpp")
.file("src/network.cpp")
.file("src/storage.cpp")
.include("include")
.std("c++20")
.compile("myproject");
}cxx supporte les namespaces C++ via l'attribut namespace :
#[cxx::bridge(namespace = "mylib::core")]
mod ffi {
unsafe extern "C++" {
include!("mylib/core/engine.h");
type Engine; // → mylib::core::Engine en C++
fn create_engine(path: &CxxString) -> UniquePtr<Engine>;
}
}CxxVector<T> implémente les traits Rust standard pour l'itération :
let items: &CxxVector<f64> = engine.get_data();
// Itération
for value in items.iter() {
println!("{}", value);
}
// Accès par index
let first = items.get(0).unwrap();
// Taille
println!("Count: {}", items.len());Vec<T> (Rust) et CxxVector<T> (C++) sont des types distincts avec des allocateurs différents. La conversion implique une copie :
// CxxVector → Vec (copie)
let cpp_vec: &CxxVector<f64> = engine.get_data();
let rust_vec: Vec<f64> = cpp_vec.iter().copied().collect();
// Pour éviter la copie, préférer passer des slices (&[T]) dans le bridgeLa recommandation de cxx est de privilégier les slices (&[T]) dans les signatures de bridge quand les données ne sont que lues. Les slices ne copient rien — elles sont des vues sur la mémoire existante.
cxx supporte les pointeurs de fonctions C dans le bridge :
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
type Engine;
fn set_callback(self: Pin<&mut Engine>, cb: fn(i32, &CxxString));
}
}Le fn(i32, &CxxString) est un pointeur de fonction (pas une closure). Pour des callbacks avec état, le pattern recommandé est d'exposer un type Rust opaque que le C++ stocke et invoque via le bridge.
cxx est puissant mais volontairement limité. Les restrictions suivantes sont des choix de design, pas des bugs.
Templates C++. On ne peut pas déclarer type HashMap<K, V> dans le bridge. Chaque instanciation concrète doit être déclarée individuellement (type StringIntMap), avec un typedef côté C++ si nécessaire.
Héritage. Le bridge ne modélise pas les hiérarchies de classes C++. On ne peut pas déclarer que Circle hérite de Shape. Chaque type est opaque et indépendant. Le polymorphisme dynamique (vtable) doit être géré manuellement, typiquement en exposant les méthodes de la classe de base sur chaque type concret.
Surcharge de fonctions. Deux fonctions avec le même nom mais des signatures différentes ne peuvent pas coexister dans un même bridge. La solution est de donner des noms Rust distincts avec l'attribut #[cxx_name] :
unsafe extern "C++" {
#[cxx_name = "process"]
fn process_int(self: &Engine, value: i32);
#[cxx_name = "process"]
fn process_float(self: &Engine, value: f64);
}Lambdas et closures. Seuls les pointeurs de fonctions (fn(...)) traversent le bridge. Les std::function et les closures Rust avec capture ne sont pas supportés directement.
Types génériques dans les structs partagées. Les structs partagées ne supportent pas les paramètres de type. Chaque struct doit être concrète.
Pour les API C++ volumineuses avec de l'héritage profond, des templates omniprésents et de la surcharge systématique, cxx seul peut s'avérer trop contraignant. C'est le cas d'usage d'autocxx (section 43.3.3), qui adopte une approche par génération automatique à partir des headers.
L'autre option est de créer une couche d'adaptation C++ — un ensemble de fonctions et classes C++ simplifiées qui encapsulent l'API complexe et exposent une interface adaptée aux contraintes de cxx. Ce pattern "façade C++ pour cxx" est courant dans les projets industriels.
| Aspect | FFI manuelle (43.3.1) | cxx |
|---|---|---|
| Source de vérité | Aucune (déclarations dupliquées) | #[cxx::bridge] unique |
| Vérification de type | Aucune entre C++ et Rust | Compile-time bidirectionnelle |
| Types à la frontière | Types C uniquement | String, Vec, UniquePtr, structs, enums |
| Gestion des erreurs | Codes d'erreur manuels | Result ↔ exceptions automatique |
Surface unsafe |
Omniprésente côté Rust | Limitée au bloc unsafe extern "C++" |
| Génération de code | Manuelle (ou bindgen) | Automatique par proc-macro |
| Support templates | Via instanciation C manuelle | Non supporté |
| Support héritage | Via handle opaque | Non supporté |
| Build | CMake + Cargo séparés | cxx-build intégré à Cargo |
cxx transforme l'interopérabilité C++/Rust d'un exercice de plomberie FFI fragile en une interface déclarative vérifiée au compile-time. Pour les interfaces bien définies — les cas les plus courants dans une stratégie de migration progressive — c'est l'outil de référence en 2026.
📎 La section suivante (43.3.3) présente
autocxx, qui adopte l'approche complémentaire : au lieu de décrire manuellement l'interface, il parse directement les headers C++ et génère automatiquement les bindings Rust.