Tyler Mandry
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # dyn and RPITIT [dyn initiative notes](https://hackmd.io/BrUI9ZEtTwK4hqhF4qC1wA) Welcome! This document explores how to combine `dyn` and `impl Trait` in return position. This is crucial pre-requisite for async functions in traits. As a motivating example, consider the trait `AsyncIterator`: ```rust trait AsyncIterator { type Item; async fn next(&mut self) -> Option<Self::Item>; } ``` The `async fn` here is, of course, short for a function that returns `impl Future`: ```rust trait AsyncIterator { type Item; fn next(&mut self) -> impl Future<Output = Option<Self::Item>>; } ``` The focus of this document is on how we can support `dyn AsyncIterator`. For an examination of why this is difficult, see [this blog post](https://smallcultfollowing.com/babysteps//blog/2021/09/30/dyn-async-traits-part-1/). ## Key details Here is a high-level summary of the key details of our approach: * Natural usage: * To use dynamic dispatch, just write `&mut dyn AsyncIterator`, same as any other trait. * Similarly, on the impl side, just write `impl AsyncIterator for MyType`, same as any other trait. * Allocation by default, but not required: * By default, trait functions that return `-> impl Trait` will allocate a `Box` to store the trait, but only when invoked through a `dyn Trait` (static dispatch is unchanged). * To support no-std or high performance scenarios, types can customize how an `-> impl Trait` function is dispatch through `dyn`. We show how to implement an `InlineAsyncIterator` type, for example, that wraps another `AsyncIterator` and stores the resulting futures on pre-allocated stack space. * rust-lang will publish a crate, `dyner`, that provides several common strategies. * Separation of concerns: * Users of a `dyn AsyncIterator` do not need to know (or care) whether the impl allocates a box or uses some other allocation strategy. * Similarly, authors of a type that implements `AsyncIterator` can just write an `impl`. That code can be used with any number of allocation adapters. ## How it feels to use Let's start with how we expect to *use* `dyn AsyncIterator`. This section will also elaborate some of our desiderata[^showoff], such as the ability to use `dyn AsyncIterator` conveniently in both std and no-std scenarios. [^showoff]: Ever since I once saw Dave Herman use this bizarre latin plural, I've been in love with it. --nikomatsakis ### How you write a function with a `dyn` argument We expect people to be able to write functions that take a `dyn AsyncIterator` trait as argument in the usual way: ```rust async fn count(i: &mut dyn AsyncIterator) -> usize { let mut count = 0; while let Some(_) = i.next().await { count += 1; } count } ``` One key part of this is that we want `count` to be invokable from both a std and a no-std environment. ### How you implement a trait with async fns This, too, looks like you would expect. ```rust struct YieldingRangeIterator { start: u32, stop: u32, } impl AsyncIterator for YieldingRangeIterator { type Item = u32; async fn next(&mut self) { if self.start < self.stop { let i = self.start; self.start += 1; tokio::thread::yield_now().await; Some(i) } else { None } } } ``` ### How you invoke `count` in std You invoke it as you normally would, by performing an unsize coercion. Invoking the method requires an allocator by default. ```rust let x = YieldingRangeIterator::new(...); let c = count(&mut x /* as &mut dyn AsyncIterator */).await; ``` ### How you invoke `count` in no-std or other specialized cases You need a "preallocating" wrapper or other strategy. This takes the form of a struct defined for each trait that wraps a `impl YourTrait` (e.g., `impl AsyncIterator`) and customizes the allocation strategy for the returned future. The `dyner` crate packages up some common strategies and structs for standard traits. For example, to pre-allocate the `next` future on the stack, which is useful both for performance or no-std scenarios, you could use an "inline" adapter type, like the `InlineAsyncIterator` type provided by the `dyner` crate: ```rust use dyner::InlineAsyncIterator; // ^^^^^^^^^^^^^^^^^^^ // Inline adapter type async fn count_range(mut x: YieldingRangeIterator) -> usize { // allocates stack space for the `next` future: let inline_x = InlineAsyncIterator::new(x); // invoke the `count` fn, which will no use that stack space // when it runs count(&mut inline_x).await } ``` Dyner provides some other strategies, such as the `CachedAsyncIterator` (which caches the returned `Box` and re-uses the memory in between calls) and the `BoxInAllocatorAsyncIterator` (which uses a `Box`, but with a custom allocator). ### How you apply an existing "adapter" strategy to your own traits The `InlineAsyncIterator` adapts an `AsyncIterator` to pre-allocate stack space for the returned futures, but what if you want to apply that inline stategy to one of your traits? You can do that by using the `#[inline_adapter]` attribute macro applied to your trait definition: ```rust #[inline_adapter(InlineMyTrait)] trait MyTrait { async fn some_function(&mut self); } ``` This will create an adapter type called `InlineMyTrait` (the name is given as an argument to the attribute macro). You would then use it by invoking `new`: ```rust fn foo(x: impl MyTrait) { let mut w = InlineMyTrait::new(x); bar(&mut w); } fn bar(x: &mut dyn MyTrait) { x.some_function(); } ``` If the trait is not defined in your crate, and hence you cannot use an attribute macro, you can use this alternate form, but it requires copying the trait definition: ```rust dyner::inline::adapter_struct! { struct InlineAsyncIterator for trait MyTrait { async fn foo(&mut self); } } ``` ## How it works internally This section is going to dive into the details of how this proposal works within the compiler. As a user, you should not generally have to know these details unless you intend to implement your own adapter shims. ### Return to our running example Let's repeat our running example trait, `AsyncIterator`: ```rust trait AsyncIterator { type Item; async fn next(&mut self) -> Option<Self::Item>; } async fn count(i: &mut dyn AsyncIterator) -> usize { let mut count = 0; while let Some(_) = i.next().await { count += 1; } count } ``` Recall that `async fn next` desugars to a regular function that returns an `impl Trait`: ```rust trait AsyncIterator { type Item; fn next(&mut self) -> impl Future<Output = Option<Self::Item>>; } ``` Which in turn desugars to a trait with an associated type (note: precise details here are still being debated, but this desugaring is "close enough" for our purposes here): ```rust trait AsyncIterator { type Item; type next<'me>: Future<Output = Option<Self::Item>> fn next(&mut self) -> Self::next<'_>; } ``` ### Challenges of invoking an async fn through `dyn` When `count` is compiled, it needs to invoke `i.next()`. But the `AsyncIterator` trait simply declares that `next` returns some `impl Future` (i.e., "some kind of future"). Each impl will define its own return type, which will be some kind of special struct that is "sufficiently large" to store all the state that needed by that particular impl. When an async function is invoked through static dispatch, the caller knows the exact type of iterator it has, and hence knows exactly how much stack space is needed to store the resulting future. When `next` is invoked through a `dyn AsyncIterator`, however, we can't know the specific impl and hence can't use that strategy. Instead, what happens is that invoking `next` on a `dyn AsyncIterator` *always* yields a *pointer* to a future. Pointers are always the same size no matter how much memory they refer to, so this strategy doesn't require knowing the "underlying type" beneath the `dyn AsyncIterator`. Returning a pointer, of course, requires having some memory to point at, and that's where things get tricky. The easiest (and default) solution is to allocate and return a `Pin<Box<dyn Future>>`, but we wish to support other kinds of pointers as well (e.g., pointers into pre-allocated stack space). We'll explain how our system works in stages, first exploring a version that hardcodes the use of `Box`, and then showing how that can be generalized. ### How the running example could work with `Box` Before we get into the full system, we're going to start by *just* explaining how a system that hardcodes `Pin<Box<dyn Future>>` would work. In that case, if we had a `dyn AsyncIterator`, the vtable for that async-iterator would be a struct sort of like this: ```rust struct AsyncIteratorVtable<I> { type_tags: usize, drop_in_place_fn: fn(*mut ()), // function that frees the memory for this trait next_fn: fn(&mut ()) -> Pin<Box<dyn Future<Output = Option<I> + '_>> } ``` This struct has three fields: * `type_tags`, which stores type information used for [`Any`](https://doc.rust-lang.org/std/any/trait.Any.html) * `drop_in_place_fn`, a funcdtion that drops the memory of the underlying value. This is used when the a `dyn AsyncIterator` is dropped; e.g., when a `Box<dyn AsyncIterator>` is dropped, it calls [`drop_in_place`](https://doc.rust-lang.org/std/ptr/fn.drop_in_place.html) on its contents. * `next_fn`, which stores the function to call when the user invokes `next`. You can see that this function is declared to return a `Pin<Box<dyn Future>>`. (This struct is just for explanatory purposes; if you'd like to read more details about vtable layout, see [this description](https://rust-lang.github.io/dyn-upcasting-coercion-initiative/design-discussions/vtable-layout.html).) Invoking `i.next()` (where `i: &mut dyn AsynIterator`) ultimately invokes the `next_fn` from the vtable and hence gets back a `Pin<Box<dyn Future>>`: ```rust i.next().await // becomes let f: Pin<Box<dyn Future<Output = Option<I>>>> = i.next(); f.await ``` ### How to build a vtable that returns a boxed future We've seen how `count` calls a method on a `dyn AsyncIterator` by loading `next_fn` from the vtable, but how do we construct that vtable in the first place? Let's consider the struct `YieldingRangeIterator` and its `impl` of `AsyncIterator` that we saw before in an earlier section: ```rust struct YieldingRangeIterator { start: u32, stop: u32, } impl AsyncIterator for YieldingRangeIterator { type Item = u32; async fn next(&mut self) -> u32 {...} } ``` There's a bit of a trick here. Normally, when we build the vtable for a trait, it points directly at the functions from the impl. But in this case, the function in the impl has a different return type: instead of returning a `Pin<Box<dyn Future>>`, it returns some `impl Future` type that could have any size. This is a problem. To solve it, the vtable doesn't directly reference the `next` fn from the impl, instead it references a "shim" function that allocates the box: ```rust fn yielding_range_shim( this: &mut YieldingRangeIterator, ) -> Pin<Box<dyn Future<Output = Option<u32>>>> { Box::pin(<YieldingRangeIterator as AsyncIterator>::next(this)) } ``` This shim serves as an "adaptive layer" on the callee's side, converting from the `impl Future` type to the `Box`. More generally, we can consider the process of invoking a method through a dyn as having adaptation on both sides, like shown in this diagram: ![diagram](https://gist.githubusercontent.com/nikomatsakis/c0772e1827fd50e72c5052c8504b8a69/raw/4e943c7f762ec9ded11b5467ae952a70a5c4c24a/diagram.svg) (This diagram shows adaptation happening to the arguments too; but for this part of the design, we only need the adaptation on the return value.) ### Generalizing from box with dynx structs So far our design has hardcoded the use of `Box` in the vtable. We can generalize this by introducing a new concept into the compiler; we currently call this a "dynx type"[^better-name]. Like closure types, dynx types are anonymous structs introduced by the compiler. Instead of returning a `Pin<Box<dyn Future>>`, the `next` function will return a `dynx Trait`, which represents "some kind of pointer to a `dyn Trait`". **Note that the `dynx Trait` types are anonymous and that the `dynx` syntax we are using here is for explanatory purposes only.** Users still work with pointer types like `&dyn Trait` , `&mut dyn Trait`, etc.[^user-facing] [^better-name]: Obviously the name "dynx type" is not great. We were considering "object type" (with e.g. `obj Trait` as the explanatory syntax) but we're not sure what to use here. [^user-facing]: It may make sense to use `dynx` as the basis for a user-facing feature at some point, but we are not proposing that here. At runtime, a `dynx Trait` struct has the same size as a `Box<dyn Trait>` (two machine words). If a `dynx Trait` were an ordinary struct, it might look like this: ```rust struct dynx Trait { data: *mut (), vtable: *mut (), } ``` Like a `Box<dyn Trait>`, it carries a vtable that lets us invoke the methods from `Trait` dynamically. Unlike a `Box<dyn Trait>`, it does not hardcode the memory allocation strategy. **Instead, a dynx vtable repurposes the "drop" function slot to mean "drop this pointer and its contents".** This allows `dynx Trait` types to be created from any kind of pointer, such as a `Box`, `&`, `&mut`, `Rc`, or `Arc`. When a `dynx Trait` struct is dropped, it invokes this drop function from its destructor: ```rust impl Drop for dynx Trait { fn drop(&mut self) { let drop_fn: fn(*mut ()) = self.vtable.drop_fn; drop_fn(self.data); } } ``` ### Using dynx in the vtable Now that we have `dynx`, we can define the vtable for an `AsyncIterator` almost exactly like we saw before, but using `dynx Future` instead of a pinned box: ```rust= struct AsyncIteratorVtable<I> { ..., /* some stuff */ next: fn(&mut ()) -> dynx Future<Output = Option<I>> // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Look ma, no box! } ``` Calling `i.next()` for some `i: &mut dyn AsyncIterator` will now give back a `dynx` result: ```rust i.next().await // becomes let f: dynx Future<Output = Option<I>> = i.next(); f.await ``` When the dynx `f` is dropped, its destructor will call the destructor from the vtable, freeing the backing memory in whatever way is appropriate for the kind of pointer that the `dynx` was constructed from. **What have we achieved?** By using `dynx` in the vtable, we have made it so that the caller doesn't know (or have to know) what memory management strategy is in use for the resulting trait object. It knows that it has an instance of some struct (`dynx Future`, specifically) that implements `Future`, is 2-words in size, and can be dropped in the usual way. ### The shim function builds the dynx In the previous section, we saw that using `dynx` in the vtable means that the caller no longer knows (or cares) what memory management strategy is in use. This section explains how the callee actually constructs the `dynx` that gets returned (and how impls can tweak that construction to choose the memory management strategy they want). Absent any other changes, the `impl` of `AsyncIter` contains an `async fn next()` that returns an `impl Future` which could have any size. Therefore, to construct a `dynx Future`, we are going to need an adaptive shim, just like we used in the `Pin<Box>` example. In fact, the default shim is almost exactly the same as we saw in that case. It allocates a pinned box to store the future and returns it. The main difference is that it converts this `Pin<Box<T>>` into a `dynx Future` before returning, rather than coercing to a `Pin<Box<dyn Future>>` return type (the next section will cover shim functions that don't allocate a box): ```rust // "pseudocode", you couldn't actually write this because there is no // user-facing syntax for the `dynx Future` type fn yielding_range_shim( this: &mut YieldingRangeIterator, ) -> dynx Future<Output = Option<u32>> { let boxed = Box::pin(<YieldingRangeIterator as AsyncIterator>::next(this)); // invoke the (inherent) `new` method that converts to a `dynx Future` <dynx Future>::new(boxed) } ``` The most interesting part of this function is the last line, which construct the `dynx Future` from its `new` function. Intuitively, the `new` function takes one argument, which must implement the trait `IntoRawPointer` (added by this design). The `IntoRawPointer` trait is implemented for smart pointer types int the standard library, like `Box`, `Pin<Box>`, and `Rc`, and represents "some kind of pointer" as well as "how to drop that pointer": ```rust // Not pseudocode, will be added to the stdlib and implemented // by various types, including `Box` and `Pin<Box>`. unsafe trait IntoRawPointer: Deref { /// Convert this pointer into a raw pointer to `Self::Target`. /// /// This raw pointer must be valid to dereference until `drop_raw` (below) is invoked; /// this trait is unsafe because the impl must ensure that to be true. fn into_raw(self) -> *mut Self::Target; /// Drops the smart pointer itself as well as the contents of the pointer. /// For example, when `Self = Box<T>`, this will free the box as well as the /// `T` value. unsafe fn drop_raw(this: *mut Self::Target); } ``` The `<dynx Future>::new` method just takes a parameter of type `impl IntoRawPointer` and invokes `into_raw` to convert it into a raw pointer. This raw pointer is then packaged up, together with a modified vtable for `Future`, into the `dynx` structure. The modified vtable is the same as a normal `Future` vtable, except that the "drop" slot is modified so that its drop function points to `IntoRawPointer::drop_raw`, which will be invoked on the data pointer when the `dynx` is dropped. In pseudocode, it looks like this: ```rust // "pseudocode", you couldn't actually write this because there is no // user-facing syntax for the `dynx Future` type; but conceptually this // inherent function exists (in the actual implementation, it may be inlined // by the compiler, since you could never name it struct dynx Future<O> { data: *mut (), // underlying data pointer vtable: &'static FutureVtable<O>, // "modified" vtable for `Future<Output = O>` for the underlying type } struct FutureVtable<O> { /// Invokes `Future::poll` on the underlying data. /// /// Unsafe condition: Expects the output from `IntoRawPointer::into_raw` /// which must not have already been freed. poll_fn: unsafe fn(*mut (), cx: &mut Context<'_>) -> Ready<()>, /// Frees the memory for the pointer to future. /// /// Unsafe condition: Expects the output from `IntoRawPointer::into_raw` /// which must not have already been freed. drop_fn: unsafe fn(*mut ()), } impl<O> dynx Future<Output = O> { fn new<RP>(from: RP) where RP: IntoRawPointer, RP::Target: Sized, // This must be sized so that we know we have a thin pointer. RP::Target: Future<Output = O>, // The target must implement the future trait. RP: Unpin, // Required because `Future` has a `Pin<&mut Self>` method, see discussion later. { let data = IntoRawPointer::into_raw(from); let vtable = FutureVtable<O> { poll_fn: <RP::Target as Future>::poll, drop_fn: |ptr| RP::drop_raw(ptr), }; // construct vtable dynx Future { data, vtable } } } impl<O> Future for dynx Future<Output = O> { type Output = O; fn poll( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Ready<()> { // Unsafety condition is met since... // 1. self.data was initialized in new and is otherwise never changed. // 2. drop must not yet have run or else self would not exist. unsafe { // Conceptually... let pin: Pin<RP> = Pin::new(self); // requires `RP: Unpin`. let pin_mut_self: Pin<&mut RP::Target> = pin.as_mut(); self.vtable.poll_fn(pin_mut_self, cx); self = pin.into_inner(); // XXX is this quite right? self.vtable.poll_fn(self.data, cx) } } } impl<O> Drop for dynx Future<Output = O> { fn drop(&mut self) { // Unsafety condition is met since... // 1. self.data was initialized in new and is otherwise never changed. // 2. drop must not yet have run or else self would not exist. unsafe { self.vtable.drop_fn(self.data); } } } ``` ### Identity shim functions: avoiding the box allocation In the previous section, we explained how the default "shim" created for an `async fn` allocates `Box` to store the future; this `Box` is then converted to a `dynx Future` when it is returned. Using `Box` is a convenient default, but of course it's not always the right choice: for this reason, you can customize what kind of shim using an attribute, `#[dyn]`, attached to the method in the impl: * `#[dyn(box)]` -- requests the default strategy, allocating a box * `#[dyn(identity)]` -- requests a shim that just converts the returned future into a `dynx`. The returned future must be of a suitable pointer type (more on that in the next section). An impl of `AsyncIterator` that uses the default boxing strategy *explicitly* would look like this: ```rust impl AsyncIterator for YieldingRangeIterator { type Item = u32; #[dyn(box)] async fn next(&mut self) { /* same as above */ } } ``` If we want to avoid the box, we can instead write an impl for `AsyncIterator` that uses `dyn(identity)`. In this case, the impl is responsible for converting the `impl Future` return value into a an appropriate pointer from which a `dynx` can be constructed. For example, suppose that we are ok with allocating a `Box`, but we want to do it from a custom allocator. What we would like is an adapter `InAllocator<I>` which adapts some `I: AsyncIterator` so that its futures are boxed in a particular allocator. You would use it like this: ```rust fn example<A: Allocator>(allocator: A) { let mut iter = InAllocator::new(allocator, YieldingRangeIterator::new(); fn_that_takes_dyn(&mut iter); } fn fn_that_takes_dyn(x: &mut dyn AsyncIterator) { // This call will go into the `InAllocator<YieldingRangeIterator>` and // hence will allocate a box using the custom allocator `A`: let value = x.next().await; } ``` To implement `InAllocator<I>`, we first define the struct itself: ```rust struct InAllocator<A: Allocator, I: AsyncIterator> { allocator: A, iterator: I, } impl<A: Allocator, I: AsyncIterator> InAllocator<A, I> { pub fn new( allocator: A, iterator: I, ) -> Self { Self { allocator, iterator } } } ``` and then we implement `AsyncIterator` for `InAllocator<..>`, annotating the `next` method with `#[dyn(identity)]`. The `next` method ```rust impl<A, I> AsyncIterator for InAllocator<A, I> where A: Allocator + Clone, I: AsyncIterator, { type Item = u32; #[dyn(identity)] fn next(&mut self) -> Pin<Box<I::next, A>> { let future = self.iterator.next(); Pin::from(Box::new_in(future, self.allocator.clone())) } } ``` ## Things we will address in a follow-up document We have presented a lot of material here, but believe it or not, it is not the complete design! There are some interesting details to come, and they become important indeed in the "feel" of the overall design. Covering those details however would make this document too long, so we're going to split it into parts. Nonetheless, for completeness, this section lists out some of those "questions yet to come". ### Which values that can be converted into a dynx struct? In the previous section, we showed how a `#[dyn(identity)]` function must return "something that can be converted into a dynx struct", and we showed that a case of returning a `Pin<Box<impl Future, A>>` type. But what are the general rules for constructing a `dynx` struct? You're asking the right question, but that's a part of the design we haven't bottomed out yet. In short, there are two "basic" approaches we could take. One of them is more conservative, in that it doesn't change much about Rust today, but it's also much more complex, because `dyn` dealing with all the "corner cases" of `dyn` is kind of complicated. The other is more radical, but may result in an overall smoother, more coherent design. Apart from that tantalizing tidbit, we are intentionally not providing the details here, because this document is long enough as it is! The next document dives into this question, along with a related question, which is how `dynx` and sealed traits interact. This is actually a complex question with (at least) two possible answers. We ### What about dyn with sendable future, how does that work? We plan to address this in a follow-up RFC. The core idea is to build on the notation that one would use to express that you wish to have an async fn return a `Send`. As an example, one might write `AsyncIterator<next: Send>` to indicate that `next()` returns a `Send` future; when we generate the vtable for a `dyn AsyncIterator<next: Send>`, we can ensure that the bounds for `next` are applied to its return type, so that it would return a `dynx Future + Send` (and not just a `dynx Future`). We have also been exploring a more convenient shorthand for declaring "all the futures returned by methods in trait should be `Send`", but to avoid bikeshedding we'll avoid talking more about that in this document! ### How do `dynx Trait` structs and "sealed traits" interact? As described here, every dyn-safe trait `Trait` gets an "accompanying" `dynx Trait` struct and an `impl Trait for dynx Trait` impl for that struct. This can have some surprising interactions with unsafe code -- if you have a trait that can only be safely implemented by types that meet certain criteria, the impl for a `dynx` type may not meet those criteria. This can lead to undefined behavior. The question then is: *whose fault is that?* In other words, is it the language's fault, for adding impls you didn't expect, or the code author's fault, for not realizing those impls would be there (or perhaps for not declaring that their trait had additional safety requirements, e.g. by making the trait unsafe). This question turns out to be fairly complex, so we'll defer a detailed discussion beyond this summary. ([Details here]) A `dynx Trait` type is basically an implicit struct that implements `Trait`. For some traits, notably "sealed" traits, this could potentially be surprising. Consider a trait `PodTrait` that is meant to be implemented only for "plain old data" types, like `u8` and `u32`. By leveraging the "sealed trait" pattern, one can make it so that users cannot provide any impls of this trait: ```rust pub mod public_api { mod sealed { pub trait Supertrait { } } /// **Guarantee.** Every type `T` that implements `PodTrait` meets /// this condition: /// /// * Given a `&T`, there are `byte_len` bytes of data behind the /// pointer that safely be zeroed pub trait PodTrait: sealed::Supertrait { fn byte_len(&self) -> usize; } impl PodTrait for u8 { fn byte_len(&self) -> usize { 1 } } impl PodTrait for u32 { fn byte_len(&self) -> usize { 4 } } ... } ``` This trait could then be used to build safe abstractions that leverage unsafe reasoning: ```rust pub mod public_api { ... pub fn zero(pod: &mut impl PodTrait) { let n = pod.byte_len(); // OK: We have inspected every impl of `PodTrait` // and we know that `byte_len` accurately // describes the length of a pointer. unsafe { <*mut _>::write_bytes(pod, 0, n) } } ... } ``` Unfortunately, this reasoning is not sound if combined with a `dynx` type: ```rust pub mod public_api { ... trait GetPodTrait { fn get(&self) -> impl PodTrait; } } fn method(x: &dyn GetPodTrait) { // `y` will be a `dynx GetPodTrait`, which will be // a `Box<u8>` or `Box<u32>` let mut y = x.get(); // Calling `zero` is trying to zero the memory *referenced* by // the box, but it will actually zero the box pointer itself. // Bad! public_api::zero(&mut y); } ``` What went wrong here? The problem is implementing `PodTrait` carries an additional proof obligation beyond those that the Rust type checker is aware of. As the comment says, one must guarantee that `byte_len`, invoked on an `&impl PodTrait`, correctly describes the number of bytes in that memory (and that it can safely be zeroed). This invariant was manually verified for the `u8` and `u32` impls, but it does not hold for the `impl PodTrait for dynx PodTrait` generated by the compiler. There are a few ways to resolve this conundrum: * Declare that `PodTrait` should have been declared as `unsafe`, and that `-> impl Foo` is not dyn safe if `Foo` is an unsafe trait. * One could argue that the original code was wrong to declare `PodTrait` as safe; if it were declared as `unsafe`, and we used that to suppress the `dynx` impl (after all, we can't know whether it satisfies the extra conditions), the above code would no longer compile. * However, many Rust devs consider `unafe` to be more of a tool for the user -- if you have private functions and types, you are supposed to be able to reason fully about what they do without needing internal `unsafe` annotations (there hasn't been a formal decision about whether this is a valid principle, but it has many adherents, and it's possible we should make it one). * Furthermore, this means that unsafe traits can't be used with `dynx`, which could *in some cases* be useful. * We could have an opt-in for this case. * Some form of opt-in for dyn compatibility (aka, dyn safety). * Ignoring backwards compatiblility, we could require traits to "opt-in" to being dyn-compatible (e.g., one might write `dyn trait Foo` instead of just having a `trait Foo` that doesn't make use of various prohibited features). In that case, the user is "opting in" to the generation of the `dynx` impl; e.g., a `dyn trait PodTrait` declaration would be simply broken, because that `dyn trait` declaration implies the generation of an `impl PodTrait for dynx PodTrait`, and the safety criteria are not met (this is subtle, but then the code itself is subtle). * We could leverage editions in various ways to resolve the backwards compatibility issues. * Arguments in favor of opt-in: * The fact that the compiler implicitly decides whether your trait is dyn compatible or not is a common source of user confusion -- it's easy to not realize you are promising people dyn compatibility, and also easy to not realize that you've lost it via some change to the crate. * Furthermore, there are some soundness holes (e.g., [#57893](https://github.com/rust-lang/rust/issues/57893)) that are much harder to fix because there is no declaration of dyn compatibility. In those cases, if we knew the trait was meant to be dyn compatible, we could easily enforce stricter rules that would prevent unsoundness. But detecting whether the trait would meet those stricter rules is difficult, and so it is hard to determine automatically whether the trait should be dyn compatible or not. * Finally, some form of explicit "opt-in" to dyn compatibility might allow us to close various ergonomic holes, such as automatically providing an impl of `Trait` for `Box<dyn Trait>`, `&dyn Trait`, and other pointer types (perhaps all pointer types). * Arguments against: * Backwards compatibility. We currently determine automatically if traits are "dyn compatible" or not. That code has to keep working. * Arguably, traits should be dyn compatible by default, and you should "opt out" to make use of the extended set of features that purely static traits provide (we could, of course, do that over an edition). * If a trait `SomeTrait` includes an `-> impl Foo` method, make the trait dyn safe only if `SomeTrait` could *itself* have declared a struct that implements `Foo` * This is a rather wacky rule, but the idea is that if something is "sealed", then either (a) the trait `SomeTrait` is outside the sealed abstraction, and so won't be able to implement it; or (b) it's your own fault, because it's inside the sealed abstraction. * In this case, the offending code is inside the sealed abstraction, so we'd still have the bug. * Introduce a "sealed" keyword and declare that code is wrong. * We would rather not entangle these designs; also, like the "unsafe"-based solution, combining sealed + dyn compatible would likely make sense. ### What does it take to create a dynx? the pointer type `P` from which a `dynx Bounds` is constructed has to meet various conditions depending on the details of `Bounds`: * Must be `IntoRawPointer` which ensures: * `Deref` and `DerefMut` are stable, side-effect free and all that * they deref to the same memory as `into_raw` * If `Bounds` includes a `&mut self` method, `P` must be `DerefMut` * If `Bounds` includes a `&self` method, `P` must be `Deref` * If `Bounds` includes `Pin<&mut Self>`, `P` must be `Unpin` ... and ... something something `DerefMut`? how do you get from `Pin<P>` to `Pin<&mut P::Target>`? * If `Bounds` includes `Pin<&Self>`, `P` must be `Unpin` ... and ... something something `DerefMut`? how do you get from `Pin<P>` to `Pin<&mut P::Target>`? * If `Bounds` includes an auto trait `AutoTrait`, `P` must implement `AutoTrait` * and: `dynx Bounds` implements the auto trait `AutoTrait` (in general, `dynx Bounds` implements all of `Bounds`) * `Bounds` must be "dyn safe" alternative: * P must implement `Bounds` * and we extend `IntoRawPointer` to be able to convert from raw pointer to `&Self`, `&mut Self` and friends ### Is all the `Pin` reasoning really correct? * `dynx Future` must be constructed from a `Pin<P>` pointer * in general, `dynx Trait` needs a pinned input if `Trait` has `Pin<&...>` methods * xxx work through whether pin is needed ### Should "dynx" be a user-facing feature? Arguably, yes, though obviously not with that name. A `dynx Trait` is a very convenient little entity: like a `dyn Trait`, it supports dynamic dispatch, but unlike a `dyn Trait`, it is `Sized`. This means that it goes much further towards realizing the original hope, that one could write `fn foo(x: impl Debug)` and then supply `foo` with a `dyn Debug`. In initial versions of our proposal, we were looking at introducing dynx as a user-facing feature, with the "working keyword" of `obj` (e.g., `obj Debug` would replace `dyn Debug`), but we decided it was overloading this proposal to try and mix that in. Instead, we focused on integrating `dynx` as an "invisible" piece of the semantics, so that *later* we can potentially add it as a user-facing feature that replaces `dyn` more generally. ## Appendix: User-facing extensions This is a brief summary of the user-facing changes. * Extend the definition of `dyn Trait` to include: * Async functions * Functions that return `-> impl Trait` (note that `-> (impl Trait, impl Trait)` or other such constructions are not supported) * So long as `Trait` is dyn safe * Extend the definition of `impl` to permit * `#[dyn(box)]` and `#[dyn(identity)]` annotations * When impleme Implementation steps: ## Appendix: Inline async iterator adaptor This Appendix demonstrates how the inline async iterator adapter works. ```rust pub struct InlineAsyncIterator<'me, T: AsyncIterator> { iterator: T, future: MaybeUninit<T::next<'me>>, } impl<T> AsyncIterator for InlineAsyncIterator<T> where T: AsyncIterator, { type Item = T::Item; #[dyn(identity)] // default is "identity" fn next<'a>(&'a mut self) -> InlineFuture<'me, T::next<'me>> { let future: T::next<'a> = MaybeUninit::new(self.iterator.next()); // Why do we need this transmute? Is 'a not sufficient? let future: T::next<'me> = transmute(future); self.future = future; InlineFuture::new(self.future.assume_init_mut()) } } pub struct InlineFuture<'me, F> where F: Future { future: *mut F, phantom &'me mut F } impl<'me, F> InlineFuture<'me, F> { /// Unsafety condition: /// /// `future` must remain valid for all of `'me` pub unsafe fn new(future: *mut F) -> Self { Self { future } } } impl<'me, F> Future for InlineFuture<'me, F> where F: Future, { fn poll(self: Pin<&mut Self>, context: &mut Context) { // Justified by the condition on `new` unsafe { ... } } } impl<F> IntoRawPointer for InlineFuture<F> { type Target = F; // This return value is the part that has to be thin. fn into_raw(self) -> *mut Self::Target { self.future } // This will be the drop function in the vtable. unsafe fn drop_raw(this: *mut Self::Target) { unsafe { drop_in_place(self.future) } } } impl<'me> Drop for InlineFuture<'me, F> { fn drop(&mut self) { unsafe { <Self as IntoRawPointer>::drop_raw(self.future); } } } ```

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully