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

Translating between Result ⟷ exceptions #53

Closed
dtolnay opened this issue Mar 2, 2020 · 1 comment
Closed

Translating between Result ⟷ exceptions #53

dtolnay opened this issue Mar 2, 2020 · 1 comment
Labels
new binding Involves adding a new public type to cxx crate or rust/cxx.h header

Comments

@dtolnay
Copy link
Owner

dtolnay commented Mar 2, 2020

Currently we catch and abort on unwinding across the FFI boundary in either direction; it's left to the user to communicate all failures via ordinary return values or out-parameters.

It would be nice if C++ exceptions were exposed to Rust as an idiomatic Result error value. For C++ functions that are declared in the bridge as returning a Result, we would not abort on exceptions but instead marshal them across as some kind of cxx::Exception object.

#[cxx::bridge]
mod ffi {
    extern "C" {
        fn f() -> Result<()>;
    }
}

Conversely, Rust functions that are declared as returning a Result could transmit failures as an exception on the C++ side.

#[cxx::bridge]
mod ffi {
    extern "Rust" {
        fn g() -> Result<()>;
    }
}

In both cases, long term we would want the translation to be customizable because not all codebases use exceptions; some would prefer a mapping of Rust Result to Leaf or Outcome. Some thoughts in #16.

@dtolnay
Copy link
Owner Author

dtolnay commented Mar 15, 2020

I started putting together a design for the exception-to-Result direction of this.

I'm trying to leave open the possibility of different codebases having different try-catch logic for extracting messages from exceptions, while still providing a reasonable default conversion. I think there will be a way for us to accommodate this with an expression SFINAE that looks like the following:

struct trycatch {
  template <typename T> trycatch(T);
  static char use_default;
};

template <typename Try, typename Fail,
          typename = decltype(trycatch(std::declval<Try>()).use_default)>
static void trycatch(Try &&func, Fail &&fail) noexcept try {
  func();
} catch (const std::exception &e) {
  fail(e.what());
}

which we invoke in generated code roughly as:

// union with the same layout as std::result::Result<T, cxx::Exception>
rust::Result<T> result;

trycatch(
    [&] {
      new (&result) rust::Result<T>(the_function(args...));
    },
    [&](rust::Exception e) noexcept {
      new (&result) rust::Result<T>(std::move(e));
    });

return result;

In this implementation, CXX provides a default try-catch based on std::exception::what but the user can replace it by providing any function template with this signature in their header:

template <typename Try, typename Fail>
void trycatch(Try &&func, Fail &&fail) noexcept;

Since our generated code includes the user's header before our default provided trycatch implementation, according to https://timsong-cpp.github.io/cppwp/basic.scope.hiding their trycatch function hides the dummy trycatch struct and the expression SFINAE makes our default provided one not exist.

As an example in Folly's case the codebase might use this which provides more type information:

template <typename Try, typename Fail>
static void trycatch(Try &&func, Fail &&fail) noexcept try {
  func();
} catch (const std::exception &e) {
  fail(folly::toStdString(folly::exceptionStr(e)));
} catch (...) {
  fail("<unknown exception>");
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new binding Involves adding a new public type to cxx crate or rust/cxx.h header
Projects
None yet
Development

No branches or pull requests

1 participant