[llvm-dev] [RFC] Error handling in LLVM libraries.
Craig, Ben via llvm-dev
llvm-dev at lists.llvm.org
Wed Feb 3 10:55:19 PST 2016
This is mostly in line with what I thought the answers would be, so +1
from me, at least for the concept. I haven't peered into the
implementation.
On 2/3/2016 12:18 PM, Lang Hames wrote:
> Hi Craig,
>
> > TypedError Err = foo();
> > // no checking in between
> > Err = foo();
>
> This will cause an abort - the assignment operator for TypedError
> checks that you're not overwriting an unhanded error.
>
> > TypedError Err = foo();
> > functionWithHorribleSideEffects();
> > if (Err) return;
>
> This is potentially reasonable code - it's impossible to distinguish
> in general from:
>
> TypedError Err = foo();
> functionWithPerfectlyReasonableSideEffects();
> if (Err) return;
>
> That said, to avoid problems related to this style we can offer style
> guidelines. Idiomatic usage of the system looks like:
>
> if (auto Err = foo())
> return Err;
> functionWithHorribleSideEffects();
>
> This is how people tend to write error checks in most of the LLVM code
> I've seen to date.
>
> > Do you anticipate giving these kinds of errors to out of tree projects? If so, are there any
> kind of binary compatibility guarantee?
>
> Out of tree projects can use the TypedError.h header and derive their
> own error classes. This is all pure C++, I don't think there are
> binary compatibility issues.
>
> > What about errors that should come out of constructors? Or
> <shudder> destructors?
>
> TypedError can't be "thrown" in the same way that C++ exceptions can.
> It's an ordinary C++ value. You can't return an error from a
> constructor, but you can pass a reference to an error in and set that.
> In general the style guideline for a "may-fail" constructors would be
> to write something like this:
>
> class Foo {
> public:
>
> static TypedErrorOr<Foo> create(int X, int Y) {
> TypedError Err;
> Foo F(X, Y, Err);
> if (Err)
> return std::move(Err);
> return std::move(F);
> }
>
> private:
> Foo(int x, int y, TypedError &Err) {
> if (x == y) {
> Err = make_typed_error<BadFoo>();
> return;
> }
> }
> };
>
> Then you have:
>
> TypedErrorOr<Foo> F = Foo::create(X, Y);
>
>
> The only way to catch failure of a destructor is for the class to hold
> a reference to a TypedError, and set that. This is extremely difficult
> to do correctly, but as far is I know all error schemes suffer from
> poor interaction with destructors. In LLVM failing destructors are
> very rare, so I don't anticipate this being a problem in general.
>
> > If a constructor fails and doesn't establish it's invariant, what
> will prevent the use of that invalid object?
>
> If the style guideline above is followed the invalid object will never
> be returned to the user. Care must be taken to ensure that the
> destructor can destruct the partially constructed object, but that's
> always the case.
>
> > How many subclasses do you expect to make of TypedError? Less than
> 10? More than 100?
>
> This is a support library, so it's not possible to reason about how
> many external clients will want to use it in their projects, or how
> many errors they would define. In LLVM I'd like to see us adopt a
> 'less-is-more' approach: New error types should be introduced
> sparingly, and each new error type should require a rationale for its
> existence. In particular, distinct error types should only be
> introduced when it's reasonable for some client to make a meaningful
> distinction between them. If an error is only being returned in order
> to produce a string diagnostic, a generic StringDiagnosticError should
> suffice.
>
> Answering your question more directly: In the LLVM code I'm familiar
> with I can see room for more than 10 error types, but fewer than 100.
>
> > How common is it to want to handle a specific error code in a
> non-local way? In my experience, I either want a specific error
> handled locally, or a fail / not-failed from farther away. The answer
> to this question may influence the number of subclasses you want to make.
>
> Errors usually get handled locally, or just produce a diagnostic and
> failure, however there are some cases where we want non-local recovery
> from specific errors. The archive-walking example I gave earlier is
> one such case. You're right on the point about subclasses too - that's
> what I was hoping to capture with my comment above: only introduce an
> error type if it's meaningful for a client to distinguish it from
> other errors.
>
> > Are file, line number, and / or call stack information captured?
> I've found file and line number information to be incredibly useful
> from a productivity standpoint.
>
> I think that information is helpful for programmatic errors, but those
> are better represented by asserts or "report_fatal_error". This system
> is intended to support modelling of non-programmatic errors - bad
> input, resource failures and the like. For those, the specific point
> in the code where the error was triggered is less useful. If such
> information is needed, this system makes it easy to break on the
> failure point in a debugger.
>
> Cheers,
> Lang.
>
>
> On Wed, Feb 3, 2016 at 6:15 AM, Craig, Ben via llvm-dev
> <llvm-dev at lists.llvm.org <mailto:llvm-dev at lists.llvm.org>> wrote:
>
> I've had some experience dealing with rich error descriptions
> without exceptions before. The scheme I used was somewhat similar
> to what you have. Here are some items to consider.
>
> * How will the following code be avoided? The answer may be
> compile time error, runtime error, style recommendations, or maybe
> something else.
>
> TypedError Err = foo();
> // no checking in between
> Err = foo();
>
> * How about this?
>
> TypedError Err = foo();
> functionWithHorribleSideEffects();
> if(Err) return;
>
> * Do you anticipate giving these kinds of errors to out of tree
> projects? If so, are there any kind of binary compatibility
> guarantee?
>
> * What about errors that should come out of constructors? Or
> <shudder> destructors?
>
> * If a constructor fails and doesn't establish it's invariant,
> what will prevent the use of that invalid object?
>
> * How many subclasses do you expect to make of TypedError? Less
> than 10? More than 100?
>
> * How common is it to want to handle a specific error code in a
> non-local way? In my experience, I either want a specific error
> handled locally, or a fail / not-failed from farther away. The
> answer to this question may influence the number of subclasses you
> want to make.
>
> * Are file, line number, and / or call stack information
> captured? I've found file and line number information to be
> incredibly useful from a productivity standpoint.
>
>
> On 2/2/2016 7:29 PM, Lang Hames via llvm-dev wrote:
>> Hi All,
>>
>> I've been thinking lately about how to improve LLVM's error model
>> and error reporting. A lack of good error reporting in Orc and
>> MCJIT has forced me to spend a lot of time investigating
>> hard-to-debug errors that could easily have been identified if we
>> provided richer error information to the client, rather than just
>> aborting. Kevin Enderby has made similar observations about the
>> state of libObject and the difficulty of producing good error
>> messages for damaged object files. I expect to encounter more
>> issues like this as I continue work on the MachO side of LLD. I
>> see tackling the error modeling problem as a first step towards
>> improving error handling in general: if we make it easy to model
>> errors, it may pave the way for better error handling in many
>> parts of our libraries.
>>
>> At present in LLVM we model errors with std::error_code (and its
>> helper, ErrorOr) and use diagnostic streams for error reporting.
>> Neither of these seem entirely up to the job of providing a solid
>> error-handling mechanism for library code. Diagnostic streams are
>> great if all you want to do is report failure to the user and
>> then terminate, but they can't be used to distinguish between
>> different kinds of errors, and so are unsuited to many use-cases
>> (especially error recovery). On the other hand, std::error_code
>> allows error kinds to be distinguished, but suffers a number of
>> drawbacks:
>>
>> 1. It carries no context: It tells you what went wrong, but not
>> where or why, making it difficult to produce good diagnostics.
>> 2. It's extremely easy to ignore or forget: instances can be
>> silently dropped.
>> 3. It's not especially debugger friendly: Most people call the
>> error_code constructors directly for both success and failure
>> values. Breakpoints have to be set carefully to avoid stopping
>> when success values are constructed.
>>
>> In fairness to std::error_code, it has some nice properties too:
>>
>> 1. It's extremely lightweight.
>> 2. It's explicit in the API (unlike exceptions).
>> 3. It doesn't require C++ RTTI (a requirement for use in LLVM).
>>
>> To address these shortcomings I have prototyped a new
>> error-handling scheme partially inspired by C++ exceptions. The
>> aim was to preserve the performance and API visibility of
>> std::error_code, while allowing users to define custom error
>> classes and inheritance relationships between them. My hope is
>> that library code could use this scheme to model errors in a
>> meaningful way, allowing clients to inspect the error information
>> and recover where possible, or provide a rich diagnostic when
>> aborting.
>>
>> The scheme has three major "moving parts":
>>
>> 1. A new 'TypedError' class that can be used as a replacement for
>> std::error_code. E.g.
>>
>> std::error_code foo();
>>
>> becomes
>>
>> TypedError foo();
>>
>> The TypedError class serves as a lightweight wrapper for the real
>> error information (see (2)). It also contains a 'Checked' flag,
>> initially set to false, that tracks whether the error has been
>> handled or not. If a TypedError is ever destructed without being
>> checked (or passed on to someone else) it will call
>> std::terminate(). TypedError cannot be silently dropped.
>>
>> 2. A utility class, TypedErrorInfo, for building error class
>> hierarchies rooted at 'TypedErrorInfoBase' with custom RTTI. E.g.
>>
>> // Define a new error type implicitly inheriting from
>> TypedErrorInfoBase.
>> class MyCustomError : public TypedErrorInfo<MyCustomError> {
>> public:
>> // Custom error info.
>> };
>>
>> // Define a subclass of MyCustomError.
>> class MyCustomSubError : public TypedErrorInfo<MyCustomSubError,
>> MyCustomError> {
>> public:
>> // Extends MyCustomError, adds new members.
>> };
>>
>> 3. A set of utility functions that use the custom RTTI system to
>> inspect and handle typed errors. For example
>> 'catchAllTypedErrors' and 'handleTypedError' cooperate to handle
>> error instances in a type-safe way:
>>
>> TypedError foo() {
>> if (SomeFailureCondition)
>> return make_typed_error<MyCustomError>();
>> }
>>
>> TypedError Err = foo();
>>
>> catchAllTypedErrors(std::move(Err),
>> handleTypedError<MyCustomError>(
>> [](std::unique_ptr<MyCustomError> E) {
>> // Handle the error.
>> return TypedError(); // <- Indicate success from handler.
>> }
>> )
>> );
>>
>>
>> If your initial reaction is "Too much boilerplate!" I understand,
>> but take comfort: (1) In the overwhelmingly common case of simply
>> returning errors, the usage is identical to std::error_code:
>>
>> if (TypedError Err = foo())
>> return Err;
>>
>> and (2) the boilerplate for catching errors is usually easily
>> contained in a handful of utility functions, and tends not to
>> crowd the rest of your source code. My initial experiments with
>> this scheme involved updating many source lines, but did not add
>> much code at all beyond the new error classes that were introduced.
>>
>>
>> I believe that this scheme addresses many of the shortcomings of
>> std::error_code while maintaining the strengths:
>>
>> 1. Context - Custom error classes enable the user to attach as
>> much contextual information as desired.
>>
>> 2. Difficult to drop - The 'checked' flag in TypedError ensures
>> that it can't be dropped, it must be explicitly "handled", even
>> if that only involves catching the error and doing nothing.
>>
>> 3. Debugger friendly - You can set a breakpoint on any custom
>> error class's constructor to catch that error being created.
>> Since the error class hierarchy is rooted you can break on
>> TypedErrorInfoBase::TypedErrorInfoBase to catch any error being
>> raised.
>>
>> 4. Lightweight - Because TypedError instances are just a pointer
>> and a checked-bit, move-constructing it is very cheap. We may
>> also want to consider ignoring the 'checked' bit in release mode,
>> at which point TypedError should be as cheap as std::error_code.
>>
>> 5. Explicit - TypedError is represented explicitly in the APIs,
>> the same as std::error_code.
>>
>> 6. Does not require C++ RTTI - The custom RTTI system does not
>> rely on any standard C++ RTTI features.
>>
>> This scheme also has one attribute that I haven't seen in
>> previous error handling systems (though my experience in this
>> area is limited): Errors are not copyable, due to ownership
>> semantics of TypedError. I think this actually neatly captures
>> the idea that there is a chain of responsibility for dealing with
>> any given error. Responsibility may be transferred (e.g. by
>> returning it to a caller), but it cannot be duplicated as it
>> doesn't generally make sense for multiple people to report or
>> attempt to recover from the same error.
>>
>> I've tested this prototype out by threading it through the
>> object-creation APIs of libObject and using custom error classes
>> to report errors in MachO headers. My initial experience is that
>> this has enabled much richer error messages than are possible
>> with std::error_code.
>>
>> To enable interaction with APIs that still use std::error_code I
>> have added a custom ECError class that wraps a std::error_code,
>> and can be converted back to a std::error_code using the
>> typedErrorToErrorCode function. For now, all custom error code
>> classes should (and do, in the prototype) derive from this
>> utility class. In my experiments, this has made it easy to thread
>> TypedError selectively through parts of the API. Eventually my
>> hope is that TypedError could replace std::error_code for
>> user-facing APIs, at which point custom errors would no longer
>> need to derive from ECError, and ECError could be relegated to a
>> utility for interacting with other codebases that still use
>> std::error_code.
>>
>> So - I look forward to hearing your thoughts. :)
>>
>> Cheers,
>> Lang.
>>
>> Attached files:
>>
>> typed_error.patch - Adds include/llvm/Support/TypedError.h (also
>> adds anchor() method to lib/Support/ErrorHandling.cpp).
>>
>> error_demo.tgz - Stand-alone program demo'ing basic use of the
>> TypedError API.
>>
>> libobject_typed_error_demo.patch - Threads TypedError through the
>> binary-file creation methods (createBinary, createObjectFile,
>> etc). Proof-of-concept for how TypedError can be integrated into
>> an existing system.
>>
>>
>>
>> _______________________________________________
>> LLVM Developers mailing list
>> llvm-dev at lists.llvm.org <mailto:llvm-dev at lists.llvm.org>
>> http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev
>
> --
> Employee of Qualcomm Innovation Center, Inc.
> Qualcomm Innovation Center, Inc. is a member of Code Aurora Forum, a Linux Foundation Collaborative Project
>
>
> _______________________________________________
> LLVM Developers mailing list
> llvm-dev at lists.llvm.org <mailto:llvm-dev at lists.llvm.org>
> http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev
>
>
--
Employee of Qualcomm Innovation Center, Inc.
Qualcomm Innovation Center, Inc. is a member of Code Aurora Forum, a Linux Foundation Collaborative Project
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160203/1f3dc6b8/attachment.html>
More information about the llvm-dev
mailing list