Limited unsized values for boxing futures with AFIDT

First, allow functions to be coerced from returning a sized type to an unsized type (wrapping with a trampoline function, discussed later):

async fn foo() -> i32 {
  123
}

...

let unsized_fn: fn() -> dyn Future<Output = i32> = foo;

Then allow functions to receive a single unsized argument, restricted to the last parameter in the parameter list:

fn boxing_fn<T: ?Sized>(unsized: T) -> Box<T> {
  // ... implementation omitted
  // Layout::from_value(&unsized), alloc, non_overlapping copy, and Box::from_raw
}

With these two restrictions lifted, the following is allowed:

let boxed_future = boxing_fn(unsized_fn());

But of course, this is still disallowed:

let future = unsized_fn();

Then in dyn traits, unspecified associated types are turned into an unsized return value IF the associated type's trait bounds are dyn compatible.

pub trait AsyncRunnable {
  // Sugar for `fn run() -> impl Future<Output = ()>`
  // As a dyn trait this becomes `fn run() -> dyn Future<Output = ()>`
  async fn run(self: Box<Self>);
}

let boxed_runnable: Box<dyn AsyncRunnable> = ...;
let boxed_run = boxing_fn(boxed_runnable.run());

More specific trait bounds can be introduced too. e.g. dyn AsyncRunnable<run(..): Send> which augments the return type to return dyn Future<Output = ()> + Send.


Implementation Details

This is where this idea dies.

Because the form is restricted to outer_call(args..., inner_call(args...)) we can pass outer_call as a "continuation" to invoke after inner_call but within the stack frame of the sized return value.

This is pseudo code at best, just to show what the implementation could look like. For brevity it assumes that drops and panics don't happen (hence the lack of ManuallyDrop and friends), and ignores unsafe.

fn transformed_boxing_fn<T: ?Sized>(args: NonNull<()>, ret: NonNull<Box<T>>, unsized: NonNull<T>) {
  // load arguments from args
  // call boxing_fn (possibly inlined)
  // store result in ret
}

fn unsized_fn_trampoline(
  boxing_fn: fn(NonNull<()>, NonNull<()>, NonNull<dyn Future<Output = i32>>),
  boxing_fn_args: NonNull<()>,
  boxing_fn_ret: NonNull<()>,
  unsized_fn_args: ...
) {
  let result = foo(...unsized_fn_args); // Doing the original call within the trampoline
  // Call the boxing function passed into the trampoline
  boxing_fn(boxing_fn_args, boxing_fn_ret, NonNull::from(&mut result));
}

...

// Transformation that takes place at the callsite:
let boxed_future = {
  let boxing_fn_args = ();
  let boxing_fn_ret = MaybeUninit::<Box<dyn Future<Output = i32>>>::uninit();
  // This is the function that was generated by the original coercion to an unsized_fn.
  unsized_fn_trampoline(
    NonNull::from(&boxing_fn_args),
    NonNull::from(&mut boxing_fn_ret).cast(),
    transformed_boxing_fn
  );
  boxing_fn_ret.assume_init()
};
1 Like

Looks like it is similar/alternative idea to that one? Dyn async traits, part 10: Box box box · baby steps

Yeah, the major difference though is this makes no assumptions about the existence of Box (which doesn't exist on no_std without alloc). And for example, Bumpalo's Bump::alloc could be used instead of Box::new (assuming both are updated to take an unsized argument).

Hopefully, custom alllcators will become stable

Proof of concept implementation: Rust Playground

The call_unsize macro is a bit of a mess. But hopefully it shows how the implementation could work.