Expand description
floop is a more convenient and less error-prone replacement for loop { select! { ... }} with support for any runtime, no-std, and no-alloc.
floop is not a replacement for select without a loop and does not need to be put inside a loop (it generates its own loop).
unlike loop select, the futures don’t need to be cancel safe, as futures are not cancelled when another arm finishes
(loop select requires cancel-safe futures or work arounds to make futures cancel safe, floop doesn’t,
the worst part is that loop select doesn’t warn you when a future is not cancel safe, it just silently causes unspecified behaviour),
futures also don’t need to be fused, unpin, or have some other obscure property.
attempts are made to generate rust-analyzer friendly code when an error occures, instead of just panicking.
floop is a proc-macro, but it does not depend on syn, it uses unsynn, so compile times shouldn’t be too bad.
like loop select, each arm can mutate shared state without synchronization, but the futures can’t, the futures run concurrently, the arms serially, both are async.
arms can also have conditions (including if let conditions),
each arm’s condition is only evaluated when the future would be created/recreated, not while its’s running.
a before and after arm can also be added (both are optional), before runs at the start of the loop, after runs at the end.
§Syntax
syntax is difficult to explain so here’s an example (the functions are not part of the macro):
floop! {
// you need to specify whether `floop` should be `biased` or `unbiased`.
unbiased
// the main difference to normal select is that `=` has been replaced with `in`.
// ↓↓
foo in timer::every(Duration::from_millis(40)) => println!("tick"),
// `after` doesn't need to be last, and `before` doesn't need to be first,
// but putting them at the start and end is more readable.
after => at_the_end_of_loop(),
// and the condition has been moved before the future,
// this makes `if let` conditions more rust-analyzer-friendly.
// the condition and future need to be separated by a comma.
// ↓
(bar, baz) in if should_receive_messages(), receive() => {}
}
// note: `floop` evaluates to a future, so you'll need to await it (it's a expression),
// or otherwise poll it to completion.§Biasedness
biased just polls the futures in order,
unbiased doesn’t use randomness, instead the futures are polled in order,
but polling doesn’t start at the 0th future, it starts 1 after the last-polled future
(and wraps around the end when trying to poll a future past the end),
this ensures that each future has an equal oppertunity to finish,
but without the added non-determism and complexity of random polling.
§Break
each arm can break, but break only stops the specific arm, the loop stops once all arms broke (meaning break doesn’t cancel any futures).
the value of the future generated by floop is a tuple of all break values.
before and after can’t break, they are either not inside a loop, or the break is turned into a compilation error if they are inside a loop,
the compiler error may not be perticularly readable (the macro doesn’t detect break, it just generates code that attempts to make all usages of break invalid),
if a type error mentions __FloopDontBreak then you tried to break in before or after.
§Continue
continue works as expected, if you call continue the arm stops early, but floop continues running and,
the arm will run again the next time its future finishes.
continue also works in before and after, unconditionally continuing in before would likely result in a infinite loop.
§Return
returning the outer function from inside a async block is not possible (only the block is broken), and floop generates a async block,
but returning can be enabled by adding return after biased/unbiased,
when returning is enabled floop does not evaluated to a future, it directly awaits the future it created.
like with break, the compiler error when trying to return without enabling return is not very readable,
if a type error mentions __FloopDontReturn then you tried to return without adding return to the start of the macro.
? (the question mark operator) also works as expected, as it just desugars to a return and match.
Footgun: returning results in all local variables being dropped, which results in all unfinished futures being canceled, which brings back cancel safety. however non-cancel-safety is less bad in this case as futures are only cancelled once when your function exits, not while it is running.
example:
/// runs a single connection on some imaginary protocol.
async fn connection() {
floop! {
unbiased
return
message in receive_message() => {
match &message {
"foo" | "bar" | "baz" => proccess_message(message),
_ => {
eprintln!("invalid message, terminating connection")l
return;
}
}
}
// this future will be canceled when `foo` returns, but that's fine as the entire connection is terminated.
_ in send_messages() => { ... }
}
}§Footguns
you can use await inside the arms, but they run serially, not concurrently,
so waiting in a arm blocks the entire loop, which can lead to deadlocks or low throughput/high latency.
(this is the same behaviour as loop select)
conditions are only checked after any arm finishes, not while waiting, for example the following would randomly deadlock:
floop! {
// biasedness has no effect on the result of this example.
unbiased;
// if the coinflip fails pending will run, which never finishes, so the coin won't have a chance to be flipped again,
// and it will eventually fail.
message in if coinflip(), receive_message => process_message(message),
_ in pending() => {}
// one fix would be to use a timer to make `floop` constantly reevaluate the condition, but that's a hack.
// fix in timer::ever(Duration::from_millis(20)) => {}
}the best solution would be to ensure conditions are only affected by code inside the arms.
§Non-footguns
using references to futures would result in a finished future being polled (which is very bad), and would be a giant footgun,
but attempts to do so are detected at compile time and cause a readable, detailed, and friendly compilation error.
(owned futures containing references, such as a socket’s receive future with a reference to the buffer to store a UDP datagram in, in are fine)
for example the following won’t compile:
let mut future = async {
...
}
floop! {
// you can also use the `footgun` keyword to disable the check and make this compile,
// you shouldn't in most cases,
// but there may be some future that should be polled after it is finished.
// (e.g. `foo in footgun &mut future`).
bad in &mut future => println!("YAY, poll after finish!"),
}however the footgun detector may get disabled by compiler bugs (it currently uses work arounds for 140655),
please open an issue if you can find any edge case where a future of type &mut T or &T compiles.
§Cancel safety
the future returned by floop is not cancel safe, even if all futures used inside it are.
none of the futures used inside floop need to be cancel safe, floop does not cancel futures.
§Implementation detatils
floop expands to roughly the following code, ignoring all the work arounds, edge cases, footgun prevention, break, and unbiasedness.
(arms are destructured into ($pattern_n in if $condition_n, $future_expr_n => $arm_n)
async {
enum Output<T0..TN> {
VariantN(TN)
}
let future_n = None;
let pin_n = None;
loop {
#before;
if future_n.is_none() && condition_n {
future_n = future_expr_n;
future_ref = ...; // setting a option to Some and getting a mutable reference to its value is surprisngly annoying.
unsafe {
pin_n = Pin::new_unchecked(future_ref);
}
}
let function = |ctx| {
if let Poll::Ready(ready) = pin_n.poll(ctx) {
return Poll::Ready(Output::VariantN(ready));
}
}
let function = poll_fn(function);
match function.await {
Output::VariantN => {
#arm_n;
pin_n = None;
future_n = None;
}
}
#after;
}
}§error[E0034]: multiple applicable items in scope
this error occures when another error prevents type inference used in the const autoref specialization that detects footguns at compile time from working correctly, fixing all other errors should result in this error being fixed, please open an issue if it doesn’t.
Macros§
- floop
- see the crate level docs