A way to impl async trait without allocation on async fn call

Fancyflame/impl_trait (github.com)
This is a snippet of test fn. Miri passed.

fn main() {
    let boxed = <Foo as MyTrait>::call();
    assert_eq!(pollster::block_on(boxed), "hello world");

Although we already have async-trait crate, it allocates on every async fn called. This crate can bypass the overhead. The test function is at lib.rs
I hope someone can tell me whether this implementation is sound, thanks!

This isn't ok because you're using u8 to store the futures. A future might contain uninitialized memory (e.g. padding), but an u8 is not allowed to be uninitialized. To see this, try this code:

struct WithPadding {
    small: u8,
    large: u32,

async fn call() -> String {
    let val = WithPadding {
        small: 0,
        large: 10,
    "hello world".into()
test test::main ... error: Undefined Behavior: constructing invalid value at .0[0]: encountered uninitialized bytes
  --> src/specified_box.rs:43:17
43 |                 buffer,
   |                 ^^^^^^ constructing invalid value at .0[0]: encountered uninitialized bytes
   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
   = note: BACKTRACE:
   = note: inside `specified_box::SpecifiedBox::<buffer_sel::Align4<12>, dyn std::future::Future<Output = std::string::String>>::new::<[async fn body@src/lib.rs:46:31: 54:6]>` at src/specified_box.rs:43:17: 43:23
note: inside `<test::Foo as test::MyTrait>::call`
  --> src/lib.rs:66:13
66 |             Self::Output::new(call())
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^
note: inside `test::main`
  --> src/lib.rs:72:21
72 |         let boxed = <Foo as MyTrait>::call();
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^
note: inside closure
  --> src/lib.rs:71:15
70 |     #[test]
   |     ------- in this procedural macro expansion
71 |     fn main() {
   |               ^
   = note: this error originates in the attribute macro `test` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to previous error

To fix this, you can use MaybeUninit<u8> instead.

1 Like

Thanks a lot! I fixed it with MaybeUinit<u8> as you said, now it passed Miri.
But I have a question, [u8] here can only be read with the type who initializes it, so theoretically what the padding bytes exactly are doesn't matter, will it still be UB? Or is reading uninitialized bytes illegal, no matter what I'm going to do with them?

It's legal in the same sense as unreachable_unchecked is legal. And means essentially the same thing: code which compiler can assume wouldn't be executed and which it may safely remove.

I prefer to use unreachable_unchecked, though, it declares my intent much more precisely.

The instant you create an uninitialized u8, you have UB. Even if you don't read it at all.

Got it. Thanks.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.