How can AssertUnwindSafe be used with the CatchUnwind future
Asked Answered
C

1

5

I want to be able to pass an mutable reference to function, but catch unwinds that may come from that function. The purpose is for use in writing some test wrappers (setup, teardown), not general error handling.

If I were using typical sync code, I can get this to compile and work...

struct MyStruct {
    n: u32
}

fn my_func(s: &mut MyStruct) {
    s.n += 1;
    panic!("Oh no!");
}

fn main() {
    let mut ctx = MyStruct { n: 1 };
    let mut wrapper = std::panic::AssertUnwindSafe(&mut ctx);
    let result = std::panic::catch_unwind(move || {
        my_func(*wrapper);
    });
    
    // Do some cleanup of `ctx` here.

    if let Err(err) = result {
        std::panic::resume_unwind(err);
    }
}

However, I haven't been able to figure out how to do this using futures and async/await. In that case, I would be trying to call a function that has been declared as async. I have tried various things like the code below:

async fn run_async(s: &mut MyStruct) {
    s.n += 1;
    panic!("Oh no!");
}

#[tokio::main]
async fn main() {
    let mut ctx = MyStruct { n : 1 };
    let wrapper = std::panic::AssertUnwindSafe(&mut ctx);
    let result = async move {
        run_async(*wrapper).catch_unwind().await
    }.await;
    
    println!("{:?}", result);
}

However, I typically will end up with an error such as:

the type &mut MyStruct may not be safely transferred across an unwind boundary`.

I was under the belief that AssertUnwindSafe was supposed to help with these problems, as they did with the sync code. But there is obviously something I'm not understanding at the intersection of AssertUnwindSafe and the async/await.

Coincident answered 17/1, 2021 at 15:45 Comment(0)
M
6

With std::panic::catch_unwind, the closure provided must be UnwindSafe, and using a mutable reference inside will make the closure not implement UnwindSafe. This is why wrapping the reference and moving that instead works.

However, with futures::future::FutureExt::catch_unwind, the future provided must be UnwindSafe, and the future generated by run_async doesn't care if the reference came from an AssertUnwindSafe wrapper or not since you're unwrapping it before you call it. So, you should assert that the future itself is safe:

use futures::future::FutureExt;

struct MyStruct {
    n: i32
}

async fn run_async(s: &mut MyStruct) {
    s.n += 1;
    panic!("Oh no!");
}

#[tokio::main]
async fn main() {
    let mut ctx = MyStruct { n : 1 };
    let result = async move {
        // AssertUnwindSafe moved to the future
        std::panic::AssertUnwindSafe(run_async(&mut ctx)).catch_unwind().await
    }.await;
    
    println!("{:?}", result);
}
Motion answered 17/1, 2021 at 17:52 Comment(1)
Thanks! Although I didn't fully specify in the question, I also wanted to use ctx after awaiting the async block. Thus, I needed a line like let wrapper = &mut ctx; before the async block, then run_async(wrapper) to ensure that ctx isn't moved into the async block, but a mutable reference of it was. However, understanding that AssertUnwindSafe should be wrapping the future, not the value going into the future, was what I was missing.Coincident

© 2022 - 2024 — McMap. All rights reserved.