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

Update exception handling to use std::exception_ptr #1180

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

schreter
Copy link
Contributor

This makes the interface between C++ exception handling and Rust Result cleaner and allows passing C++ exception from the inner C++ call to the outer C++ call via Rust's CxxException unmodified, i.e., without losing information.

To allow custom exception types, trait ToCxxException was introduced. With this trait, it's possible to convert a Rust error to the wrapper CxxExeception via a C++ function, thus allowing throwing other exceptions as well. This required changing r#try function into macros, so we can properly default to rust::Error for errors not having ToCxxException defined.

Background: The throw statement in C++ (__cxa_throw) effectively first allocates space on the heap and creates the exception within, then starts unwinding. This can be also done via standard C++11 API in two steps. First, std::make_exception_ptr() creates a new std::exception_ptr, which points into this allocated space and internally is just a pointer (it's a smart pointer much like std::shared_ptr). Then, std::rethrow_exception() can be used to actually throw and/or rethrow this exception.

Basically, the new implementation now uses std::make_exception_ptr() called from Rust to construct an exception for the Result<_, E> and then after returning it back to C++ via CxxResult (which is now BTW smaller, just 8B) the C++ part throws it using std::rethrow_exception().

The exception object can be passed as a pointer via `CxxException`
error through Rust frames.

A method to create the default message-based `rust::Error` and to drop
and clone the exception are provided.
This represents C++ `std::exception_ptr` on the Rust side via
`CxxException` object, which can be mentioned as a return type for
functions creating custom exceptions or evaluating exceptions caught
from C++ (added in further commits).
This makes the interface between C++ exception handling and Rust
`Result` cleaner and allows passing C++ exception from the inner C++
call to the outer C++ call via Rust's `CxxException` unmodified, i.e.,
without losing information.

To allow custom exception types, trait `ToCxxException` was introduced.
With this trait, it's possible to convert a Rust error to the wrapper
`CxxExeception` via a C++ function, thus allowing throwing other
exceptions as well. This required changing `r#try` function into
macros, so we can properly default to `rust::Error` for errors not
having `ToCxxException` defined.

Background: The `throw` statement in C++ (__cxa_throw) effectively
first allocates space on the heap and creates the exception within,
then starts unwinding. This can be also done via standard C++11 API in
two steps. First, `std::make_exception_ptr()` creates a new
`std::exception_ptr`, which points into this allocated space and
internally is just a pointer (it's a smart pointer much like
`std::shared_ptr`). Then, `std::rethrow_exception()` can be used to
actually throw and/or rethrow this exception.

Basically, the new implementation now uses `std::make_exception_ptr()`
called from Rust to construct an exception for the `Result<_, E>` and
then after returning it back to C++ via `CxxResult` (which is now BTW
smaller, just 8B) the C++ part throws it using
`std::rethrow_exception()`.
@schreter schreter force-pushed the std_exception_ptr branch 2 times, most recently from a4c2566 to bb68c92 Compare February 25, 2023 10:29
@schreter
Copy link
Contributor Author

@dtolnay I seem unable to get the "ui" checks correct. With "my" nightly compiler (freshly updated), the output differs from what CI is expecting (mine also prints out the trait). Now, optically, there is no difference (except for $RUST replacing the path and such). I'll try to analyze further.

Any tips getting this to work? E.g., do you use an explicit nightly version?

Further, it seems like msvc compiler ABI is not yet updated to use exception_ptr via a simple pointer. I'll address that in a follow-up commit (I need to change the repr appropriately).

BTW, how to add a reviewer? The "Reviewers" section to the right doesn't allow me to add any.

Thanks.

Since invalid `Result<T>` output changed, the expected file had to be
updated.

The output actually got better, since now the location of the actual
declaration of the offending struct is also shown, not only the usage.
@schreter schreter force-pushed the std_exception_ptr branch 2 times, most recently from dfd6648 to dc203b7 Compare February 25, 2023 11:58
Contrary to the most platforms, which just store a single pointer in
`std::exception_ptr`, MSVC stores two. Add necessary code to handle
this.
Previously, modifying tests.h/cc would not trigger rebuild of the
bridge, effectively preventing test development.
C++ error message returned by `what` must be NUL-terminated. However,
the current copy function only copied the characters, but didn't add
the NUL. Allocate one more byte and set it to NUL.
@schreter
Copy link
Contributor Author

@dtolnay OK, I found the root cause of the ui test problem.

The crate trybuild normalizes /rustlib/src/rust/library/ to $RUST/ and locally on my machine, it gets normalized correctly. However, in CI, the path is /rustc/c5c7d2b37780dac1092e75f12ab97dd56c30861d/library/core/src/fmt/mod.rs:786:1, which does not get normalized, since the path is /library/core/..., which is NOT normalized. No idea why it's different here, but that's the problem.

I'll disable the test for now, since it's not a problem in the implementation, but rather in the trybuild crate. I'll open a bug report on trybuild for that.

…ate.

The normalization in `trybuild` crate doesn't work in the CI (but works
fine locally).
@schreter
Copy link
Contributor Author

I addressed the normalization issue in dtolnay/trybuild#223 and created a PR with the fix in dtolnay/trybuild#224. When that is through, the disabled test can be re-enabled.

Instead, expand the match directly in `expand.rs` to minimize the
public API.
@schreter
Copy link
Contributor Author

@dtolnay the change is now complete (modulo the one disabled ui test, which requires the already-merged, but not released normalization fix dtolnay/trybuild#224 in trybuild).

Can you please review & comment and ideally merge?

The PR is composed of three commits, which are touching individual aspects needed to implement the feature:

  • support for CxxException wrapping C++ std::exception_ptr
  • parser and generator extension to handle the new type
  • use CxxException as the means to transfer the exception
  • and a few fixup commits to get everything working correctly (most notably, the one for MSVC).

This is to make the reviewing easier.

Other exception changes (like panic handling, transparent forwarding of C++ exceptions through Rust frames, etc.) depend on this. I'll publish them when this one is merged.

Thanks.

@schreter
Copy link
Contributor Author

@dtolnay This PR sits here already since one month without any review (as are also the RFCs for further direction). Since most of the other "goodies" we developed internally and want to publish for everyone depend on it (such as panic and bad_alloc handling), it needs to go in first.

It seems like you lost interest in development of cxx crate (though you do some minor maintenance) as I also don't see reactions from you on issues raised against the crate since quite some time. Can you please name a second maintainer who can review and merge the changes? Otherwise the development is stuck. Looking at the contribution amount, @adetaylor contributed second most code, so maybe you can delegate to him (if he cares)?

Again, I don't want to publish a forked crate, I'd like to bring the changes upstream.

Thanks.

@riidefi
Copy link

riidefi commented Jun 6, 2023

This is a really useful feature! A lot of C++ uses exceptions, and letting cxx seamlessly bridge that gap with Rust is awesome. However, in a lot of workplaces and projects, they are entirely forbidden.

I noticed your patch adds a bunch of exception code not checking for the RUST_CXX_NO_EXCEPTIONS preprocessor definition. I am weary that this may be problematic as the behavior used here is not defined in noexcept builds (and in some implementations could possibly terminate).

I really appreciate the work here, but I think it's important that we take one step forward without another back and continue to support the no-except case.

I understand a lot of code still uses exceptions, so this approach is a useful feature for many users. But looking forward, I feel like any discussion about this particular feature is woefully incomplete without addressing the elephant in the room--std::expected. With AppleClang rebasing to LLVM15 just a few days ago, and with all major compiler vendors supporting std::expected: it really does seem like the future of C++ error handling may be.. Rust. Another viable direction could be to just map Rust Results to std::expected types. This comes with all the benefits of Result in Rust in addition to being a very simple, direct mapping. While I again understand this is not specifically relevant to this PR, I have included it for completeness in the broader conversation about the future of cxx and exception handling.

Best

@schreter
Copy link
Contributor Author

schreter commented Jun 6, 2023

@riidefi Thanks for the feedback.

I noticed your patch adds a bunch of exception code not checking for the RUST_CXX_NO_EXCEPTIONS preprocessor definition.

It may be lacking some, I'm not sure. I changed primarily the code which was already protected.
In any case, I hope there are tests which check that the compilation w/o exceptions still works. Knowing the author's code, I'm 99% sure there are :-).

But looking forward, I feel like any discussion about this particular feature is woefully incomplete without addressing the elephant in the room--std::expected.

Right... It would be nice to support it as well, but it's not that simple. The error type must be then transferrable to C++ per value as well, which would quite limit us here.

Aside from that, huge amount of existing code in C++ (including STL) unfortunately uses exceptions at many places. Even with std::expected, we'll most likely see mixed exception handling (e.g., std::bad_alloc still being thrown).

Exception handling has one more advantage - the backtrace of the exception can be recorded in the exception and it's very helpful for diagnosis purposes. Doing the same with Rust's Result and/or std::expected is not that simple, since there is no "hook" for adding information to the error being propagated (and, the propagation can be optimized out). Having callstacks (and additional information) for all exceptions did save us quite a lot of times.

The std:: expected support should be addressed by a different PR. First, we need to clarify the syntax. Then, there is the prerequisite of pass-by-value for error objects, then the mapping of error objects from Rust to C++ (which would be basically ToCxxException, maybe renamed to ToCxxError), etc.

Unfortunately, my patches to cxx are not being reviewed, so I can't make a progress here. I'm strongly considering either forking the crate or just giving up contributions upstream (we have our own private copy with other goodies as well, like panic handling, pass-by-value of opaque Rust types, etc.). You may try bugging the author of the crate.

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

Successfully merging this pull request may close these issues.

2 participants