-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Custom Future contexts #2900
Comments
For the record, similar ideas have already been discussed before stabilization of the In no particular order, hoping I don't include irrelevant stuff, and pointing to the first comment in the series of comments that are relevant: |
async-std futures do not need to be polled in an async-std context. tokio futures also kinda don't need to. The main difference is how timers are implemented: tokio puts timers on the runtime threads, async-std has a separate thread. That means tokio futures relying on timers cannot be polled from plain threads. There are compat libraries that "upgrade" a thread to having tokio timers allocated. The problem here is essentially that futures (as all other Rust data structures!) rely on environmental setup. This may be the runtime, the kernel or an initialised background logger. I do not think that encoding all this complexity in the type system is worth the work, especially as those are "hard panic" scenarios that fail immediately and reliably at initialisation. |
I think that adding:
to a Context would be usefull in many scenarios.. |
I may be misunderstanding, but that would restrict futures to only the platforms that have liballoc. Futures already work on platforms where that’s not present, so that suggestion seems to be a non starter. |
It's true It's possible to build a I've no idea if or why this matters here, but it's independently useful like |
In theory it should not be required to have liballoc to use it. You can construct Box from pointer to stack/statically allocated memory (which is unsafe but still usable). Basically my opinion is that you should be able to pass some custom data with context and it needs to store pointer to such data. So for me an Option<Box> is just a safe way of defining this pointer. |
As you can see from this playground snippet, you cannot construct boxes that point to static memory soundly (the same applies for stack-based, except it arguably has even more footguns). Limiting the usage of futures to targets with alloc is incredibly short-sighted and completely disenfranchises an entire segment of the rust community as futures are used within embedded code that doesn't necessarily have access to alloc as demonstrated in Phil Opp's OS series. This is also incredibly not zero-overhead as it requires both the usage of heap allocation and dynamic casting, rendering |
Well the simple solution would be to store just a raw pointer. This would make it much simpler in terms of compatibility and slightly more complicated in terms of usage of such data. Something like: |
What would it take to progress this issue? It seems that the current point is agreeing on how to allow access to "user data" within Context. What are the issues with @Alexei-Kornienko's suggestion above, to just add a pointer to Context? It would be incredibly useful to allow an executor to pass context to futures, not only to avoid TLS but also to allow cleaner design and thread resource management. |
Enforcing |
I think this is related to the context/capabilities proposal that @tmandry, @yoshuawuyts, and myself have been looking into. I'm definitely interested in the idea of custom contexts of some kind, but I'm not totally sure what that looks like yet. |
That seems a lot more general than what's being asked for here: some sort of intrinsic for accessing the My use case for this is writing a custom executor in a WASM context, where the executor is created by the host and lives on the heap with some associated data attached to it. The host side calls into the executor, the executor updates the associated data, it runs for a bounded amount of CPU time (modulo how cooperative the spawned tasks are), and then returns back to the host. I could hoist this associated data and the Having Context types with user-data would simplify a lot of this; I would pass in an exclusive reference to the associated data, and a Spawner that is a thin wrapper around an exclusive reference to the internal task list. The references can't be held on to over |
I've written up a proposed interface (names are placeholders) at this playground, along with a couple examples of using it (in particular, the "passing-references-to-Futures" use-case, and what happens when a Future tries to hold on to a reference without the lifetimes matching). |
It is definitely more general than what is being asked for here, no doubt about that. But it has the advantage of being statically analyzed and verifiable. I'm concerned because we already have problems where libraries are implicitly dependent on one executor or another, and it seems like these problems could be exacerbated. (That said, I certainly see the "perfect enemy of good" argument here.) Just to be sure, am I missing something? I think that if we were to extend |
Hmm. Thinking about this more, it seems like On the augmenting side, you have hard issues regarding composability. E.g. I want to await a Future that takes Foo as aux and another that takes Bar as aux, how do I combine that in the top-level Future? How do I distinguish, e.g., pulling it from the caller-passed aux data vs. being something the top-level Future generates on its own? With the Footnotes
|
Maybe this is irrelevant (I'm new to both Rust and async Rust) but the waker in the context now allows accessing a raw untyped custom pointer: https://doc.rust-lang.org/nightly/core/task/struct.RawWaker.html#method.data: let data: &T = unsafe { &*(ctx.waker().as_raw().data() as *const _) }; My (niche) use case is to use async Rust for streaming event processing. The async processing function lives in a Linux kernel module and is attached to a tracepoint probes (called every time an event is emitted). This means I need a way to "push" the new event in the async code. Using that pointer avoids a global variable, which would be problematic as I'm targeting Coroutines would probably be more adapted to my use case but it looked quite unstable, so I fell back on async. |
The fact that Of course you can workaround it by providing a dummy empty context. Like such example I've found: use std::task::{Context, RawWaker, RawWakerVTable, Waker};
fn do_nothing(_ptr: *const ()) {}
fn clone(ptr: *const ()) -> RawWaker {
RawWaker::new(ptr, &VTABLE)
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, do_nothing, do_nothing, do_nothing);
fn main() {
let raw = RawWaker::new(std::ptr::null(), &VTABLE);
let waker = unsafe { Waker::from_raw(raw) };
let context = Context::from_waker(&waker);
// use the context
} Obviously this is ugly, inconvinient and contains error-prone unsafe code to just literally do nothing. As a last resort, at least have someting like |
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 aContext
.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.
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:
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 implementFuture
for all context types. Iftask::current
is used, the type of the context is known.The text was updated successfully, but these errors were encountered: