Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Foreign" exceptions and catch_unwind? #35

Open
madsmtm opened this issue Nov 2, 2021 · 12 comments
Open

"Foreign" exceptions and catch_unwind? #35

madsmtm opened this issue Nov 2, 2021 · 12 comments

Comments

@madsmtm
Copy link

madsmtm commented Nov 2, 2021

Objective-C have exceptions similar to C++ exceptions (not very familiar with these, maybe some of this apply there as well?), and catching them require using the objc_begin_catch and objc_end_catch helper functions, similar to Rust calling the internal __rust_panic_cleanup function as part of catch_unwind.

I'd like the ability to create a function similar to catch_unwind, but which catches Objective-C exceptions instead of Rust panics. This would be an unsafe function intended to be used on the FFI boundary just before sending an Objective-C message (equivalent to calling an "C-unwind" function).

In the objc-crate's exception handling this is currently done by letting an Objective-C compiler (like clang) generate a helper function that uses @try and @catch. This adds unnecessary overhead, and is bad for cross-compilation and all that. Using the underlying intrinsic core::intrinsics::try we can avoid the overhead in objc by effectively re-implementing most of catch_unwind, see an example implementation here: SSheldon/rust-objc-exception@a351a8a.

But obviously core::intrinsics::try is an intrinsic, and not intended to be stabilized, so I'd like to discuss a way we could stabilize it, or parts of the catch_unwind implementation, to allow this use-case.

Originally posted as an internals thread.

@madsmtm
Copy link
Author

madsmtm commented Nov 2, 2021

An idea that would accommodate my use-case would be to add something similar to std::panicking::#try, but with the error type being generic and found from a closure:

pub unsafe fn new_try<R, E, F: FnOnce() -> R, FE: FnOnce(*mut u8) -> E>(f: F, fe: FE) -> Result<R, E> {
    // ...

    #[cold]
    unsafe fn cleanup(payload: *mut u8) -> E {
        fe(payload)
    }

    // ...
}

pub unsafe fn r#try<R, F: FnOnce() -> R>(f: F) -> Result<R, Box<dyn Any + Send>> {
    new_try(f, |payload| {
        let obj = unsafe { Box::from_raw(__rust_panic_cleanup(payload)) };
        panic_count::decrease();
        obj
    })
}

@BatmanAoD
Copy link
Member

I'm sorry to be the bearer of bad news, but catch_unwind is explicitly UB for anything other than Rust panics, with no plan on our roadmap to change this; and, although it's called out in RFC-2945 as something we'd like to address, I suspect this would be a very uphill battle.

One of the requirements for this group's work has always been to avoid specifying how the exception handling mechanism works. Currently, it's implemented using the "native" exception handling mechanism in LLVM, which in theory would make it possible for catch_unwind to do what you're saying, at least for C++ (and I would hope that since Objective-C is an LLVM-compiled language, it would work for that as well). But I believe the compiler team has discussed other approaches that would be incompatible with the native mechanism.

That said, I do see how this would be useful, and I don't know for sure that specifying it would be too restrictive for the compiler implementation. I'm not sure who has the expertise to discuss this further; @Amanieu perhaps?

@BatmanAoD
Copy link
Member

I also don't remember who on the compiler team had alternative ideas/hopes/designs for unwinding.

@BatmanAoD
Copy link
Member

catch_unwind is no longer explicitly UB, but it does currently (...and previously) just abort the process.

@madsmtm
Copy link
Author

madsmtm commented Sep 9, 2024

Yeah I've followed rust-lang/rust#128321, it's excellent that it's now specified instead of UB, that solves a lot of my concerns here, so thanks a lot for that!

Now I no longer feel like Rust needs to expose a way to handle custom panics, so I'm going to close this.

The only real remaining issue is that, as you said, that the process just aborts, which is obviously a terrible user experience and hard to debug, but that's a "quality of implementation" issue, and the best way forwards for me here would be to integrate the Objective-C(++) exception catching mechanism in Rust's libunwind itself (please correct mere here if I'm wrong?)

@madsmtm madsmtm closed this as completed Sep 9, 2024
@nbdd0121
Copy link

nbdd0121 commented Sep 9, 2024

I think it's still desirable to be able to interact directly with foreign exceptions. Let's use this issue to keep it on radar for the WG.

@nbdd0121 nbdd0121 reopened this Sep 9, 2024
@BatmanAoD BatmanAoD changed the title Custom catch_unwind? "Foreign" exceptions and catch_unwind? Sep 14, 2024
@BatmanAoD
Copy link
Member

In that case, I'm renaming the thread to be a little more general.

@purplesyringa
Copy link

I am also interested in this. I'd like to both throw and catch foreign exceptions through Rust frames, at least on a subset of platforms. I'm currently doing this in Itanium ABI EH-conformant platforms by manually invoking _Unwind_RaiseException for throwing and core::intrinsics::catch_unwind for catching. I understand that this is undocumented behavior, but it works well in practice, so getting a stable API would be useful.

@BatmanAoD
Copy link
Member

@purplesyringa Thanks for adding a use-case; could you explain a little more about why you need _Unwind_RaiseException rather than throwing and catching Rust panics (possibly with panic_any if you need a payload)? Do you also need the exceptions to be caught by C++ catch?

@purplesyringa
Copy link

I'm working on what is effectively an alternative panic/exception implementation for Rust, with a significant focus on performance. As such, going through the default Rust machinery is counterproductive. My main problems are:

  • Panics are considered cold, which inhibits inlining in many contexts.
  • As panic_unwind is decoupled from std, communication between std and the unwind runtime goes through extern "Rust", which requires many indirect calls, at least without LTO.
  • libunwind always allocates the _Unwind_Exception on the heap with Box, which is slow. But as panics are seldom nested, using a small thread-local buffer/stack for the in-flight exceptions turns out to be both applicable in 99% cases and much more efficient.
  • Catching and then immediately rethrowing a panic can be implemented by simply calling _Unwind_ThrowException, but Rust currently needs to deallocate the _Unwind_Exception, extract the box, return to the caller, then pack the box again and allocate a new expression object.

To be clear, this is not just theory, and I have seen significant performance improvements by implementing a direct Itanium interface in my usecases. I'm hoping to publish a crate soon, I can send a link here if you're interested.

@BatmanAoD
Copy link
Member

Are you calling _Unwind_RaiseException via "normal" FFI (declaring with an extern block and calling that), or some other mechanism?

@purplesyringa
Copy link

I'm directly calling the extern "C-unwind" fn _Unwind_RaiseException, yes. You can take a look at the code here: https://github.com/iex-rs/lithium/blob/master/src/backend/itanium.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants