I understand that unsized locals are unstable currently due to implementation issues, among other things. I'd like to propose an alternative, mainly for the async ecosystem, but also generally usable in other situations.
High Level Overview
Fn Declaration and Body
A fn in the form fn return_unsized_trait() -> dyn Trait
or fn return_unsized_array() -> [u32]
would compile with this feature. Unlike impl Trait
, the return values of these functions do not need to match in size, vtable or length.
Example:
fn return_unsized_debug(x: bool) -> dyn Debug {
if x { "foo" } else { 123 }
}
fn return_unsized_array(empty: bool) -> [u32] {
if empty { [] } else { [1, 2, 3] }
}
Fn Calls
In the simple case, calling a fn that returns an unsized parameter is possible if it appears in return position:
fn return_unsized_debug(empty: bool) -> dyn Debug {
return_unsized_array(empty)
}
In the more complex but general case, calling a fn that returns an unsized argument requires specifying a Place:
fn my_boxed_future() -> Box<dyn Future<Output = ()>> {
my_unsized_future() in Box // /!\ New syntax /!\
}
This will require a new trait lang item in core::ops
, which I've tenatively called InPlace
. Name not final, of course.
Trait Objects & async fn
Given a trait, like:
trait DebugSnapshot {
type Output: Debug + ?Sized;
fn snapshot(&self) -> Self::Output;
}
You can call snapshot
on a concrete type, granting a possibly sized return value, or you can coerce it into its unsized version, dyn DebugSnapshot<Output = dyn Debug>
. This is backed by a separate vtable, which handles the unsized return value calling convention & then calls the original implementation.
This also works for Fn types, so FnOnce() -> i32
can be coerced into FnOnce() -> dyn Debug
with the additional glue vtable.
In the async case, anonymous associated types are generated for each method. To avoid the need to specify every single associated type in the trait, methods referring to unspecified types will not be callable. This allows the following to work:
trait AsyncFoo {
async fn foo(&self);
async fn bar(&self);
}
// dyn AsyncFoo<foo() = dyn Future<...>> & dyn AsyncFoo<bar() = dyn Future<...>>
// could point to the same vtable. But if a method is not included in a coercion
// it can be safely removed from the vtable.
An alternative in this case, which feels a little too magical to me, is that a dyn trait with associated types could implicitly make all unspecified associated types dyn, utilizing the bounds found in the declaration.
Nitty Gritty Implementation Details
Possible implementation: Rust Playground
This is not final, and this is not the preferred implementation. What I'd like to show first is how it works from the language point of view, and then show how the underlying calling convention & implementation could work.
Other Possibilities
Since the placement syntax works for any type, you could utilize some crate to provide inline stack storage if the returned unsized value is small enough with suitable alignment. e.g. my_future() in Inline<dyn Future, 4 /* usizes wide & aligned */>
Or, if arbitrary expressions are allowed, you could utilize a crate like Bumpalo to place the unsized value into it, and then receive a &mut
reference. e.g. let node: &mut dyn GraphNode = my_unsized_graph_node() in bump;