From 58223db65fdf68b541ed1154764da3124c568497 Mon Sep 17 00:00:00 2001 From: "Brian R. Bondy" Date: Fri, 22 May 2026 21:11:59 -0400 Subject: [PATCH] Add best practice: convert rust::Str/rust::String at the C++ boundary (#36614) * Add best practice: convert rust::Str/rust::String at the C++ boundary --------- Co-authored-by: Brian Johnson <34129+bridiver@users.noreply.github.com> --- docs/best-practices/coding-standards-apis.md | 177 +++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/docs/best-practices/coding-standards-apis.md b/docs/best-practices/coding-standards-apis.md index 061bfe25bfd..7db57709273 100644 --- a/docs/best-practices/coding-standards-apis.md +++ b/docs/best-practices/coding-standards-apis.md @@ -1256,3 +1256,180 @@ base::ReadUnicodeCharacter( base::ReadUnicodeCharacter( base::as_string_view(data), &i, &code_point); ``` + +--- + + + +## ✅ String Usage in Rust/C++ FFI + +**Pick FFI string types by data flow (borrow vs. own) and payload size (standard vs. large).** Rust's `String`/`Vec` and C++'s `std::string`/`std::vector` use different allocators and memory layouts, so the wrong choice causes hidden copies, lifetime bugs, or container overhead. Sections below cover each case with examples. + +### Rust → C++ read-only text — `&str` (maps to `rust::Str`) + +Zero-copy. Rust retains ownership; C++ reads as a view. Use for logging, printing, parsing. + +```rust +// ❌ WRONG - forces C++ container overhead for a borrow +unsafe extern "C++" { + fn log_event(name: &CxxString); +} + +// ✅ CORRECT - zero-copy borrow +unsafe extern "C++" { + fn log_event(name: &str); +} +``` + +```cpp +void LogEvent(rust::Str name) { + // Treat rust::Str as std::string_view — read only, do not store. + VLOG(1) << base::RustStrToStringView(name); +} +``` + +### C++ → Rust read-only text — `&str` (maps to `rust::Str`) + +Zero-copy. Rust receives a native `&str` it can search, slice, or print immediately. Use for validation, key lookup. + +```rust +// ❌ WRONG - bridge type leaks into idiomatic Rust code +fn is_valid_key(key: &CxxString) -> bool; + +// ✅ CORRECT - native &str on the Rust side +fn is_valid_key(key: &str) -> bool { + KNOWN_KEYS.contains(&key) +} +``` + +```cpp +bool valid = ffi::is_valid_key(key_string_view); // implicit conversion +``` + +### Rust → C++ ownership, standard text — `String` (maps to `rust::String`) + +Container move; no text copy. C++ owns the `rust::String` for the scope of the call. **Deep-copy into `std::string` to preserve past the function's return** — never store `rust::String` as a class member. + +```rust +// ✅ Return owned text by value +fn build_report() -> String { + format!("report: {}", compute()) +} +``` + +```cpp +// ❌ WRONG - bridge type held past FFI call; ties C++ class to Rust +// allocator and bridge layout. +class Reporter { + rust::String last_report_; +}; +void Reporter::Refresh() { + last_report_ = ffi::build_report(); +} + +// ✅ CORRECT - deep-copy into std::string at the boundary +class Reporter { + std::string last_report_; +}; +void Reporter::Refresh() { + rust::String r = ffi::build_report(); + last_report_.assign(r.data(), r.length()); +} +``` + +### Rust → C++ ownership, MASSIVE payload — `Box>` (maps to `rust::Box>`) + +Zero-copy. Heap pointer moves to C++; C++ wraps bytes in `std::string_view` or `base::span`. Rust's allocator cleans up when `Box` drops. Use for file buffers, JSON blobs, streaming payloads. + +```rust +// ❌ WRONG - container copy through rust::String layout +fn produce_payload() -> String; + +// ✅ CORRECT - pointer move, zero-copy +fn produce_payload() -> Box>; +``` + +```cpp +void Consume(rust::Box> buf) { + base::span bytes(buf); + // ...consume bytes. Rust deallocator runs when buf drops. +} +``` + +### C++ → Rust ownership, standard text — `String` (maps to `rust::String`) + +Deep copy into Rust heap. C++ builds text, copies into `rust::String`, drops its local copy. Rust gains un-aliased control of an idiomatic container. + +```rust +// ❌ WRONG - borrowing CxxString into long-lived Rust state ties Rust's +// lifetime to a C++ container. +fn store_name(name: &CxxString); + +// ✅ CORRECT - C++ deep-copies at the call site +fn store_name(name: String); +``` + +```cpp +std::string computed = base::StrCat({prefix, suffix}); +ffi::store_name(rust::String(computed)); // deep copy into Rust heap +``` + +### C++ → Rust ownership, MASSIVE payload — `UniquePtr` + +Zero-copy pointer move. Allocation context stays C++-side; `cxx` invokes the C++ destructor when Rust drops the `UniquePtr`. Use for ingesting large blobs. + +```rust +// ❌ WRONG - forces deep copy of large buffer +fn ingest(blob: String); + +// ✅ CORRECT - zero-copy pointer move +fn ingest(blob: UniquePtr); +``` + +```cpp +auto blob = std::make_unique(LoadLargeFile()); +ffi::ingest(std::move(blob)); // ownership transfers to Rust +``` + +### Collections of strings + +Same borrow-vs-own split. `&CxxVector` is almost never the right answer. + +**Rust → C++ read-only array — `&[&str]`** + +Zero-copy, zero-allocation. No C++ container overhead. + +```rust +// ❌ WRONG - C++ container overhead for a borrow +unsafe extern "C++" { + fn process(keys: &CxxVector); +} + +// ✅ CORRECT +unsafe extern "C++" { + fn process(keys: &[&str]); +} +``` + +**C++ → Rust ownership, large — `UniquePtr>`** + +Container lifetime owned by Rust; strings readable without per-element conversion. + +```rust +fn ingest(items: UniquePtr>); +``` + +```cpp +auto items = std::make_unique>(BuildItems()); +ffi::ingest(std::move(items)); +``` + +**C++ → Rust ownership, small / ergonomic — `Vec` (deep copy)** + +Highest safety. Fully decouples from C++ memory on receipt. + +```rust +fn ingest_small(items: Vec); +``` + +---