Skip to content

Custom Future contexts #2900

@Diggsey

Description

@Diggsey

Currently, the Context type passed into a future is fixed. However, I think there would be a lot of value in either allowing custom context types, or allowing some way to access custom data on a Context.

Actix

Actix has a model where each actor in the system acts like its own mini executor. You can spawn futures onto an actor's executor, and those futures have mutable access to the actor's state. This is essential to the way the actor model works. However, this is not achievable with standard futures, and so actix defines its own ActorFuture trait.

The only difference is that the poll method takes additional mutable references to both the actor and its executor context. If these pieces of information were accessible somehow from the normal future context then actix would not need a custom future trait. Furthermore, it could even benefit from async/await syntax with a minor extension.

Tokio

Tokio (and I believe async-std?) implements several futures which are "privileged" - ie. they must be polled on a tokio executor or they will fail. However, there's currently no way to check this at compile time. If it was possible to use custom contexts, then these futures could require a tokio-sp;ecific context to be polled.

Implementation

The blocker for this is that the context has a lifetime parameter, and so we need some form of HKT to make this work. Luckily, the one form of HKTs that we do have (GATs) should be enough to allow it, assuming they are ever stabilized.

pub trait ContextProvider {
    type Context<'a>;
}

pub trait Future<P: ContextProvider=TaskContextProvider> {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: P::Context<'_>) -> Poll<Self::Output>;
}

pub struct TaskContextProvider;

impl ContextProvider for TaskContextProvider {
    type Context<'a> = &'a mut std::task::Context<'a>;
}

This would allow actix and tokio to define their own context providers, and in general, executors would be able to avoid TLS entirely.

Trait objects and interoperability

One of the problems with making futures generic is that it's both more difficult to use runtime polymorphism, and it can also divide the ecosystem. I think this can be avoided by creating a trait to encapsulate the current behaviour of Context, and requiring that new contexts implement a superset of that functionality. This means that all futures would easily be convertible up from a "normal future" into one that accepts a custom context.

There will also be ways to convert in the other direction: if you want to convert an actor future into a normal future, you just spawn it onto an actor. If you want to convert a tokio future to a normal future you spawn it onto the tokio executor.

Async/await

In order for async/await to work cleanly with custom contexts, there needs to be a way for code to access the context. There has previously been talk of fully generalizing the async/generator transformation to allow passing in arbitrary arguments, but I think we can avoid that complexity: simply provide an intrinsic which can be called from inside async functions and code blocks which gives the current context:

use std::task;

async fn foo() {
    task::current::<Context>().waker().wake_by_ref();
}

Also, this solves the problem of how to tell the compiler what contexts an async function supports: if the task::current intrinsic is not used, then the anonymous type will generically implement Future for all context types. If task::current is used, the type of the context is known.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions