Files
llvm-project/orc-rt/unittests/ErrorExceptionInteropTest.cpp
Lang Hames 8f51da369e [orc-rt] Add Error / Exception interop. (#172247)
The ORC runtime needs to work in diverse codebases, both with and
without C++ exceptions enabled (e.g. most LLVM projects compile with
exceptions turned off, but regular C++ codebases will typically have
them turned on). This introduces a tension in the ORC runtime: If a C++
exception is thrown (e.g. by a client-supplied callback) it can't be
ignored, but orc_rt::Error values will assert if not handled prior to
destruction. That makes the following pattern fundamentally unsafe in
the ORC runtime:

```
if (auto Err = orc_rt_operation(...)) {
  log("failure, bailing out"); // <- may throw if exceptions enabled
  // Exception unwinds stack before Error is handled, triggers Error-not-checked
  // assertion here.
  return Err;
}
```

We can resolve this tension by preventing any exceptions from unwinding
through ORC runtime stack frames. We can do this while preserving
exception *values* by catching all exceptions (using `catch (...)`) and
capturing their values as a std::exception_ptr into an Error.

This patch adds APIs to simplify conversion between C++ exceptions and
Errors. These APIs are available only when enabled when the ORC runtime
is configured with ORC_RT_ENABLE_EXCEPTIONS=On (the default).

- `ExceptionError` wraps a std::exception_ptr.

- `runCapturingExceptions` takes a T() callback and converts any
exceptions thrown by the body into Errors. If T is Expected or Error
already then runCapturingExceptions returns the same type. If T is void
then runCapturingExceptions returns an Error (returning Error::success()
if no exception is thrown). If T is any other type then
runCapturingExceptions returns an Expected<T>.

- A new Error::throwOnFailure method is added that converts failing
values into thrown exceptions according to the following rules:
1. If the Error is of type ExceptionError then std::rethrow_exception is
called on the contained std::exception_ptr to rethrow the original
exception value.
2. If the Error is of any other type then std::unique_ptr<T> is thrown
where T is the dynamic type of the Error.

These rules allow exceptions to be propagated through the ORC runtime as
Errors, and for ORC runtime errors to be converted to exceptions by
clients.
2025-12-15 16:10:46 +11:00

214 lines
6.7 KiB
C++

//===- ErrorExceptionInterorTest.cpp --------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Test interoperability between errors and exceptions.
//
//===----------------------------------------------------------------------===//
#include "orc-rt-c/config.h"
#include "orc-rt/Error.h"
#include "gtest/gtest.h"
#include <system_error>
using namespace orc_rt;
namespace {
class CustomError : public ErrorExtends<CustomError, ErrorInfoBase> {
public:
std::string toString() const noexcept override { return "CustomError"; }
};
} // anonymous namespace
#if ORC_RT_ENABLE_EXCEPTIONS
#define EXCEPTION_TEST(X) \
do { \
X; \
} while (false)
#else
#define EXCEPTION_TEST(X) GTEST_SKIP() << "Exceptions disabled"
#endif
TEST(ErrorExceptionInterorTest, SuccessDoesntThrow) {
// Test that Error::success values don't throw when throwOnFailure is called.
EXCEPTION_TEST({
try {
auto E = Error::success();
E.throwOnFailure();
} catch (...) {
ADD_FAILURE() << "Unexpected throw";
}
});
}
TEST(ErrorExceptionInteropTest, VoidReturnSuccess) {
// Test that runCapturingExceptions returns Error::success for void()
// function objects.
EXCEPTION_TEST({
bool Executed = false;
auto Result = runCapturingExceptions([&]() { Executed = true; });
static_assert(std::is_same_v<decltype(Result), Error>,
"Expected return type to be Error");
EXPECT_FALSE(Result); // Error::success() evaluates to false
EXPECT_TRUE(Executed);
});
}
TEST(ErrorExceptionInteropTest, ErrorReturnPreserved) {
// Test that plain Errors returned from runCapturingExceptions are returned
// as expected.
EXCEPTION_TEST({
auto Result = runCapturingExceptions(
[]() -> Error { return make_error<StringError>("original error"); });
EXPECT_TRUE(!!Result);
EXPECT_EQ(toString(std::move(Result)), "original error");
});
}
TEST(ErrorExceptionInteropTest, NonFallibleTReturnWrapped) {
// Test that; for function types returning a non-Error, non-Expected type T;
// runCapturingExceptions returns an Expected<T>.
EXCEPTION_TEST({
auto Result = runCapturingExceptions([]() { return 42; });
static_assert(std::is_same_v<decltype(Result), Expected<int>>,
"Expected return type to be Expected<int>");
EXPECT_TRUE(!!Result);
EXPECT_EQ(*Result, 42);
});
}
TEST(ErrorExceptionInteropTest, ExpectedReturnPreserved) {
// Test that Expected success values are returned as expected.
EXCEPTION_TEST({
auto Result = runCapturingExceptions([]() -> Expected<int> { return 42; });
EXPECT_TRUE(!!Result);
EXPECT_EQ(*Result, 42);
});
}
TEST(ErrorExceptionInteropTest, CatchThrownInt) {
// Check that we can capture a thrown exception into an Error and recognize it
// as a thrown exception.
EXCEPTION_TEST({
auto E = runCapturingExceptions([]() { throw 42; });
EXPECT_TRUE(!!E);
EXPECT_TRUE(E.isA<ExceptionError>());
consumeError(std::move(E));
});
}
TEST(ErrorExceptionInteropTest, RoundTripExceptionThroughError) {
// Check that we can rethrow an exception that has been captured into an
// error without affecting the dynamic type or value (e.g. we don't actually
// rethrow the wrong type / value).
EXCEPTION_TEST({
int Result = 0;
try {
auto E = runCapturingExceptions([]() { throw 42; });
EXPECT_TRUE(!!E);
E.throwOnFailure();
} catch (int N) {
Result = N;
} catch (...) {
ADD_FAILURE() << "Caught unexpected error type";
}
EXPECT_EQ(Result, 42);
});
}
static std::string peekAtErrorMessage(Error &Err) {
std::string Msg;
Err = handleErrors(std::move(Err), [&](std::unique_ptr<ErrorInfoBase> EIB) {
Msg = EIB->toString();
return make_error(std::move(EIB));
});
return Msg;
}
TEST(ErrorExceptionInteropTest, RoundTripErrorThroughException) {
// Test Error → Exception → Error preserves type and message
EXCEPTION_TEST({
auto OriginalErr = make_error<StringError>("hello, error!");
std::string OriginalMsg = peekAtErrorMessage(OriginalErr);
Error RecoveredErr = Error::success();
try {
OriginalErr.throwOnFailure();
} catch (ErrorInfoBase &EIB) {
ErrorAsOutParameter _(RecoveredErr);
RecoveredErr = restore_error(std::move(EIB));
} catch (...) {
ADD_FAILURE() << "Caught unexpected error type";
}
EXPECT_TRUE(RecoveredErr.isA<StringError>());
EXPECT_EQ(toString(std::move(RecoveredErr)), OriginalMsg);
});
}
TEST(ErrorExceptionInteropTest, ThrowErrorAndCatchAsException) {
// Check that we can create an Error value, throw it as an exception, and
// match its dynamic type to a catch handler.
EXCEPTION_TEST({
bool HandlerRan = false;
std::string Msg;
try {
auto E = make_error<CustomError>();
E.throwOnFailure();
} catch (CustomError &E) {
HandlerRan = true;
} catch (ErrorInfoBase &E) {
ADD_FAILURE() << "Failed to downcase error to dynamic type";
} catch (...) {
ADD_FAILURE() << "Caught unexpected error type";
}
});
}
TEST(ErrorExceptionInteropTest, ErrorExceptionToString) {
/// Check that exceptions can be converted to Strings as exepcted.
EXCEPTION_TEST({
{
// std::exception should be converted by calling `.what()`;
class MyException : public std::exception {
public:
~MyException() override {}
const char *what() const noexcept override { return "what"; }
};
EXPECT_EQ(toString(runCapturingExceptions([]() { throw MyException(); })),
"what");
}
{
// std::error_code should be converted by calling `.message()`.
auto EC = std::make_error_code(std::errc::cross_device_link);
std::string ECErrMsg = EC.message();
EXPECT_EQ(toString(runCapturingExceptions([&]() { throw EC; })),
ECErrMsg);
}
{
// std::string should be converted by copying its value.
std::string ErrMsg = "foo";
EXPECT_EQ(toString(runCapturingExceptions([&]() { throw ErrMsg; })),
ErrMsg);
}
{
// Check that exceptions of other types produce the expected
// "unrecognized type" error message:
EXPECT_EQ(toString(runCapturingExceptions([]() { throw 42; })),
"C++ exception of unknown type");
}
});
}