Skip to content

Commit 8f51da3

Browse files
authored
[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.
1 parent c24f66e commit 8f51da3

File tree

6 files changed

+457
-24
lines changed

6 files changed

+457
-24
lines changed

orc-rt/include/orc-rt/Error.h

Lines changed: 189 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#ifndef ORC_RT_ERROR_H
1010
#define ORC_RT_ERROR_H
1111

12+
#include "orc-rt-c/config.h"
1213
#include "orc-rt/CallableTraitsHelper.h"
1314
#include "orc-rt/Compiler.h"
1415
#include "orc-rt/RTTI.h"
@@ -19,20 +20,71 @@
1920
#include <string>
2021
#include <type_traits>
2122

23+
#if ORC_RT_ENABLE_EXCEPTIONS
24+
#include <exception>
25+
#endif // ORC_RT_ENABLE_EXCEPTIONS
26+
2227
namespace orc_rt {
2328

29+
class Error;
30+
2431
/// Base class for all errors.
2532
class ErrorInfoBase : public RTTIExtends<ErrorInfoBase, RTTIRoot> {
2633
public:
27-
virtual std::string toString() const = 0;
34+
virtual std::string toString() const noexcept = 0;
35+
36+
private:
37+
#if ORC_RT_ENABLE_EXCEPTIONS
38+
friend class Error;
39+
friend Error restore_error(ErrorInfoBase &&);
40+
41+
virtual void throwAsException() = 0;
42+
43+
virtual Error restoreError() noexcept = 0;
44+
#endif // ORC_RT_ENABLE_EXCEPTIONS
45+
};
46+
47+
/// Like RTTI-extends, but injects error-related helper methods.
48+
template <typename ThisT, typename ParentT>
49+
class ErrorExtends : public ParentT {
50+
public:
51+
static_assert(std::is_base_of_v<ErrorInfoBase, ParentT>,
52+
"ErrorExtends must extend ErrorInfoBase derivatives");
53+
54+
// Inherit constructors and isA methods from ParentT.
55+
using ParentT::isA;
56+
using ParentT::ParentT;
57+
58+
static char ID;
59+
60+
static const void *classID() noexcept { return &ThisT::ID; }
61+
62+
const void *dynamicClassID() const noexcept override { return &ThisT::ID; }
63+
64+
bool isA(const void *const ClassID) const noexcept override {
65+
return ClassID == classID() || ParentT::isA(ClassID);
66+
}
67+
68+
static bool classof(const RTTIRoot *R) { return R->isA<ThisT>(); }
69+
70+
#if ORC_RT_ENABLE_EXCEPTIONS
71+
void throwAsException() override {
72+
throw ThisT(std::move(static_cast<ThisT &>(*this)));
73+
}
74+
75+
Error restoreError() noexcept override;
76+
#endif // ORC_RT_ENABLE_EXCEPTIONS
2877
};
2978

79+
template <typename ThisT, typename ParentT>
80+
char ErrorExtends<ThisT, ParentT>::ID = 0;
81+
3082
/// Represents an environmental error.
3183
class ORC_RT_NODISCARD Error {
3284

3385
template <typename T> friend class Expected;
3486

35-
friend Error make_error(std::unique_ptr<ErrorInfoBase> Payload);
87+
friend Error make_error(std::unique_ptr<ErrorInfoBase> Payload) noexcept;
3688

3789
template <typename... HandlerTs>
3890
friend Error handleErrors(Error E, HandlerTs &&...Hs);
@@ -48,7 +100,7 @@ class ORC_RT_NODISCARD Error {
48100
/// Move-construct an error. The newly constructed error is considered
49101
/// unchecked, even if the source error had been checked. The original error
50102
/// becomes a checked success value.
51-
Error(Error &&Other) {
103+
Error(Error &&Other) noexcept {
52104
setChecked(true);
53105
*this = std::move(Other);
54106
}
@@ -57,7 +109,7 @@ class ORC_RT_NODISCARD Error {
57109
/// you cannot overwrite an unhandled error. The current error is then
58110
/// considered unchecked. The source error becomes a checked success value,
59111
/// regardless of its original state.
60-
Error &operator=(Error &&Other) {
112+
Error &operator=(Error &&Other) noexcept {
61113
// Don't allow overwriting of unchecked values.
62114
assertIsChecked();
63115
setPtr(Other.getPtr());
@@ -73,48 +125,58 @@ class ORC_RT_NODISCARD Error {
73125
}
74126

75127
/// Create a success value.
76-
static Error success() { return Error(); }
128+
static Error success() noexcept { return Error(); }
77129

78130
/// Error values convert to true for failure values, false otherwise.
79-
explicit operator bool() {
131+
explicit operator bool() noexcept {
80132
setChecked(getPtr() == nullptr);
81133
return getPtr() != nullptr;
82134
}
83135

84136
/// Return true if this Error contains a failure value of the given type.
85-
template <typename ErrT> bool isA() const {
137+
template <typename ErrT> bool isA() const noexcept {
86138
return getPtr() && getPtr()->isA<ErrT>();
87139
}
88140

141+
#if ORC_RT_ENABLE_EXCEPTIONS
142+
void throwOnFailure() {
143+
if (auto P = takePayload())
144+
P->throwAsException();
145+
}
146+
#endif // ORC_RT_ENABLE_EXCEPTIONS
147+
89148
private:
90-
Error() = default;
149+
Error() noexcept = default;
91150

92-
Error(std::unique_ptr<ErrorInfoBase> ErrInfo) {
151+
Error(std::unique_ptr<ErrorInfoBase> ErrInfo) noexcept {
93152
auto RawErrPtr = reinterpret_cast<uintptr_t>(ErrInfo.release());
94153
assert((RawErrPtr & 0x1) == 0 && "ErrorInfo is insufficiently aligned");
95154
ErrPtr = RawErrPtr | 0x1;
96155
}
97156

98-
void assertIsChecked() {
157+
void assertIsChecked() noexcept {
99158
if (ORC_RT_UNLIKELY(!isChecked() || getPtr())) {
100159
fprintf(stderr, "Error must be checked prior to destruction.\n");
101160
abort(); // Some sort of JIT program abort?
102161
}
103162
}
104163

105-
template <typename ErrT = ErrorInfoBase> ErrT *getPtr() const {
164+
template <typename ErrT = ErrorInfoBase> ErrT *getPtr() const noexcept {
106165
return reinterpret_cast<ErrT *>(ErrPtr & ~uintptr_t(1));
107166
}
108167

109-
void setPtr(ErrorInfoBase *Ptr) {
168+
void setPtr(ErrorInfoBase *Ptr) noexcept {
110169
ErrPtr = (reinterpret_cast<uintptr_t>(Ptr) & ~uintptr_t(1)) | (ErrPtr & 1);
111170
}
112171

113-
bool isChecked() const { return ErrPtr & 0x1; }
172+
bool isChecked() const noexcept { return ErrPtr & 0x1; }
114173

115-
void setChecked(bool Checked) { ErrPtr = (ErrPtr & ~uintptr_t(1)) | Checked; }
174+
void setChecked(bool Checked) noexcept {
175+
ErrPtr = (ErrPtr & ~uintptr_t(1)) | Checked;
176+
}
116177

117-
template <typename ErrT = ErrorInfoBase> std::unique_ptr<ErrT> takePayload() {
178+
template <typename ErrT = ErrorInfoBase>
179+
std::unique_ptr<ErrT> takePayload() noexcept {
118180
static_assert(std::is_base_of_v<ErrorInfoBase, ErrT>,
119181
"ErrT is not an ErrorInfoBase subclass");
120182
std::unique_ptr<ErrT> Tmp(getPtr<ErrT>());
@@ -127,10 +189,22 @@ class ORC_RT_NODISCARD Error {
127189
};
128190

129191
/// Create an Error from an ErrorInfoBase.
130-
inline Error make_error(std::unique_ptr<ErrorInfoBase> Payload) {
192+
inline Error make_error(std::unique_ptr<ErrorInfoBase> Payload) noexcept {
131193
return Error(std::move(Payload));
132194
}
133195

196+
#if ORC_RT_ENABLE_EXCEPTIONS
197+
198+
template <typename ThisT, typename ParentT>
199+
Error ErrorExtends<ThisT, ParentT>::restoreError() noexcept {
200+
return make_error(
201+
std::make_unique<ThisT>(std::move(*static_cast<ThisT *>(this))));
202+
}
203+
204+
inline Error restore_error(ErrorInfoBase &&EIB) { return EIB.restoreError(); }
205+
206+
#endif // ORC_RT_ENABLE_EXCEPTIONS
207+
134208
/// Construct an error of ErrT with the given arguments.
135209
template <typename ErrT, typename... ArgTs> Error make_error(ArgTs &&...Args) {
136210
static_assert(std::is_base_of<ErrorInfoBase, ErrT>::value,
@@ -497,7 +571,7 @@ template <typename T> T &cantFail(Expected<T &> E) {
497571

498572
/// Convert the given error to a string. The error value is consumed in the
499573
/// process.
500-
inline std::string toString(Error Err) {
574+
inline std::string toString(Error Err) noexcept {
501575
assert(Err && "Cannot convert success value to string");
502576
std::string ErrMsg;
503577
handleAllErrors(std::move(Err),
@@ -506,15 +580,111 @@ inline std::string toString(Error Err) {
506580
}
507581

508582
/// Simple string error type.
509-
class StringError : public RTTIExtends<StringError, ErrorInfoBase> {
583+
class StringError : public ErrorExtends<StringError, ErrorInfoBase> {
510584
public:
511585
StringError(std::string ErrMsg) : ErrMsg(std::move(ErrMsg)) {}
512-
std::string toString() const override { return ErrMsg; }
586+
std::string toString() const noexcept override { return ErrMsg; }
513587

514588
private:
515589
std::string ErrMsg;
516590
};
517591

592+
/// APIs for C++ exception interop.
593+
#if ORC_RT_ENABLE_EXCEPTIONS
594+
595+
class ExceptionError : public ErrorExtends<ExceptionError, ErrorInfoBase> {
596+
public:
597+
ExceptionError(std::exception_ptr E) : E(std::move(E)) {}
598+
std::string toString() const noexcept override;
599+
void throwAsException() override { std::rethrow_exception(E); }
600+
601+
private:
602+
mutable std::exception_ptr E;
603+
};
604+
605+
namespace detail {
606+
607+
// In general we need to wrap a return type of T with an Expected.
608+
template <typename RetT> struct ErrorWrapImpl {
609+
typedef Expected<RetT> return_type;
610+
611+
template <typename OpFn> static return_type run(OpFn &&Op) { return Op(); }
612+
};
613+
614+
// If the return is already an Expected value then we don't need to add
615+
// an additional level of wrapping.
616+
template <typename RetT> struct ErrorWrapImpl<Expected<RetT>> {
617+
typedef Expected<RetT> return_type;
618+
619+
template <typename OpFn> static return_type run(OpFn &&Op) { return Op(); }
620+
};
621+
622+
// Errors stay errors.
623+
template <> struct ErrorWrapImpl<Error> {
624+
typedef Error return_type;
625+
626+
template <typename OpFn> static return_type run(OpFn &&Op) { return Op(); }
627+
};
628+
629+
// void returns become Error returns.
630+
template <> struct ErrorWrapImpl<void> {
631+
typedef Error return_type;
632+
633+
template <typename OpFn> static return_type run(OpFn &&Op) {
634+
Op();
635+
return Error::success();
636+
}
637+
};
638+
639+
template <typename Callable>
640+
struct ErrorWrap
641+
: public CallableTraitsHelper<detail::ErrorWrapImpl, Callable> {};
642+
643+
} // namespace detail
644+
645+
/// Run the given callback capturing any exceptions thrown into an
646+
/// Error / Expected failure value.
647+
///
648+
/// The return type depends on the return type of the callback:
649+
/// - void callbacks return Error
650+
/// - Error callbacks return Error
651+
/// - Expected<T> callbacks return Expected<T>
652+
/// - other T callbacks return Expected<T>
653+
///
654+
/// If the operation succeeds then...
655+
/// - If its result is non-void it is returned as an Expected<T> success
656+
/// value
657+
/// - If its result is void then Error::success() is retured
658+
///
659+
/// If the operation fails then...
660+
/// - If the exception type is std::unique_ptr<ErrorInfoBase> (i.e. a throw
661+
/// orc_rt failure value) then an Error is constructed to hold the
662+
/// failure value.
663+
/// - If the exception has any other type then it's captured as an
664+
/// ExceptionError.
665+
///
666+
/// The scheme allaws...
667+
/// 1. orc_rt::Error values that have been converted to exceptions via
668+
/// Error::throwOnFailure to be converted back into Errors without loss
669+
/// of dynamic type info.
670+
/// 2. Other Exceptions caught by this function to be converted back into
671+
/// exceptions via Error::throwOnFailure without loss of dynamic
672+
/// type info.
673+
674+
template <typename OpFn>
675+
typename detail::ErrorWrap<OpFn>::return_type
676+
runCapturingExceptions(OpFn &&Op) noexcept {
677+
try {
678+
return detail::ErrorWrap<OpFn>::run(std::forward<OpFn>(Op));
679+
} catch (ErrorInfoBase &EIB) {
680+
return restore_error(std::move(EIB));
681+
} catch (...) {
682+
return make_error<ExceptionError>(std::current_exception());
683+
}
684+
}
685+
686+
#endif // ORC_RT_ENABLE_EXCEPTIONS
687+
518688
} // namespace orc_rt
519689

520690
#endif // ORC_RT_ERROR_H

orc-rt/lib/executor/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
set(files
22
AllocAction.cpp
3+
Error.cpp
34
ResourceManager.cpp
45
RTTI.cpp
56
Session.cpp

orc-rt/lib/executor/Error.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//===- Error.cpp ----------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
//
9+
// Contains the implementation of APIs in the orc-rt/Error.h header.
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#include "orc-rt/Error.h"
14+
15+
#include <system_error>
16+
17+
namespace orc_rt {
18+
19+
#if ORC_RT_ENABLE_EXCEPTIONS
20+
21+
std::string ExceptionError::toString() const noexcept {
22+
std::string Result;
23+
try {
24+
std::rethrow_exception(E);
25+
} catch (std::exception &SE) {
26+
Result = SE.what();
27+
E = std::current_exception();
28+
} catch (std::error_code &EC) {
29+
try {
30+
// Technically 'message' itself can throw.
31+
Result = EC.message();
32+
} catch (...) {
33+
Result = "std::error_code (.message() call failed)";
34+
}
35+
E = std::current_exception();
36+
} catch (std::string &ErrMsg) {
37+
Result = ErrMsg;
38+
E = std::current_exception();
39+
} catch (...) {
40+
Result = "C++ exception of unknown type";
41+
E = std::current_exception();
42+
}
43+
return Result;
44+
}
45+
46+
#endif // ORC_RT_ENABLE_EXCEPTIONS
47+
48+
} // namespace orc_rt

orc-rt/unittests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ add_orc_rt_unittest(CoreTests
1717
CallableTraitsHelperTest.cpp
1818
EndianTest.cpp
1919
ErrorTest.cpp
20+
ErrorExceptionInteropTest.cpp
2021
ExecutorAddressTest.cpp
2122
IntervalMapTest.cpp
2223
IntervalSetTest.cpp

0 commit comments

Comments
 (0)