Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Context::ext #123203

Merged
merged 4 commits into from Apr 3, 2024
Merged

Add Context::ext #123203

merged 4 commits into from Apr 3, 2024

Conversation

jkarneges
Copy link
Contributor

@jkarneges jkarneges commented Mar 29, 2024

This change enables Context to carry arbitrary extension data via a single &mut dyn Any field.

#![feature(context_ext)]

impl Context {
    fn ext(&mut self) -> &mut dyn Any;
}

impl ContextBuilder {
    fn ext(self, data: &'a mut dyn Any) -> Self;

    fn from(cx: &'a mut Context<'_>) -> Self;
    fn waker(self, waker: &'a Waker) -> Self;
}

Basic usage:

struct MyExtensionData {
    executor_name: String,
}

let mut ext = MyExtensionData {
    executor_name: "foo".to_string(),
};

let mut cx = ContextBuilder::from_waker(&waker).ext(&mut ext).build();

if let Some(ext) = cx.ext().downcast_mut::<MyExtensionData>() {
    println!("{}", ext.executor_name);
}

Currently, Context only carries a Waker, but there is interest in having it carry other kinds of data. Examples include LocalWaker, a reactor interface, and multiple arbitrary values by type. There is also a general practice in the ecosystem of sharing data between executors and futures via thread-locals or globals that would arguably be better shared via Context, if it were possible.

The ext field would provide a low friction (to stabilization) solution to enable experimentation. It would enable experimenting with what kinds of data we want to carry as well as with what data structures we may want to use to carry such data.

Dedicated fields for specific kinds of data could still be added directly on Context when we have sufficient experience or understanding about the problem they are solving, such as with LocalWaker. The ext field would be for data for which we don't have such experience or understanding, and that could be graduated to dedicated fields once proven.

Both the provider and consumer of the extension data must be aware of the concrete type behind the Any. This means it is not possible for the field to carry an abstract interface. However, the field can carry a concrete type which in turn carries an interface. There are different ways one can imagine an interface-carrying concrete type to work, hence the benefit of being able to experiment with such data structures.

Passing interfaces

Interfaces can be placed in a concrete type, such as a struct, and then that type can be casted to Any. However, one gotcha is Any cannot contain non-static references. This means one cannot simply do:

struct Extensions<'a> {
    interface1: &'a mut dyn Trait1,
    interface2: &'a mut dyn Trait2,
}

let mut ext = Extensions {
    interface1: &mut impl1,
    interface2: &mut impl2,
};

let ext: &mut dyn Any = &mut ext;

To work around this without boxing, unsafe code can be used to create a safe projection using accessors. For example:

pub struct Extensions {
    interface1: *mut dyn Trait1,
    interface2: *mut dyn Trait2,
}

impl Extensions {
    pub fn new<'a>(
        interface1: &'a mut (dyn Trait1 + 'static),
        interface2: &'a mut (dyn Trait2 + 'static),
        scratch: &'a mut MaybeUninit<Self>,
    ) -> &'a mut Self {
        scratch.write(Self {
            interface1,
            interface2,
        })
    }

    pub fn interface1(&mut self) -> &mut dyn Trait1 {
        unsafe { self.interface1.as_mut().unwrap() }
    }

    pub fn interface2(&mut self) -> &mut dyn Trait2 {
        unsafe { self.interface2.as_mut().unwrap() }
    }
}

let mut scratch = MaybeUninit::uninit();
let ext: &mut Extensions = Extensions::new(&mut impl1, &mut impl2, &mut scratch);

// ext can now be casted to `&mut dyn Any` and back, and used safely
let ext: &mut dyn Any = ext;

Context inheritance

Sometimes when futures poll other futures they want to provide their own Waker which requires creating their own Context. Unfortunately, polling sub-futures with a fresh Context means any properties on the original Context won't get propagated along to the sub-futures. To help with this, some additional methods are added to ContextBuilder.

Here's how to derive a new Context from another, overriding only the Waker:

let mut cx = ContextBuilder::from(parent_cx).waker(&new_waker).build();

@rustbot
Copy link
Collaborator

rustbot commented Mar 29, 2024

Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @Amanieu (or someone else) some time within the next two weeks.

Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Mar 29, 2024
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@Amanieu Amanieu added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. and removed T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Mar 30, 2024
@Amanieu
Copy link
Member

Amanieu commented Mar 30, 2024

@rust-lang/wg-async Do you have any thoughts on this API?

@SkiFire13
Copy link
Contributor

How does this compare with an API like the one for Error::provide? I guess someone may want multiple extensions in the same future.

@jkarneges
Copy link
Contributor Author

Yes, I think there will be a need for multiple extensions, but probably as traits and not just as concrete types, so I'm not sure if an API like Error::provide is quite right. Of course, Any is limited to concrete types so any traits have to be wrapped anyway. Maybe it could work. Each trait could be individually wrapped, and the consumer could request the wrapper type.

Another thing I believe will be desirable is overriding certain extensions in sub-contexts. For example, it may be useful to offer an extension for spawning tasks that could initially be provided by the top level executor but overridden to create task arenas. Maybe that could work with a provider-like API by chaining the provide calls up through the parent contexts, and the first to return something wins.

My main concern with a decision about this though is all experimentation would need to be against nightly. I would love to see the single Any field land in stable, and then in third party crates we create a provider-like API on top of that field and see if it sticks.

@SkiFire13
Copy link
Contributor

Yes, I think there will be a need for multiple extensions, but probably as traits and not just as concrete types, so I'm not sure if an API like Error::provide is quite right

With Error::provide you could just request/provide a reference of type &(dyn Trait + 'static).

Maybe that could work with a provider-like API by chaining the provide calls up through the parent contexts, and the first to return something wins.

Yeah I think this could work nicely with a Error::provide-like API, you just need to have the child context extension store a reference to the parent extension and call provide on it if the child cannot satisfy the request. With the dyn Any approach you would have to store a raw pointer to the parent, which requires the user to use unsafe, and downcasting would no longer just work since you wouldn't get the parent extension.

My main concern with a decision about this though is all experimentation would need to be against nightly. I would love to see the single Any field land in stable, and then in third party crates we create a provider-like API on top of that field and see if it sticks.

I'm not sure stable is the right place to do this kind of experimentation since if it turns out to be the wrong abstraction this is going to stick.

Also, writing a provider-like on top of it is going to require unsafe because ideally the provider would be able to store references, but Any forces it to be 'static. You have the Extensions example for this, but I wonder if it's actually sound. What if you nest two calls to poll and get two different 'static Extensions? Then you would be able to swap them, return from the innermost poll and still have access to its Extensions. This seems pretty tricky to get right.

@conradludgate
Copy link
Contributor

Yeah I think this could work nicely with a Error::provide-like API, you just need to have the child context extension store a reference to the parent extension and call provide on it if the child cannot satisfy the request

This is what I did here https://docs.rs/context-rs/latest/src/context_rs/waker.rs.html#9-12.

Two issues I see:

  • this amount of indirection might be bad for wake performance.
  • cannot provide through subtasks (eg tokio::spawn or FuturesUnordered)

@jkarneges
Copy link
Contributor Author

jkarneges commented Apr 2, 2024

I'm not sure stable is the right place to do this kind of experimentation since if it turns out to be the wrong abstraction this is going to stick.

By "stick" do you mean end up mostly unused? If Any has limits on what it can carry and thus experimentation with a known ideal data structure on stable might remain impossible even if the field were stabilized, then I agree that would be unfortunate.

I suppose it would be unfortunate not only for introducing a field that may go unused, but also because it would imply that experimentation with the ideal data structure would need to happen on nightly. That is a pretty rough fate.

If my example isn't sound, maybe someone else can come up with an approach that is. It's worth investigating very deeply into whether a simple field like Any can be made to work. Compared to the uphill battle of getting popular async runtimes to experiment against a complex API in nightly, it would be time well spent.

@Amanieu Amanieu added the I-libs-api-nominated The issue / PR has been nominated for discussion during a libs-api team meeting. label Apr 2, 2024
@joshtriplett
Copy link
Member

joshtriplett commented Apr 2, 2024

Not objecting to experimenting with this, but registering that if this were up for stabilization I would currently expect to object to stabilizing it. I wanted to document this to make it clear that it's not as simple as "this solves the problem it set out to solve so let's stabilize it as-is".

Concretely:

  • This is a lot of complexity to add to this interface.
  • dyn Any is always painful.
  • Sharing one pointer between multiple potential users is even more painful, and a Provider-style API would be even more complexity.
  • Everyone would pay the cost of this, whether they use it or not. And to the best of my knowledge, this isn't something needed by all async runtimes.
  • The potential users of this (AIUI) currently do have alternative solutions. Those solutions may not be ideal, but we have to weigh that against the complexity (which is a cost to everyone).

To be clear: still very much in favor of async interoperability, but I don't think this is a necessary component of async interoperability.

Request: please link to this comment as an unresolved question in the tracking issue for this.

@jkarneges
Copy link
Contributor Author

Everyone would pay the cost of this, whether they use it or not.

How so? It's just an optional pointer-sized field. If nothing opts-in to use it, then it's basically zero cost.

The potential users of this (AIUI) currently do have alternative solutions.

Thread-local storage? Pass the data to the futures directly? These are not great options.

There's also waker hijacking via waker_getters. It's not stabilized yet and seems like a worse solution for carrying extra data than a dyn Any field (see conradludgate's comment), but I suppose it could be a better option than TLS and direct passing.

@Amanieu
Copy link
Member

Amanieu commented Apr 2, 2024

We discussed this in the libs-api meeting. We're OK with merging this to enable experimentation on nightly, but nobody was very happy with the API itself. We should only stabilize this if we have extensively explored other API solutions (such as rust-lang/libs-team#347, but that has its own drawbacks in terms of API complexity).

r=me once a tracking issue has been created

@jkarneges
Copy link
Contributor Author

@Amanieu made a tracking issue

@Amanieu
Copy link
Member

Amanieu commented Apr 2, 2024

@bors r+

@bors
Copy link
Contributor

bors commented Apr 2, 2024

📌 Commit 036085d has been approved by Amanieu

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 2, 2024
bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 3, 2024
Rollup of 5 pull requests

Successful merges:

 - rust-lang#122865 (Split hir ty lowerer's error reporting code in check functions to mod errors.)
 - rust-lang#122935 (rename ptr::from_exposed_addr -> ptr::with_exposed_provenance)
 - rust-lang#123182 (Avoid expanding to unstable internal method)
 - rust-lang#123203 (Add `Context::ext`)
 - rust-lang#123380 (Improve bootstrap comments)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit 9c1c0bf into rust-lang:master Apr 3, 2024
11 checks passed
@rustbot rustbot added this to the 1.79.0 milestone Apr 3, 2024
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Apr 3, 2024
Rollup merge of rust-lang#123203 - jkarneges:context-ext, r=Amanieu

Add `Context::ext`

This change enables `Context` to carry arbitrary extension data via a single `&mut dyn Any` field.

```rust
#![feature(context_ext)]

impl Context {
    fn ext(&mut self) -> &mut dyn Any;
}

impl ContextBuilder {
    fn ext(self, data: &'a mut dyn Any) -> Self;

    fn from(cx: &'a mut Context<'_>) -> Self;
    fn waker(self, waker: &'a Waker) -> Self;
}
```

Basic usage:

```rust
struct MyExtensionData {
    executor_name: String,
}

let mut ext = MyExtensionData {
    executor_name: "foo".to_string(),
};

let mut cx = ContextBuilder::from_waker(&waker).ext(&mut ext).build();

if let Some(ext) = cx.ext().downcast_mut::<MyExtensionData>() {
    println!("{}", ext.executor_name);
}
```

Currently, `Context` only carries a `Waker`, but there is interest in having it carry other kinds of data. Examples include [LocalWaker](rust-lang#118959), [a reactor interface](rust-lang/libs-team#347), and [multiple arbitrary values by type](https://docs.rs/context-rs/latest/context_rs/). There is also a general practice in the ecosystem of sharing data between executors and futures via thread-locals or globals that would arguably be better shared via `Context`, if it were possible.

The `ext` field would provide a low friction (to stabilization) solution to enable experimentation. It would enable experimenting with what kinds of data we want to carry as well as with what data structures we may want to use to carry such data.

Dedicated fields for specific kinds of data could still be added directly on `Context` when we have sufficient experience or understanding about the problem they are solving, such as with `LocalWaker`. The `ext` field would be for data for which we don't have such experience or understanding, and that could be graduated to dedicated fields once proven.

Both the provider and consumer of the extension data must be aware of the concrete type behind the `Any`. This means it is not possible for the field to carry an abstract interface. However, the field can carry a concrete type which in turn carries an interface. There are different ways one can imagine an interface-carrying concrete type to work, hence the benefit of being able to experiment with such data structures.

## Passing interfaces

Interfaces can be placed in a concrete type, such as a struct, and then that type can be casted to `Any`. However, one gotcha is `Any` cannot contain non-static references. This means one cannot simply do:

```rust
struct Extensions<'a> {
    interface1: &'a mut dyn Trait1,
    interface2: &'a mut dyn Trait2,
}

let mut ext = Extensions {
    interface1: &mut impl1,
    interface2: &mut impl2,
};

let ext: &mut dyn Any = &mut ext;
```

To work around this without boxing, unsafe code can be used to create a safe projection using accessors. For example:

```rust
pub struct Extensions {
    interface1: *mut dyn Trait1,
    interface2: *mut dyn Trait2,
}

impl Extensions {
    pub fn new<'a>(
        interface1: &'a mut (dyn Trait1 + 'static),
        interface2: &'a mut (dyn Trait2 + 'static),
        scratch: &'a mut MaybeUninit<Self>,
    ) -> &'a mut Self {
        scratch.write(Self {
            interface1,
            interface2,
        })
    }

    pub fn interface1(&mut self) -> &mut dyn Trait1 {
        unsafe { self.interface1.as_mut().unwrap() }
    }

    pub fn interface2(&mut self) -> &mut dyn Trait2 {
        unsafe { self.interface2.as_mut().unwrap() }
    }
}

let mut scratch = MaybeUninit::uninit();
let ext: &mut Extensions = Extensions::new(&mut impl1, &mut impl2, &mut scratch);

// ext can now be casted to `&mut dyn Any` and back, and used safely
let ext: &mut dyn Any = ext;
```

## Context inheritance

Sometimes when futures poll other futures they want to provide their own `Waker` which requires creating their own `Context`. Unfortunately, polling sub-futures with a fresh `Context` means any properties on the original `Context` won't get propagated along to the sub-futures. To help with this, some additional methods are added to `ContextBuilder`.

Here's how to derive a new `Context` from another, overriding only the `Waker`:

```rust
let mut cx = ContextBuilder::from(parent_cx).waker(&new_waker).build();
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-libs-api-nominated The issue / PR has been nominated for discussion during a libs-api team meeting. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants