Skip to main content

Crate floop

Crate floop 

Source
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, cancel-safety doesn’t matter, 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.
    // note the comma between the condition and future.
    //                                        ↓
    (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.

§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)

§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.

§Footguns

using return should work as expected, it returns from your function,
however returning results in all local variables being dropped, which results in all unfinished futures being canceled, which brings back the entire cancel safety issue.

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 a future 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 changed inside the arms, that way they will be revealuted every time they may change.

§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;
    }
}

Macros§

floop
see the crate level docs