From ee9cbc8bbc73590f7d092194169650cfc3c966c9 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:39:46 -0800 Subject: [PATCH 1/4] feat(ffi): own C++ handles through smart pointers, shared where clones match Co-Authored-By: Claude Opus 5.5 --- ffi/MIGRATE.md | 7 ++ ffi/README.md | 35 ++++++-- ffi/fixtures/api/src/lib.rs | 61 ++++++++++++++ ffi/moq/moq.cpp | 11 ++- ffi/runtime/rt.hpp | 127 +++++++++++++++++++++++++---- ffi/src/collect.rs | 114 ++++++++++++++++++++++++++ ffi/src/cpp.rs | 51 +++++++----- ffi/src/ir.rs | 4 + ffi/tests/api.cpp | 132 ++++++++++++++++++++++++++++++- ffi/tests/fixtures.rs | 5 +- quest/m1/ffi-export/lifetimes.md | 10 +-- 11 files changed, 501 insertions(+), 56 deletions(-) diff --git a/ffi/MIGRATE.md b/ffi/MIGRATE.md index 795ad281b..bdc45e4d9 100644 --- a/ffi/MIGRATE.md +++ b/ffi/MIGRATE.md @@ -2,6 +2,13 @@ ## Unreleased +- C++ objects own their handle through `std::unique_ptr`, or `std::shared_ptr` where + copies may share one value. A `Clone` type whose copies may not, such as moq's + producers and consumers, no longer copies: `T copy = t;` is `T copy = t.clone();`, and + a braced list of such objects, which copies them, is built with `push_back` instead. + Such a type with a method named `clone` takes a `rename`. A copy of a type that shares, + such as moq's `path`, `timestamp`, and `url`, shares the handle rather than cloning in + Rust, and a copy of one a closure lends, kept past the call, aborts. - Every export writes `python/.py`, so an export fails when two items take the same Python name, such as a type `Foo` beside an error type's `FooException`, or when one takes a name the Python's runtime uses unqualified, each starting with `_`: give it diff --git a/ffi/README.md b/ffi/README.md index be9946b15..2c4387d71 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -194,23 +194,46 @@ exceptions and RTTI on or off: it throws nothing and reads no type information. `moq::track_producer::append_group`. A keyword takes a trailing `_` (`counter::new_`), as does a member named as its class or one of its fields, and a parameter named as a member or as anything in the namespace, which it would hide. -- **Objects** are classes owning their handle, move-only unless `Clone`, whose copy - clones. `&self` methods are `const`, and `self` methods are `&&`, so a call that moves - the value out reads `std::move(c).into_label()`. A handle taken by value is moved into, - by a parameter taken by value. +- **Objects** are classes owning their handle through a `std::unique_ptr` whose deleter + calls `_free`, so they move and never copy, and a `Clone` type's `clone()` crosses into + Rust for a value of its own. A type whose copies may share one value, by the rule below, + owns it through a `std::shared_ptr` instead: a copy shares the handle without crossing + into Rust, and the last copy frees it, on whichever thread drops it. `&self` methods are + `const`, and `self` methods are `&&`, so a call that moves the value out reads + `std::move(c).into_label()`. A handle taken by value is moved into, by a parameter taken + by value; moving out of a shared copy another copy holds moves out a clone, leaving that + copy whole, as Rust leaves a clone. A call on a moved-from object, or one a call moved + the value out of, aborts; a dangling reference to an object is C++'s own undefined + behavior. +- **Sharing** stands in for `clone` only where no call can tell the two apart, which the + exporter decides at compile time. A type's copies share when it is `Clone`; `Sync`, + since one that is not is borrowed on one thread at a time, which copies on two threads + would contend for; borrowed mutably by no export, through `&mut self`, a `&mut` + parameter, or a closure lent one; and its clone owns no state that changes behind `&`. + For that last, what a field reaches through an `Arc`, an `Rc`, or a shared reference, + the clone shares, and any other field must clone by a derived `Clone` or std's, with no + `Drop` of its own, and hold only such values. So a `Mutex`, `OnceLock`, or atomic it + owns, even behind a `Box`, keeps copies apart, as does a raw pointer, or a `Clone` or + `Drop` written by hand, which may count clones. std's `Clone` of a `Copy` type copies + it. Sharing still shows in a count of an `Arc`'s clones, such as `Arc::strong_count`, + which counts one per shared handle. Of moq's types, `path`, `timestamp`, and `url` + share, and its producers and consumers, whose `Clone` counts them, cross. - **Records** are structs, and **enums** an `enum class`, or for one with data a `std::variant` of a struct per variant, `_`. An enum's methods and traits are free functions taking it first. `PartialEq` is `==`, `Display` `to_string()`, `Debug` `debug()`, and `Default` `default_()`. - **Bytes** are passed as a `view`, which any contiguous bytes convert to, and returned as - a `buf`, which owns Rust's buffer without a copy. A `Vec` is a `std::vector`, of + a `buf`, which owns Rust's buffer without a copy, so it outlives the object it came + from. From C++20, either converts to `std::span`. A `view` or span + lives only as long as the bytes it views, and a `view` lent to a callback only for the + call; reading one past that is C++'s own undefined behavior. A `Vec` is a `std::vector`, of `view`s for bytes passed in, and a socket address `socket_addr`, the C struct. `Option` is `std::optional`, a tuple `std::tuple`, and `Result` a `result`, whose `value()` aborts on an error. - **Traits** are abstract classes, passed as a `std::unique_ptr` that Rust deletes. - **Closures** lent for a call are any callable, taken by a template parameter, which Rust calls only during the call, from any thread. An object it lends is a reference to - its class for that call: moving out of it aborts. + its class for that call: moving out of it, or keeping a copy past the call, aborts. - **Futures** are move-only `future<...>`s, freed when destroyed, which cancels them and loses nothing. `poll(waker)` polls once; `std::move(f).get()` blocks the thread; `std::move(f).then(exec, callback)` polls on an executor and calls back there, returning diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index f69a55b92..812776cd3 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -674,6 +674,67 @@ impl Tally { static DROPPED: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); +/// A level its clones share through an `Arc`, with no `&mut self` method: a clone no call +/// tells apart from the original, so copies may share one handle. +#[derive(Clone)] +pub struct Gauge { + level: Arc, +} + +/// What a gauge's clones share, whose drop counts in [`dropped`], as a token's does. +struct Level(std::sync::atomic::AtomicU64); + +impl Drop for Level { + fn drop(&mut self) { + DROPPED.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } +} + +impl Gauge { + pub fn new(level: u64) -> Self { + Self { + level: Arc::new(Level(level.into())), + } + } + + /// Set the level each clone reads. + pub fn set(&self, level: u64) { + self.level.0.store(level, std::sync::atomic::Ordering::Relaxed); + } + + pub fn level(&self) -> u64 { + self.level.0.load(std::sync::atomic::Ordering::Relaxed) + } + + /// The level, moving this clone out, which leaves the others. + pub fn into_level(self) -> u64 { + self.level() + } +} + +/// The gauge lent shared to `watch` for the call. +pub fn watch(gauge: &Gauge, watch: impl FnOnce(&Gauge) -> u64) -> u64 { + watch(gauge) +} + +/// A value set once, through `&self`: its lock sits behind a `Box`, so the type is `Freeze`, +/// yet each clone copies it into a lock of its own, which the clone then sets apart. +#[derive(Clone, Default)] +pub struct Latch { + value: Box>, +} + +impl Latch { + /// Set the value, unless it was set. + pub fn set(&self, value: u32) -> bool { + self.value.set(value).is_ok() + } + + pub fn get(&self) -> Option { + self.value.get().copied() + } +} + /// A token: every field is `pub`, but its `Drop` must run when C frees it, so it is an /// object rather than a record. #[derive(Clone)] diff --git a/ffi/moq/moq.cpp b/ffi/moq/moq.cpp index d408df3ff..77771d0e7 100644 --- a/ffi/moq/moq.cpp +++ b/ffi/moq/moq.cpp @@ -204,8 +204,15 @@ int main() { { auto publication = origin.create_broadcast("room/alice"); CHECK(publication); - moq::path path = publication->broadcast().info().path(); + moq::broadcast_producer producer = publication->broadcast(); + moq::path path = producer.info().path(); CHECK(is(path.as_str(), "room/alice")); + // A path's clone is a value no call tells apart, so its copies share one handle, + // while a producer's clone counts producers, so it crosses into Rust. + moq::path shared = path; + CHECK(shared == path); + moq::broadcast_producer cloned = producer.clone(); + CHECK(is(cloned.info().path().as_str(), "room/alice")); // Paths own their segments, so C++ builds them, and relative ones, from a string. auto empty = moq::path::new_("/cam/"); @@ -412,7 +419,7 @@ int main() { CHECK(client); moq::origin_producer incoming = moq::origin_spawn(); moq::origin_consumer remote = incoming.consume(); - auto connection = moq::tokio_client(*client).with_reconnect(false).consume(std::move(incoming)); + auto connection = client->clone().with_reconnect(false).consume(std::move(incoming)); CHECK(connection); auto established = std::move(*connection).established().get(); CHECK(established); diff --git a/ffi/runtime/rt.hpp b/ffi/runtime/rt.hpp index f9acb3c35..bfa017ed5 100644 --- a/ffi/runtime/rt.hpp +++ b/ffi/runtime/rt.hpp @@ -115,6 +115,82 @@ class slot { } }; +// Frees a handle of the C type `R` with its `_free`. +template +struct deleter { + void operator()(R *raw) const noexcept { + Free(raw); + } +}; + +// The handle of an object one C++ object owns, which moves and never copies. A moved-from +// one holds null, which any call aborts on. +template +class unique { + ::std::unique_ptr> ptr_; + +public: + explicit unique(R *raw) noexcept : ptr_(raw) {} + + R *get() const noexcept { + return ptr_.get(); + } + // The handle for a call that moves the value out. + R *own() noexcept { + return ptr_.get(); + } + // `raw`, which Rust lends for a call and frees itself. + static unique lend(R *raw) noexcept { + return unique(raw); + } + // Let go of the lent `raw` unfreed, or say how C++ kept it. + const char *give_back(R *raw) noexcept { + return ptr_.release() == raw ? nullptr : "a lent object was moved out of"; + } +}; + +// The handle of an object whose copies share it, since its clone is a value no call +// tells apart from the original. The last copy frees it, on whichever thread drops it. +template +class shared { + ::std::shared_ptr ptr_; + + shared() noexcept = default; + +public: + explicit shared(R *raw) : ptr_(raw, deleter{}) {} + + R *get() const noexcept { + return ptr_.get(); + } + // The handle for a call that moves the value out, held by this copy alone: a clone's + // while another copy shares it, as Rust would move a clone out and leave the copy be. + R *own() { + if (ptr_.use_count() > 1) { + *this = shared(Clone(ptr_.get())); + } + return ptr_.get(); + } + // `raw`, which Rust lends for a call and frees itself. + static shared lend(R *raw) { + shared s; + s.ptr_ = ::std::shared_ptr(raw, [](R *) noexcept {}); + return s; + } + // Let go of the lent `raw`, or say how C++ kept it: a copy left past the call would + // outlive the loan. + const char *give_back(R *raw) noexcept { + if (ptr_.get() != raw) { + return "a lent object was moved out of"; + } + if (ptr_.use_count() != 1) { + return "a lent object was copied past its call"; + } + ptr_.reset(); + return nullptr; + } +}; + // The bytes of a view, never null, so a view of none is told apart from a missing one. inline const ::std::uint8_t *bytes(const void *data, ::std::size_t len) noexcept { static const ::std::uint8_t empty = 0; @@ -128,7 +204,8 @@ inline const ::std::uint8_t *bytes(const void *data, ::std::size_t len) noexcept class buf; -// Bytes lent to a call: text or binary, read during the call and never kept. +// Bytes lent to a call: text or binary, read during the call and never kept. A contiguous +// range, so from C++20 it converts to `std::span`, and back. class view { const ::std::uint8_t *data_; ::std::size_t size_; @@ -165,7 +242,8 @@ class view { friend struct detail::access; }; -// Owned bytes: returned by Rust without a copy, or copied from C++. +// Owned bytes: returned by Rust without a copy, or copied from C++. They outlive the object +// they came from; a view or span of them lives only as long as they do. class buf { // Rust's when `owner` is set, else C++'s, allocated with `new[]` unless empty. detail::c_buf raw_; @@ -500,15 +578,26 @@ struct access { } template static auto raw(const T &object) noexcept { - return object.raw_; + return object.raw_.get(); } + // The handle of an object a call moves the value out of. + template + static auto own(T &object) { + return object.raw_.own(); + } + // `raw` owned by a `T`. template - static T wrap(R *raw) noexcept { - return T(raw); + static T wrap(R *raw) { + return T(decltype(T::raw_)(raw)); } - template - static void release(T &object) noexcept { - object.raw_ = nullptr; + // `raw`, which Rust lends for a call, as a `T` that never frees it. + template + static T lend(R *raw) { + return T(decltype(T::raw_)::lend(raw)); + } + template + static const char *give_back(T &object, R *raw) noexcept { + return object.raw_.give_back(raw); } }; @@ -522,13 +611,14 @@ class arena { public: // `items` as the C array `C`, each lowered by `lower`: never null, so an empty one is - // told apart from a missing one. + // told apart from a missing one. Each item is mutable where `items` is, so a handle + // moved in can be owned alone. template - C list(const R &items, F lower) { + C list(R &items, F lower) { using E = ::std::remove_const_t<::std::remove_pointer_t>; auto array = ::std::make_shared<::std::vector>(); array->reserve(items.size()); - for (const auto &item : items) { + for (auto &item : items) { array->push_back(lower(item)); } static const E empty{}; @@ -554,22 +644,25 @@ ::std::vector lift_list(C c, F lift, Free free) { } // An object Rust lends a closure for one call, as its class over the lent handle, which -// it gives back unfreed. Moving out of it would keep the borrow past the call. +// it gives back unfreed. Moving out of it, or keeping a copy past the call, would keep +// the borrow past the call, so it aborts. template class lent { T object_; R *raw_; + static ::std::remove_const_t *mutable_(R *raw) noexcept { + return const_cast<::std::remove_const_t *>(raw); + } + public: - explicit lent(R *raw) noexcept - : object_(access::wrap(const_cast<::std::remove_const_t *>(raw))), raw_(raw) {} + explicit lent(R *raw) : object_(access::lend(mutable_(raw))), raw_(raw) {} lent(const lent &) = delete; lent &operator=(const lent &) = delete; ~lent() { - if (access::raw(object_) != raw_) { - fail("a lent object was moved out of"); + if (const char *kept = access::give_back(object_, mutable_(raw_))) { + fail(kept); } - access::release(object_); } T &get() noexcept { diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index 4f8930364..a8b9aa15c 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -249,6 +249,7 @@ pub fn collect<'tcx>(tcx: TyCtxt<'tcx>, crates: &[Crate<'_>]) -> Collected { c.declare(); c.define(); c.prune(); + unshare(&mut c.api); c }; // Refusing an object no export returns refuses what takes it, which may leave another @@ -872,6 +873,7 @@ impl<'tcx> Collector<'tcx> { docs, sync, copy, + shared: false, lifetimes, methods: Vec::new(), traits: Traits::default(), @@ -975,6 +977,8 @@ impl<'tcx> Collector<'tcx> { docs: docs.to_string(), sync: true, copy: false, + shared: traits.clone + && self.clone_shares(tcx.type_of(def_id).instantiate_identity().skip_norm_wip(), def_id), lifetimes: 0, methods, traits, @@ -1297,10 +1301,12 @@ impl<'tcx> Collector<'tcx> { if kind == DefKind::Struct { methods.splice(0..0, self.accessors(&site)); } + let shared = traits.clone && self.clone_shares(self.site_ty(&site), def_id); let object = &mut self.api.objects[index]; object.args = args; object.methods = methods; object.traits = traits; + object.shared = shared && object.sync; } _ => {} } @@ -2887,6 +2893,75 @@ impl<'tcx> Collector<'tcx> { .must_apply_modulo_regions() } + /// Whether a clone of the type `ty`, which `def_id` declares, is a value no shared + /// borrow can tell apart from the original, so copies may share one: each part of it + /// is reached through an `Arc`, an `Rc`, or a shared reference, which a clone shares, + /// or never changes behind `&`. A `Cell` or `Mutex` it owns, even inside a `Box`, + /// changes apart in a clone, and a `Drop` or a `Clone` written by hand may count + /// clones, so any of them, or a type it cannot see through, keeps clones apart. + fn clone_shares(&self, ty: Ty<'tcx>, def_id: DefId) -> bool { + let env = TypingEnv::post_analysis(self.tcx, def_id); + self.shares(ty, env, &mut Default::default()) + } + + fn shares( + &self, + ty: Ty<'tcx>, + env: TypingEnv<'tcx>, + seen: &mut rustc_data_structures::fx::FxHashSet>, + ) -> bool { + let tcx = self.tcx; + let ty = tcx + .try_normalize_erasing_regions(env, ty::Unnormalized::new_wip(ty)) + .unwrap_or(ty); + match ty.kind() { + ty::Bool | ty::Char | ty::Int(_) | ty::Uint(_) | ty::Float(_) | ty::Str | ty::Never | ty::FnPtr(..) => true, + ty::Ref(_, _, ty::Mutability::Not) => true, + ty::Array(inner, _) | ty::Slice(inner) => self.shares(*inner, env, seen), + ty::Tuple(elements) => elements.iter().all(|e| self.shares(e, env, seen)), + // Met again through a type that holds itself: what else it holds decides. + ty::Adt(..) if !seen.insert(ty) => true, + ty::Adt(adt, args) => { + let did = adt.did(); + let std = |names: &[&str]| { + (names.iter()).any(|name| tcx.is_diagnostic_item(rustc_span::Symbol::intern(name), did)) + }; + if adt.is_unsafe_cell() || adt.is_union() { + false + } else if adt.is_phantom_data() || Some(did) == self.bytes || std(&["Arc", "Rc", "ArcWeak", "RcWeak"]) { + true + } else if adt.is_box() + || tcx.lang_items().string() == Some(did) + || std(&[ + "Option", "Result", "Vec", "VecDeque", "HashMap", "HashSet", "BTreeMap", "BTreeSet", "Cow", + ]) { + // std's own `Clone`, written by hand, clones each element. + args.types().all(|arg| self.shares(arg, env, seen)) + } else if ["core", "alloc", "std"].contains(&tcx.crate_name(did.krate).as_str()) + && tcx.type_is_copy_modulo_regions(env, ty) + { + // std's `Clone` of a `Copy` type, such as `NonZero`'s, copies it, as std's own + // specializations of `Clone` for `Copy` types assume. + true + } else { + let derived = tcx.lang_items().clone_fn().is_some_and(|clone| { + ty::Instance::try_resolve(tcx, env, clone, tcx.mk_args(&[ty.into()])) + .ok() + .flatten() + .and_then(|instance| tcx.impl_of_assoc(instance.def_id())) + .is_some_and(|imp| tcx.is_automatically_derived(imp)) + }); + derived + && tcx.adt_destructor(did).is_none() + && adt + .all_fields() + .all(|field| self.shares(field.ty(tcx, args).skip_norm_wip(), env, seen)) + } + } + _ => false, + } + } + fn is_send(&self, ty: Ty<'tcx>, env: TypingEnv<'tcx>) -> bool { let tcx = self.tcx; let Some(send) = tcx.get_diagnostic_item(sym::Send) else { @@ -3288,6 +3363,45 @@ fn renumber(ty: &mut Type, gone: usize) { } } +/// Keep apart the copies of each object an export borrows mutably, as `&mut self` or a +/// `&mut` parameter, or lends to a closure as one: a write through one copy would show in +/// the others, as it never does in a clone. +fn unshare(api: &mut Api) { + fn borrowed(ty: &Type, out: &mut Vec) { + match ty { + Type::Ref(Mutability::Mut, inner) => { + if let Type::Object(i) = inner.base() { + out.push(*i); + } + borrowed(inner, out); + } + Type::Option(inner) | Type::Box(inner) | Type::Ref(_, inner) | Type::List(inner) => borrowed(inner, out), + Type::Tuple(elements) => elements.iter().for_each(|ty| borrowed(ty, out)), + Type::Closure(params, ret) => params.iter().chain([&**ret]).for_each(|ty| borrowed(ty, out)), + _ => {} + } + } + let mut mutated = Vec::new(); + let methods = (api.records.iter().flat_map(|r| &r.methods)) + .chain(api.enums.iter().flat_map(|e| &e.methods)) + .chain(api.callbacks.iter().flat_map(|c| &c.methods)) + .chain(&api.functions); + for f in methods { + f.params.iter().for_each(|p| borrowed(&p.ty, &mut mutated)); + } + for (i, object) in api.objects.iter().enumerate() { + for f in &object.methods { + if f.receiver == Some(Receiver::Mut) { + mutated.push(i); + } + f.params.iter().for_each(|p| borrowed(&p.ty, &mut mutated)); + } + } + for i in mutated { + api.objects[i].shared = false; + } +} + /// Whether `ty` names the object `i`. fn names(ty: &Type, i: usize) -> bool { match ty { diff --git a/ffi/src/cpp.rs b/ffi/src/cpp.rs index e254c57c2..90236a2a6 100644 --- a/ffi/src/cpp.rs +++ b/ffi/src/cpp.rs @@ -5,8 +5,9 @@ //! is `moq::track`, and a method is its C name without its type's (`moq_track_close` is //! `moq::track::close`). C names are unique, so these are too, once a keyword, or a member //! named as its class or a field, takes a trailing `_`; so does a parameter named as a -//! member or as anything in the namespace, which it would hide. An object is a move-only class -//! owning its handle, copyable if `Clone`; a record a struct; an enum an `enum class`, or +//! member or as anything in the namespace, which it would hide. An object is a class owning +//! its handle through a `std::unique_ptr`, so it moves, or where `Clone` makes a value no +//! call tells apart, a `std::shared_ptr` its copies share; a record a struct; an enum an `enum class`, or //! for one with data a `std::variant` of a struct per variant, with its methods free //! functions; a closure lent for the call a template parameter. The runtime they share, //! [`runtime/rt.hpp`](../runtime/rt.hpp), holds the bytes, results, wakers, and futures. @@ -246,13 +247,13 @@ impl<'a> Header<'a> { Type::List(inner) => { let x = format!("x{}_", lists(inner)); format!( - "a_.list<::{}>({e}, [&](const auto &{x}) {{ return {}; }})", + "a_.list<::{}>({e}, [&](auto &{x}) {{ return {}; }})", abi.list(inner), self.lower(inner, &x) ) } Type::Option(inner) => match inner.base() { - Type::Object(_) => format!("({e} ? {d}::access::raw(*{e}) : nullptr)"), + Type::Object(_) => format!("({e} ? {} : nullptr)", self.lower(inner, &format!("*{e}"))), base @ Type::List(elem) => format!( "({e} ? {} : ::{}{{}})", self.lower(base, &format!("(*{e})")), @@ -266,6 +267,7 @@ impl<'a> Header<'a> { format!("({e} ? ::{c}{{true, {value}}} : ::{c}{{}})") } }, + Type::Ref(_, inner) if matches!(inner.base(), Type::Object(_)) => format!("{d}::access::raw({e})"), Type::Box(inner) | Type::Ref(_, inner) => self.lower(inner, e), Type::Record(_) | Type::Enum(_) if self.arena(ty) => format!("{d}::lower({e}, a_)"), Type::Record(_) => format!("{d}::lower({e})"), @@ -273,7 +275,9 @@ impl<'a> Header<'a> { true => format!("static_cast<::{}>({e})", abi.enumeration(*i)), false => format!("{d}::lower({e})"), }, - Type::Object(_) => format!("{d}::access::raw({e})"), + // A `Copy` value is copied out, and any other moved out. + Type::Object(i) if abi.api.objects[*i].copy => format!("{d}::access::raw({e})"), + Type::Object(_) => format!("{d}::access::own({e})"), Type::Callback(..) => format!("{d}::lower(::std::move({e}))"), Type::Closure(..) => unreachable!("a closure lowers to its trampoline"), Type::Str | Type::String | Type::Slice | Type::Vec | Type::Bytes | Type::Path | Type::PathBuf => { @@ -594,30 +598,33 @@ impl<'a> Header<'a> { let name = self.name(&c); let t = object.traits; let text = format!("{}\n\n{}", abi.api.rust(&object.path, &object.args), object.docs); + let d = self.d(); + // Copies share a handle only where no call tells that apart from a clone. + let handle = match object.shared { + true => format!("{d}::shared<::{c}, &::{c}_free, &::{c}_clone>"), + false => format!("{d}::unique<::{c}, &::{c}_free>"), + }; let mut body = vec![ - format!("\t::{c} *raw_;\n"), - format!("\texplicit {name}(::{c} *raw) noexcept : raw_(raw) {{}}\n"), - format!("\tfriend struct {}::access;\n", self.d()), + format!("\t{handle} raw_;\n"), + format!("\texplicit {name}(decltype(raw_) raw) noexcept : raw_(::std::move(raw)) {{}}\n"), + format!("\tfriend struct {d}::access;\n"), "public:".to_string(), - format!( - "\t{name}({name} &&other) noexcept : raw_(::std::exchange(other.raw_, nullptr)) {{}}\n\t{name} &operator=({name} &&other) noexcept {{\n\t\t::std::swap(raw_, other.raw_);\n\t\treturn *this;\n\t}}\n\t~{name}() {{\n\t\t::{c}_free(raw_);\n\t}}" - ), ]; - if t.clone { + let mut members = Vec::new(); + if t.clone && !object.shared { body.push(format!( - "\t{name}(const {name} &other) : raw_(::{c}_clone(other.raw_)) {{}}\n\t{name} &operator=(const {name} &other) {{\n\t\tif (this != &other) {{\n\t\t\t*this = {name}(other);\n\t\t}}\n\t\treturn *this;\n\t}}" + "\t/// A clone of its own, through Rust's `Clone`: copies may not share one value,\n\t/// as a call could tell them apart from clones.\n\t{name} clone() const;" )); - } else { - body.push(format!( - "\t{name}(const {name} &) = delete;\n\t{name} &operator=(const {name} &) = delete;" + members.push("clone".to_string()); + self.def(format!( + "inline {name} {name}::clone() const {{\n\treturn {d}::access::wrap<{name}>(::{c}_clone(raw_.get()));\n}}\n" )); } if t.eq { body.push(format!( - "\tfriend bool operator==(const {name} &a, const {name} &b) {{\n\t\treturn ::{c}_eq(a.raw_, b.raw_);\n\t}}\n\tfriend bool operator!=(const {name} &a, const {name} &b) {{\n\t\treturn !(a == b);\n\t}}" + "\tfriend bool operator==(const {name} &a, const {name} &b) {{\n\t\treturn ::{c}_eq(a.raw_.get(), b.raw_.get());\n\t}}\n\tfriend bool operator!=(const {name} &a, const {name} &b) {{\n\t\treturn !(a == b);\n\t}}" )); } - let mut members = Vec::new(); self.traits(&name, &c, t, None, &mut body, &mut members); let owner = Ty::Handle { name: c.clone(), @@ -652,7 +659,7 @@ impl<'a> Header<'a> { let d = self.d(); let object = value.is_none(); let (this, lower) = match value { - None => ("raw_", String::new()), + None => ("raw_.get()", String::new()), Some(ty) => ( "&c_self_", format!("{}\tauto c_self_ = {};\n", self.scope([ty]), self.lower(ty, "*this")), @@ -1077,7 +1084,11 @@ impl<'a> Header<'a> { if receiver.is_some() { let (_, ty) = c_params.next().expect("a method has its receiver"); match (ty, free) { - (Ty::Handle { .. }, _) => args.push("raw_".to_string()), + // Moved out of, as a handle taken by value is. + (Ty::Handle { name, .. }, _) if receiver == Some(Receiver::Owned) && !copy(abi, name) => { + args.push("raw_.own()".to_string()) + } + (Ty::Handle { .. }, _) => args.push("raw_.get()".to_string()), (_, false) => { let ty = record(abi, owner_c.as_deref().expect("a member method has its record")); prelude.push(format!("auto c_self_ = {};", self.lower(&ty, "*this"))); diff --git a/ffi/src/ir.rs b/ffi/src/ir.rs index 7ce4a7c2a..c7b882ba9 100644 --- a/ffi/src/ir.rs +++ b/ffi/src/ir.rs @@ -119,6 +119,10 @@ pub struct Object { /// Whether it is `Copy`: taken by value, it is copied out of its handle, as Rust /// copies it, rather than moved out. pub copy: bool, + /// Whether copies of its handle may share one value, since no call tells that apart + /// from `Clone`: it is `Clone` and `Sync`, its clone owns no state that changes + /// behind `&` or counts clones, and no export borrows it mutably. + pub shared: bool, /// How many lifetime parameters it has, each `'static` in the export. pub lifetimes: usize, pub methods: Vec, diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index 20ab58781..2473fa1a6 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -14,6 +14,12 @@ #include #include #include +#if __has_include() +#include +#endif +#ifdef __cpp_lib_span +#include +#endif #include "api.hpp" @@ -163,7 +169,7 @@ int script() { print("name ", c.name(), "\n"); std::printf("is d %s\n", yes(c.is("d"))); { - api::counter copy = c; + api::counter copy = c.clone(); (void)copy.add(1); std::printf("clone %llu original %llu\n", ull(copy.get()), ull(c.get())); } @@ -915,12 +921,133 @@ int futures() { return 0; } +// Who owns each handle and bytes, beyond the transcript, each checked by an assertion. +int lifetimes() { + // Copies of a type whose clone no call tells apart share one handle: a write through + // one shows in the other, as it does between clones of an `Arc`. + { + std::uint32_t before = api::dropped(); + api::gauge a = api::gauge::new_(1); + api::gauge b = a; + b.set(2); + check(a.level() == 2, "copies of a shared type see each other's writes"); + // Moving out of one copy moves out a clone, as Rust moves out of a clone, and + // leaves the other copy whole. + check(std::move(b).into_level() == 2, "a shared copy moves out its value"); + check(a.level() == 2 && api::dropped() == before, "moving out of a shared copy leaves the others"); + { + api::gauge c = a; + (void)c; + } + check(api::dropped() == before, "a released copy frees nothing while another holds it"); + // The last copy moves out the value itself. + check(std::move(a).into_level() == 2 && api::dropped() == before + 1, "the last copy moves out the value"); + } + + // Copies of a type whose clone is a value of its own, such as a lock behind a `Box`, + // keep apart: copying is `clone()`, which crosses into Rust, and a write through one + // clone never shows in the other. + { + api::latch a = api::latch::default_(); + api::latch b = a.clone(); + check(b.set(1) && !a.get(), "a clone's write stays its own"); + check(a.set(2) && a.get() == 2u && b.get() == 1u, "each clone holds its own value"); + } + + // A copy released on one thread while a call runs on another: the call's copy keeps + // the handle, and the last copy frees it, once, on whichever thread drops it. + { + std::uint32_t before = api::dropped(); + std::optional mine(api::gauge::new_(7)); + std::mutex mutex; + std::condition_variable cv; + bool started = false; + bool released = false; + bool read = true; + std::thread caller([&, theirs = *mine]() { + { + std::lock_guard lock(mutex); + started = true; + } + cv.notify_all(); + for (;;) { + bool last; + { + std::lock_guard lock(mutex); + last = released; + } + read = read && theirs.level() == 7; + if (last) { + return; + } + } + }); + { + std::unique_lock lock(mutex); + cv.wait(lock, [&]() { return started; }); + } + mine.reset(); + { + std::lock_guard lock(mutex); + released = true; + } + caller.join(); + check(read, "a call races a copy's release unharmed"); + check(api::dropped() == before + 1, "the last copy frees the value once"); + } + + // A copy of a lent object lasts only its call, as the lent object does. + { + api::gauge gauge = api::gauge::new_(3); + check(api::watch(gauge, [](const api::gauge &lent) { + api::gauge copy = lent; + return copy.level(); + }) == 3, "a copy of a lent object reads it during the call"); + } + + // Returned bytes own Rust's buffer, so they outlive the object they came from; a view + // or span of them lasts only as long as they do. + { + api::buf payload; + { + api::message message = api::message::new_(nullptr, "kept"); + payload = message.payload(); + } + check(payload.str() == "kept", "bytes outlive the object they came from"); + std::uint32_t before = api::dropped(); + { + api::buf bytes = api::token_bytes(9, 3); + check(api::dropped() == before && bytes.size() == 3 && bytes.data()[2] == 9, "bytes hold their owner"); +#ifdef __cpp_lib_span + // From C++20, a view is a contiguous range: a span of it, and a view of a span. + std::span span = api::view(bytes); + check(span.size() == 3 && span[0] == 9, "a view converts to a span"); + check(api::len(span) == 3, "a span converts to a view"); +#endif + } + check(api::dropped() == before + 1, "bytes free their owner"); + } + + std::printf("lifetimes ok\n"); + return 0; +} + // Each breaks a contract, which must abort with a message naming the function. int broken(const char *which) { if (std::strcmp(which, "moved") == 0) { api::counter c = api::counter::new_("c", 0); api::label label = std::move(c).into_label(std::nullopt); c.get(); + } else if (std::strcmp(which, "moved from") == 0) { + api::counter c = api::counter::new_("c", 0); + api::counter d = std::move(c); + c.get(); + } else if (std::strcmp(which, "lent copied") == 0) { + static std::optional kept; + api::watch(api::gauge::new_(0), [](const api::gauge &lent) { + kept.emplace(lent); + return lent.level(); + }); } else if (std::strcmp(which, "value") == 0) { api::counter c = api::counter::new_("c", 0); c.sub(1).value(); @@ -960,6 +1087,9 @@ int main(int argc, char **argv) { if (argc > 1 && std::strcmp(argv[1], "futures") == 0) { return futures(); } + if (argc > 1 && std::strcmp(argv[1], "lifetimes") == 0) { + return lifetimes(); + } if (argc > 1) { return broken(argv[1]); } diff --git a/ffi/tests/fixtures.rs b/ffi/tests/fixtures.rs index a0603d38f..c91a106d7 100644 --- a/ffi/tests/fixtures.rs +++ b/ffi/tests/fixtures.rs @@ -184,7 +184,7 @@ fn api_c() { } /// The C++ header at each standard, with exceptions and RTTI on and off between them, -/// runs the same script, then checks its futures' semantics. +/// runs the same script, then checks its futures' semantics and who owns each handle. #[test] fn api_cpp() { let api = api(); @@ -204,8 +204,11 @@ fn api_cpp() { "the C++{standard} transcript differs from the native one" ); run(Command::new(&exe).arg("futures")); + run(Command::new(&exe).arg("lifetimes")); for (case, message) in [ ("moved", "api_counter_get: a handle's value was moved out"), + ("moved from", "api_counter_get: a handle is null"), + ("lent copied", "a lent object was copied past its call"), ("value", "value() of a result that holds an error"), ( "finished", diff --git a/quest/m1/ffi-export/lifetimes.md b/quest/m1/ffi-export/lifetimes.md index 1dfcbcce9..735117f99 100644 --- a/quest/m1/ffi-export/lifetimes.md +++ b/quest/m1/ffi-export/lifetimes.md @@ -30,14 +30,6 @@ an error, never a crash or undefined behavior. - **Go**: settled in [#411](https://github.com/moq-dev/rust2/pull/411): objects keep `Close`, with every later call returning an error, and bytes are a GC-owned `Bytes` with no raw slice, read through methods and copied by `Copy()`. -- **C++**: an object owns its handle through `std::unique_ptr` with a deleter calling - `_free`, and a `Clone` type through `std::shared_ptr`, so a copy shares one Rust handle - without crossing into Rust. Sharing must not differ from the crate's `clone`: a type whose - `clone` makes an independent value, which a `&mut self` method or interior mutability can - then tell apart, keeps an explicit `clone()` that crosses. The header keeps C++17; where C++20's `std::span` is available, - `view` converts to `std::span`, the standard's non-owning view of - contiguous bytes. A dangling reference stays C++'s own undefined behavior. - **C**: out of scope; its `_free` follows C's norms. -Public API: breaking for Swift, Python, Kotlin bytes, and C++ ownership; each gets a -`MIGRATE.md` entry. +Public API: breaking for Swift, Python, and Kotlin bytes; each gets a `MIGRATE.md` entry. From d60208c4c4bd0aff12e7c7013e8d229214e30daf Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:53:53 -0800 Subject: [PATCH 2/4] docs(ffi): name the handles in the C++ runtime and generator docs Co-Authored-By: Claude Opus 5.5 --- ffi/runtime/rt.hpp | 4 ++-- ffi/src/cpp.rs | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ffi/runtime/rt.hpp b/ffi/runtime/rt.hpp index bfa017ed5..4dec12d2d 100644 --- a/ffi/runtime/rt.hpp +++ b/ffi/runtime/rt.hpp @@ -1,5 +1,5 @@ -// The runtime every C++ binding shares: bytes, results, wakers, and futures over the C -// ABI. rust2-ffi copies it into each header, inside the export's namespace, after +// The runtime every C++ binding shares: handles, bytes, results, wakers, and futures over +// the C ABI. rust2-ffi copies it into each header, inside the export's namespace, after // `detail` names the export's C types. It throws nothing and reads no type information, // so it builds with exceptions and RTTI off. diff --git a/ffi/src/cpp.rs b/ffi/src/cpp.rs index 90236a2a6..f832723b4 100644 --- a/ffi/src/cpp.rs +++ b/ffi/src/cpp.rs @@ -7,10 +7,11 @@ //! named as its class or a field, takes a trailing `_`; so does a parameter named as a //! member or as anything in the namespace, which it would hide. An object is a class owning //! its handle through a `std::unique_ptr`, so it moves, or where `Clone` makes a value no -//! call tells apart, a `std::shared_ptr` its copies share; a record a struct; an enum an `enum class`, or -//! for one with data a `std::variant` of a struct per variant, with its methods free -//! functions; a closure lent for the call a template parameter. The runtime they share, -//! [`runtime/rt.hpp`](../runtime/rt.hpp), holds the bytes, results, wakers, and futures. +//! call tells apart, a `std::shared_ptr` its copies share; a record a struct; an enum an +//! `enum class`, or for one with data a `std::variant` of a struct per variant, with its +//! methods free functions; a closure lent for the call a template parameter. The runtime +//! they share, [`runtime/rt.hpp`](../runtime/rt.hpp), holds the handles, bytes, results, +//! wakers, and futures. use std::collections::BTreeSet; @@ -613,7 +614,7 @@ impl<'a> Header<'a> { let mut members = Vec::new(); if t.clone && !object.shared { body.push(format!( - "\t/// A clone of its own, through Rust's `Clone`: copies may not share one value,\n\t/// as a call could tell them apart from clones.\n\t{name} clone() const;" + "\t/// Rust's `Clone`, a value of its own: copies sharing one value could be told apart\n\t/// from clones, so it does not copy.\n\t{name} clone() const;" )); members.push("clone".to_string()); self.def(format!( From 67778e8139f14e9af4d4da9086021294712de3eb Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 03:06:36 -0800 Subject: [PATCH 3/4] fix(ffi): keep C++ trait members apart from a class of their name Co-Authored-By: Claude Opus 5.5 --- ffi/src/cpp.rs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/ffi/src/cpp.rs b/ffi/src/cpp.rs index f832723b4..64c395ec2 100644 --- a/ffi/src/cpp.rs +++ b/ffi/src/cpp.rs @@ -613,13 +613,14 @@ impl<'a> Header<'a> { ]; let mut members = Vec::new(); if t.clone && !object.shared { + let clone = apart("clone", &name); body.push(format!( - "\t/// Rust's `Clone`, a value of its own: copies sharing one value could be told apart\n\t/// from clones, so it does not copy.\n\t{name} clone() const;" + "\t/// Rust's `Clone`, a value of its own: copies sharing one value could be told apart\n\t/// from clones, so it does not copy.\n\t{name} {clone}() const;" )); - members.push("clone".to_string()); self.def(format!( - "inline {name} {name}::clone() const {{\n\treturn {d}::access::wrap<{name}>(::{c}_clone(raw_.get()));\n}}\n" + "inline {name} {name}::{clone}() const {{\n\treturn {d}::access::wrap<{name}>(::{c}_clone(raw_.get()));\n}}\n" )); + members.push(clone); } if t.eq { body.push(format!( @@ -667,28 +668,31 @@ impl<'a> Header<'a> { ), }; if t.display { - body.push(format!("\t::{ns}::buf to_string() const;")); - members.push("to_string".to_string()); + let member = apart("to_string", name); + body.push(format!("\t::{ns}::buf {member}() const;")); self.def(format!( - "inline ::{ns}::buf {name}::to_string() const {{\n{lower}\treturn {d}::access::adopt(::{c}_to_string({this}));\n}}\n" + "inline ::{ns}::buf {name}::{member}() const {{\n{lower}\treturn {d}::access::adopt(::{c}_to_string({this}));\n}}\n" )); + members.push(member); } if t.debug { - body.push(format!("\t::{ns}::buf debug() const;")); - members.push("debug".to_string()); + let member = apart("debug", name); + body.push(format!("\t::{ns}::buf {member}() const;")); self.def(format!( - "inline ::{ns}::buf {name}::debug() const {{\n{lower}\treturn {d}::access::adopt(::{c}_debug({this}));\n}}\n" + "inline ::{ns}::buf {name}::{member}() const {{\n{lower}\treturn {d}::access::adopt(::{c}_debug({this}));\n}}\n" )); + members.push(member); } if t.default { - body.push(format!("\tstatic {name} default_();")); - members.push("default_".to_string()); + let member = apart("default_", name); + body.push(format!("\tstatic {name} {member}();")); + members.push(member.clone()); let make = match object { true => format!("{d}::access::wrap<{name}>(c_ret_)"), false => format!("{d}::lift(c_ret_)"), }; self.def(format!( - "inline {name} {name}::default_() {{\n\tauto c_ret_ = ::{c}_default();\n\treturn {make};\n}}\n" + "inline {name} {name}::{member}() {{\n\tauto c_ret_ = ::{c}_default();\n\treturn {make};\n}}\n" )); } } @@ -1323,6 +1327,15 @@ fn runtime_names() -> impl Iterator { std::iter::once("detail").chain(names) } +/// The member `bare` a type's trait exports, in the class `class`, with `_` appended where +/// it is the class's name, which C++ would read as a constructor. +fn apart(bare: &str, class: &str) -> String { + match bare == class { + true => format!("{bare}_"), + false => bare.to_string(), + } +} + /// The C++ name of the parameter `rust`, with `_` appended while `taken` holds it or it is /// a name the header gives its own locals and members. fn parameter(rust: &str, taken: impl Fn(&str) -> bool) -> String { From 331be73cafc334a6813743ae8f85706c71cf01c7 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 06:37:55 -0800 Subject: [PATCH 4/4] docs(ffi): state that C++'s shared copies count once in an Arc's strong count Co-Authored-By: Claude Opus 5.5 --- ffi/MIGRATE.md | 4 +++- ffi/README.md | 9 +++++---- ffi/runtime/rt.hpp | 3 ++- ffi/src/collect.rs | 6 +++--- ffi/src/cpp.rs | 6 ++++-- ffi/src/ir.rs | 2 +- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/ffi/MIGRATE.md b/ffi/MIGRATE.md index bdc45e4d9..c0cfb13fc 100644 --- a/ffi/MIGRATE.md +++ b/ffi/MIGRATE.md @@ -8,7 +8,9 @@ a braced list of such objects, which copies them, is built with `push_back` instead. Such a type with a method named `clone` takes a `rename`. A copy of a type that shares, such as moq's `path`, `timestamp`, and `url`, shares the handle rather than cloning in - Rust, and a copy of one a closure lends, kept past the call, aborts. + Rust, and a copy of one a closure lends, kept past the call, aborts. So do `Arc`- and + `Rc`-backed types, such as moq's stats `registry`: an `Arc::strong_count` of what one + holds counts one for all its C++ copies, not one per copy. - Every export writes `python/.py`, so an export fails when two items take the same Python name, such as a type `Foo` beside an error type's `FooException`, or when one takes a name the Python's runtime uses unqualified, each starting with `_`: give it diff --git a/ffi/README.md b/ffi/README.md index 2c4387d71..4f94ac986 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -205,8 +205,8 @@ exceptions and RTTI on or off: it throws nothing and reads no type information. copy whole, as Rust leaves a clone. A call on a moved-from object, or one a call moved the value out of, aborts; a dangling reference to an object is C++'s own undefined behavior. -- **Sharing** stands in for `clone` only where no call can tell the two apart, which the - exporter decides at compile time. A type's copies share when it is `Clone`; `Sync`, +- **Sharing** stands in for `clone` where no call can tell the two apart but by counting + an `Arc`'s or `Rc`'s clones, which the exporter decides at compile time. A type's copies share when it is `Clone`; `Sync`, since one that is not is borrowed on one thread at a time, which copies on two threads would contend for; borrowed mutably by no export, through `&mut self`, a `&mut` parameter, or a closure lent one; and its clone owns no state that changes behind `&`. @@ -215,8 +215,9 @@ exceptions and RTTI on or off: it throws nothing and reads no type information. `Drop` of its own, and hold only such values. So a `Mutex`, `OnceLock`, or atomic it owns, even behind a `Box`, keeps copies apart, as does a raw pointer, or a `Clone` or `Drop` written by hand, which may count clones. std's `Clone` of a `Copy` type copies - it. Sharing still shows in a count of an `Arc`'s clones, such as `Arc::strong_count`, - which counts one per shared handle. Of moq's types, `path`, `timestamp`, and `url` + it. The one difference: every C++ copy of a shared object is one Rust value, so + `Arc::strong_count`, `Rc::strong_count`, or a weak count of what it holds counts one for + all of them, where Rust's clones would count one each. Of moq's types, `path`, `timestamp`, and `url` share, and its producers and consumers, whose `Clone` counts them, cross. - **Records** are structs, and **enums** an `enum class`, or for one with data a `std::variant` of a struct per variant, `_`. An enum's methods and diff --git a/ffi/runtime/rt.hpp b/ffi/runtime/rt.hpp index 4dec12d2d..25925ab6b 100644 --- a/ffi/runtime/rt.hpp +++ b/ffi/runtime/rt.hpp @@ -150,7 +150,8 @@ class unique { }; // The handle of an object whose copies share it, since its clone is a value no call -// tells apart from the original. The last copy frees it, on whichever thread drops it. +// tells apart from the original but by an `Arc`'s or `Rc`'s count, which counts one for +// every copy. The last copy frees it, on whichever thread drops it. template class shared { ::std::shared_ptr ptr_; diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index a8b9aa15c..e10fec2a9 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -2894,9 +2894,9 @@ impl<'tcx> Collector<'tcx> { } /// Whether a clone of the type `ty`, which `def_id` declares, is a value no shared - /// borrow can tell apart from the original, so copies may share one: each part of it - /// is reached through an `Arc`, an `Rc`, or a shared reference, which a clone shares, - /// or never changes behind `&`. A `Cell` or `Mutex` it owns, even inside a `Box`, + /// borrow can tell apart from the original but by an `Arc`'s or `Rc`'s count, so copies + /// may share one: each part of it is reached through an `Arc`, an `Rc`, or a shared + /// reference, which a clone shares, or never changes behind `&`. A `Cell` or `Mutex` it owns, even inside a `Box`, /// changes apart in a clone, and a `Drop` or a `Clone` written by hand may count /// clones, so any of them, or a type it cannot see through, keeps clones apart. fn clone_shares(&self, ty: Ty<'tcx>, def_id: DefId) -> bool { diff --git a/ffi/src/cpp.rs b/ffi/src/cpp.rs index 64c395ec2..ed66afa84 100644 --- a/ffi/src/cpp.rs +++ b/ffi/src/cpp.rs @@ -7,7 +7,8 @@ //! named as its class or a field, takes a trailing `_`; so does a parameter named as a //! member or as anything in the namespace, which it would hide. An object is a class owning //! its handle through a `std::unique_ptr`, so it moves, or where `Clone` makes a value no -//! call tells apart, a `std::shared_ptr` its copies share; a record a struct; an enum an +//! call tells apart but by an `Arc`'s or `Rc`'s count, a `std::shared_ptr` its copies +//! share; a record a struct; an enum an //! `enum class`, or for one with data a `std::variant` of a struct per variant, with its //! methods free functions; a closure lent for the call a template parameter. The runtime //! they share, [`runtime/rt.hpp`](../runtime/rt.hpp), holds the handles, bytes, results, @@ -600,7 +601,8 @@ impl<'a> Header<'a> { let t = object.traits; let text = format!("{}\n\n{}", abi.api.rust(&object.path, &object.args), object.docs); let d = self.d(); - // Copies share a handle only where no call tells that apart from a clone. + // Copies share a handle only where no call tells that apart from a clone, but by an + // `Arc`'s or `Rc`'s count, which counts one for every copy. let handle = match object.shared { true => format!("{d}::shared<::{c}, &::{c}_free, &::{c}_clone>"), false => format!("{d}::unique<::{c}, &::{c}_free>"), diff --git a/ffi/src/ir.rs b/ffi/src/ir.rs index c7b882ba9..9a5adcabd 100644 --- a/ffi/src/ir.rs +++ b/ffi/src/ir.rs @@ -120,7 +120,7 @@ pub struct Object { /// copies it, rather than moved out. pub copy: bool, /// Whether copies of its handle may share one value, since no call tells that apart - /// from `Clone`: it is `Clone` and `Sync`, its clone owns no state that changes + /// from `Clone` but by an `Arc`'s or `Rc`'s count: it is `Clone` and `Sync`, its clone owns no state that changes /// behind `&` or counts clones, and no export borrows it mutably. pub shared: bool, /// How many lifetime parameters it has, each `'static` in the export.