rust-lang / rust-lang/impl-trait-utils

Design: Enable dynamic dispatch on traits with `async fn` and RPITIT

Open
#34 2 comments 12 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
134
Forks
13
PR merge metrics
No merged PRs in 30d

Description

The core goal is to make async fn in trait and other RPITIT usable with dynamic dispatch via a proc macro, in a way similar to how it works with async_trait, but while continuing to allow static dispatch.

It core idea of how it works is described here: https://rust-lang.github.io/async-fundamentals-initiative/explainer/async_fn_in_dyn_trait/hardcoding_box.html

Given a trait like

#[trait_variant(dyn)]
trait MyTrait {
    type Item;
    async fn foo(&self) -> Self::Item;
}

Instead of using a type like Box<dyn MyTrait> which wouldn't compile today, we would have the proc macro create a type DynMyTrait with this interface:

impl<'a, Item> DynMyTrait<'a, Item> {
    fn new<T>(value: T) -> DynMyTrait<'a, Item>
    where
        T: MyTrait<Item = Item> + 'a,
        Item: 'a,
    {
        ...
    }
}

impl<'a, Item> MyTrait for DynMyTrait<'a, Item> {
    type Item = Item;
    fn foo(&self) -> impl Future<Output = Self::Item> { /* return Pin<Box<dyn Future<Output = Item> here */ }
}

The struct itself would look something like this. A full explanation is in the links and in the exposition below.

struct DynMyTrait<'a, Item> {
    ptr: *mut (dyn ErasedMyTrait<Item = Item> + 'a),
}

trait ErasedMyTrait {
    type Item;
    fn foo(&self) -> Pin<Box<dyn Future<Output = Self::Item>>>;
}

A full example including macro output is here: https://github.com/nikomatsakis/dyner/blob/main/src/async_iter.rs. Notes:

  • The ErasedAsyncIter trait is used to have the compiler generate a vtable for each concrete type that is used to create a DynAsyncIter. It is not part of the public interface. *mut dyn ErasedAsyncIter is a fat pointer.
  • Note the use of Ref and RefMut wrapper types (which would go in some support library) so that we can also have DynAsyncIter::from_ref and DynAsyncIter::from_ref_mut. These wrappers are slightly verbose, but due to their deref impls, can be reborrowed to create &DynMyTrait and &mut DynMyTrait respectively.
  • This code uses GATs instead of RPITIT in the original trait, since those weren't stable yet, but the same ideas should apply.
  • This code makes use of a union to do bit-twiddling on the data pointer, as a way of marking whether the underlying value is owned. This is not well-defined and should not be necessary, because we can instead have Ref<DynAsyncIter>'s field be ManuallyDrop<DynAsyncIter> EDIT: Actually we probably do need something like this if we have RefMut which gives out &mut DynAsyncIter; we just need to make it well-defined by using std pointer APIs.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the async-fundamentals initiative explainer linked in the issue, then study the complete macro output in dyner's src/async_iter.rs example. Compare the proposed DynMyTrait, erased trait, ownership, and reference-wrapper design with this project's current proc-macro capabilities. Done means async fn and other RPITIT traits support dynamic dispatch while retaining static dispatch.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.