[pre-pre-RFC] Unsized Return Values & Placement New

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;

1 Like

There were several proposals for placement new in the context of Rust. In fact, it was even implemented on nightly for a while, but later removed, due to multiple important unresolved issues and vague semantics.

There is also an open RFC for placement-by-return, which has been open since 2020 with little progress and significant issues.

Given that, the feature is certainly something that a lot of people have thought about. Any new proposal should explain how the past blockers are expected to be resolved, and how it's different from outstanding proposals.

3 Likes

I would like to highlight my crate emplacable, which is designed for exactly this problem, and at first glance looks a lot like your Playground code.

2 Likes

The moveit crate also bears mentioning, and supports stable Rust; specifically its New trait which models in-place construction, and Emplace, which provides the ability to write Box::emplace(Type::ctor()). It only provides pinning construction (since any address-sensitive types must be pinned), but the implementation pattern extends to unpinned construction as well.

What it doesn't cover is unsized values; it only covers in-place construction of statically sized types.

3 Likes

Looking at the linked RFC for placement-by-return, it looks more complicated than the hacked together playground example.

At a language level, silently changing the calling convention of the function/closure depending on whether the return value is unsized feels more viable than the continuation-like transform that could cause code bloat for no_std.

Unsized return values & locals as previously implemented on nightly feels like it should be avoided in general because of stack size limits. I could imagine accidentally writing *boxed_slice (forgetting the &) which could result in megabytes of data being thrown onto the stack which could have consequences if not noticed immediately.

The coercion of traits from sized to unsized associated types isn't covered by the linked RFC either, and I consider it a core capability here as it can massively simplify handling traits with multiple distinct associated types.

Lastly, I still haven't spent time exploring how the placement syntax could place into an expression instead of a type. I'm not a fan of having both expressions and types accepted in the same spot and I'm not sure how to disambiguate the two (or if it even needs disambiguation).

What I dig is that moveit can be used to in-place construction of C++ objects too, by integrating with cxx (it's also leveraged by the autocxx crate, which already powers some bindings).

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.