4 ms·
On top of the sibling comments pointing out that Rust doesn't use LLVM coroutines, which allocate each coroutine frame by default, you misunderstand the control
by Rusky 8y ago
On top of the sibling comments pointing out that Rust doesn't use LLVM coroutines, which allocate each coroutine frame by default, you misunderstand the control flow of Rust async functions:
A Rust future is also "responsible for making sure that it gets resumed appropriately if it suspends." Rust's await is even cheaper than C++/LLVM's- it doesn't do anything with an event loop either; it just returns, without atomically messing with any coroutine handle (which doesn't exist).
Further, Rust futures are poll-based, not callback-based. Your bullet list looks like this:
* Function call to the main entry point (which is really more of a constructor)
* The future creates its frame as a value type (not as a heap allocation) and initializes it with the call arguments
* Function return (not a suspend, because it returns an initialized future rather than Poll::Pending- so again more of a constructor return)
* (sometime later, often immediately) Function call to `poll` (initial resume)
* `poll` executes until a suspend point (function return)
The initial call to `poll` can happen because the constructed future was put on the event loop and scheduled, as you describe, but it can also happen merely because its caller is also a future that is already running somehow.
Also, note that "the event loop" is a rather loose concept here- it's just "the thing that calls top-level `poll` functions." The language doesn't ever submit anything to it, or signal it, or anything. Each call to `poll` receives a handle to that top-level caller, and each leaf future stashes that handle somewhere that will signal it when it's ready to resume. This even works in embedded microcontroller scenarios.
And finally, your eager execution example is addressed in Rust in two ways:
* First, ownership and the borrow checker prevent dangling references like this statically. In your particular case, the caller would increment the refcount as part of cloning an `Rc`, and then move the clone into the future. (Unless, of course, the future was short-lived enough, and you decided to take advantage of that to pass in a `&'a Value.Fn` instead.)
* Second, because future construction (the first three bullet points) and future execution (the last two) are decoupled, you can write your own function that does any extra construction work in the cases that it's actually necessary. For example, using Rust's syntax for async blocks:
pub fn render_to_llvm(.., fn_val: *Value.Fn, ..) -> impl Future<Output = ()> {
fn_val.base.ref();
async {
defer fn_val.base.deref(comp);
// ...
}
}