Crate susync

Source
Expand description

§Overview

An util crate to complete futures through a handle. Its main purpose is to bridge async Rust and callback-based APIs.

Inspired on the future_handles crate.

The susync crate uses standard library channels under the hood. It uses thread-safe primitives but expects low contention, so it uses a single SpinMutex for shared state. It should also work on no_std environments but it is not tested. By design handles are allowed to race to complete the future so it is ok to call complete on handle of a completed future. More info here.

§Examples

Channel-like API:

async fn func() -> Option<u32> {
    let (future, handle) = susync::create();

    func_with_callback(|res| {
        handle.complete(res);
    });

    future.await.ok()
}

Scoped API:

async fn func() -> Option<u32> {
    let future = susync::suspend(|handle| {
        func_with_callback(|res| {
            handle.complete(res);
        });
    });

    future.await.ok()
}

§Thread safety

Currently it uses thread safe primitives so keep in mind that overhead.

§Danger!

Do NOT do this, it will block forever!

async fn func() {
    let (future, handle) = susync::create();
    // Start awaiting here...
    future.await.unwrap();
    // Now we'll never be able set the result!
    handle.complete(1);
}

Awaiting a SuspendFuture before setting the result with SuspendHandle will cause a deadlock!

§Macro shortcuts

If your use case is to simply to call complete on the SuspendHandle with the arguments of a callback, the sus macro is an option.

// With one closure argument
fn func_one_arg(func: impl FnOnce(u32)) {
   func(42);
}
// Here the arguments of the closure will be passed to `complete`
let result = sus!(func_one_arg(|x| {})).await.unwrap();
assert_eq!(result, 42);

// With two closure arguments
fn func_two_args(func: impl FnOnce(u32, f32)) {
   func(42, 69.0);
}
// Here the arguments of the closure will be passed to `complete`
let result = sus!(func_two_args(|x, y| {})).await.unwrap();
assert_eq!(result, (42, 69.0));

The SuspendFuture will hold the arguments in a tuple or just a type in case it’s just one argument. You can ignore arguments by using the wildcard _ token.

fn func_with_callback(func: impl FnOnce(u32, &str)) {
   func(42, "ignored :(");
}
// Here the second argument gets ignored
let result = sus!(func_with_callback(|x, _| {})).await.unwrap();
assert_eq!(result, 42);

§Macro invariants

The callback argument must be a closure. This macro implementation revolves around generating a new closure that runs the original and also forwards the arguments to SuspendHandle::complete. The logic is wrapped in a suspend that returns a future. For this reason it’s not possible to accept anything else other than a closure because it’s not possible to infer the future output type.

The sus macro calls to_owned on all arguments so all arguments in the callback must implement ToOwned trait. This is to allow reference arguments like &str and any other reference of a type that implements Clone (check ToOwned implementors).

Still looking for ways to overcome this limitation and give the user the freedom to choose how to complete the future.

// Does *NOT* implement `ToOwned`
struct RefArg(i32);
fn func(f: impl FnOnce(&RefArg)) {
   f(&RefArg(42));
}
// *ILLEGAL*: Here the argument will clone a reference and try to outlive the scope
let RefArg(result) = sus!(func(|arg| {})).await.unwrap();

Unfortunately the error message is not very friendly because the error is trait bounds but they are implicit to the macro. So if you ever get an error message like below it is likely a reference is being passed to complete instead of an owned value.

error[E0521]: borrowed data escapes outside of closure
  --> susync/src/lib.rs:127:22
   |
13 | let RefArg(result) = sus!(func(|arg| {})).await.unwrap();
   |                      ^^^^^^^^^^^---^^^^^^
   |                      |          |
   |                      |          `arg` is a reference that is only valid in the closure body
   |                      `handle` declared here, outside of the closure body
   |                      `arg` escapes the closure body here
   |
   = note: this error originates in the macro `sus`

In case there are more than one callback argument the macro only generates the boilerplate for the last one in the argument list. For no particular reason, just vague assumption that result callbacks come last.

fn func_with_callback(func1: impl FnOnce(i32), func2: impl FnOnce(f32)) {
   func1(42);
   func2(69.0);
}
// Here the macro only applies to the last closure
let result = sus!(func_with_callback(|_i| {}, |f| {})).await.unwrap();
assert_eq!(result, 69.0);

Macros§

sus
Generate the boilerplate for the use case where the future output is equal, or similar, to the callback arguments.

Structs§

SuspendFuture
Future to suspend execution until SuspendHandle completes.
SuspendHandle
Handle to signal a SuspendFuture that a result is ready.

Enums§

SuspendError
The error returned by SuspendFuture in the case of the error variant.

Functions§

create
Creates a channel-like pair of SuspendFuture and SuspendHandle.
suspend
Creates a SuspendFuture from a closure.

Type Aliases§

SuspendResult
An ergonomic Result type to wrap standard Result with error SuspendError.