Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ffi/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## 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. 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.
- Swift's and Python's objects have no `close()`, and Python's are no context managers:
an object's handle is freed once its last reference goes, in Swift's `deinit` or
CPython's deallocation, so no reference can outlive it. Drop the reference instead of
Expand Down
36 changes: 30 additions & 6 deletions ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,23 +195,47 @@ 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` 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 `&`.
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. 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, `<enum>_<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<T>` 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<const std::uint8_t>`. 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<T>` is a `std::vector`, of
`view`s for bytes passed in, and a socket address `socket_addr`, the C struct. `Option<T>` is `std::optional<T>`, a
tuple `std::tuple`, and `Result<T, E>` a `result<T, E>`, 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
Expand Down
61 changes: 61 additions & 0 deletions ffi/fixtures/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,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<Level>,
}

/// 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<std::sync::OnceLock<u32>>,
}

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<u32> {
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)]
Expand Down
11 changes: 9 additions & 2 deletions ffi/moq/moq.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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/");
Expand Down Expand Up @@ -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);
Expand Down
132 changes: 113 additions & 19 deletions ffi/runtime/rt.hpp
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -115,6 +115,83 @@ class slot<void> {
}
};

// Frees a handle of the C type `R` with its `_free`.
template <class R, void (*Free)(R *)>
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 R, void (*Free)(R *)>
class unique {
::std::unique_ptr<R, deleter<R, Free>> 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 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 R, void (*Free)(R *), R *(*Clone)(const R *)>
class shared {
::std::shared_ptr<R> ptr_;

shared() noexcept = default;

public:
explicit shared(R *raw) : ptr_(raw, deleter<R, Free>{}) {}

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<R>(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;
Expand All @@ -128,7 +205,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<const std::uint8_t>`, and back.
class view {
const ::std::uint8_t *data_;
::std::size_t size_;
Expand Down Expand Up @@ -165,7 +243,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_;
Expand Down Expand Up @@ -500,15 +579,26 @@ struct access {
}
template <class T>
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 <class T>
static auto own(T &object) {
return object.raw_.own();
}
// `raw` owned by a `T`.
template <class T, class R>
static T wrap(R *raw) noexcept {
return T(raw);
static T wrap(R *raw) {
return T(decltype(T::raw_)(raw));
}
template <class T>
static void release(T &object) noexcept {
object.raw_ = nullptr;
// `raw`, which Rust lends for a call, as a `T` that never frees it.
template <class T, class R>
static T lend(R *raw) {
return T(decltype(T::raw_)::lend(raw));
}
template <class T, class R>
static const char *give_back(T &object, R *raw) noexcept {
return object.raw_.give_back(raw);
}
};

Expand All @@ -522,13 +612,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 <class C, class R, class F>
C list(const R &items, F lower) {
C list(R &items, F lower) {
using E = ::std::remove_const_t<::std::remove_pointer_t<decltype(C{}.ptr)>>;
auto array = ::std::make_shared<::std::vector<E>>();
array->reserve(items.size());
for (const auto &item : items) {
for (auto &item : items) {
array->push_back(lower(item));
}
static const E empty{};
Expand All @@ -554,22 +645,25 @@ ::std::vector<T> 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 T, class R>
class lent {
T object_;
R *raw_;

static ::std::remove_const_t<R> *mutable_(R *raw) noexcept {
return const_cast<::std::remove_const_t<R> *>(raw);
}

public:
explicit lent(R *raw) noexcept
: object_(access::wrap<T>(const_cast<::std::remove_const_t<R> *>(raw))), raw_(raw) {}
explicit lent(R *raw) : object_(access::lend<T>(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 {
Expand Down
Loading