Commit bf04264d authored by Kenton Varda's avatar Kenton Varda

Implement ability to easily attach context to an exception on the way up the stack.

parent 9058a3ae
......@@ -59,27 +59,49 @@ Exception::Exception(Nature nature, Durability durability, const char* file, int
Array<char> description) noexcept
: file(file), line(line), nature(nature), durability(durability),
description(move(description)) {
bool hasDescription = this->description != nullptr;
void* trace[16];
int traceCount = backtrace(trace, 16);
ArrayPtr<void*> traceArray = arrayPtr(trace, traceCount);
// Must be careful to NUL-terminate this.
whatStr = str(file, ":", line, ": ", nature,
durability == Durability::TEMPORARY ? " (temporary)" : "",
hasDescription ? ": " : "", this->description,
"\nstack: ", strArray(traceArray, " "), '\0');
traceCount = backtrace(trace, 16);
}
Exception::Exception(const Exception& other) noexcept
: file(other.file), line(other.line), nature(other.nature), durability(other.durability),
description(str(other.description)), whatStr(str(other.whatStr)) {}
description(str(other.description)), traceCount(other.traceCount) {
memcpy(trace, other.trace, sizeof(trace[0]) * traceCount);
}
Exception::~Exception() noexcept {}
void Exception::wrapContext(const char* file, int line, Array<char>&& description) {
context = heap<Context>(file, line, move(description), move(context));
}
const char* Exception::what() const noexcept {
return whatStr.begin();
uint contextDepth = 0;
const Maybe<Own<Context>>* contextPtr = &context;
while (*contextPtr != nullptr) {
++contextDepth;
contextPtr = &(***contextPtr).next;
}
Array<Array<char>> contextText = newArray<Array<char>>(contextDepth);
contextDepth = 0;
contextPtr = &context;
while (*contextPtr != nullptr) {
const Context& node = ***contextPtr;
contextText[contextDepth++] =
str(node.file, ":", node.line, ": context: ", node.description, "\n");
contextPtr = &node.next;
}
// Must be careful to NUL-terminate this.
whatBuffer = str(strArray(contextText, ""),
file, ":", line, ": ", nature,
durability == Durability::TEMPORARY ? " (temporary)" : "",
this->description == nullptr ? "" : ": ", this->description,
"\nstack: ", strArray(arrayPtr(trace, traceCount), " "), '\0');
return whatBuffer.begin();
}
// =======================================================================================
......
......@@ -73,6 +73,31 @@ public:
Durability getDurability() const { return durability; }
ArrayPtr<const char> getDescription() const { return description; }
struct Context {
// Describes a bit about what was going on when the exception was thrown.
const char* file;
int line;
Array<char> description;
Maybe<Own<Context>> next;
Context(const char* file, int line, Array<char>&& description, Maybe<Own<Context>>&& next)
: file(file), line(line), description(move(description)), next(move(next)) {}
};
inline Maybe<const Context&> getContext() const {
if (context == nullptr) {
return nullptr;
} else {
return **context;
}
}
void wrapContext(const char* file, int line, Array<char>&& description);
// Wraps the context in a new node. This becomes the head node returned by getContext() -- it
// is expected that contexts will be added in reverse order as the exception passes up the
// callback stack.
const char* what() const noexcept override;
private:
......@@ -81,7 +106,10 @@ private:
Nature nature;
Durability durability;
Array<char> description;
Array<char> whatStr;
Maybe<Own<Context>> context;
void* trace[16];
uint traceCount;
mutable Array<char> whatBuffer;
};
class Stringifier;
......
......@@ -45,9 +45,9 @@ public:
void onRecoverableException(Exception&& exception) override {
text += "recoverable exception: ";
// Only take the first line of "what" because the second line is a stack trace.
// Discard the last line of "what" because it is a stack trace.
const char* what = exception.what();
const char* end = strchr(what, '\n');
const char* end = strrchr(what, '\n');
if (end == nullptr) {
text += exception.what();
} else {
......@@ -58,9 +58,9 @@ public:
void onFatalException(Exception&& exception) override {
text += "fatal exception: ";
// Only take the first line of "what" because the second line is a stack trace.
// Discard the last line of "what" because it is a stack trace.
const char* what = exception.what();
const char* end = strchr(what, '\n');
const char* end = strrchr(what, '\n');
if (end == nullptr) {
text += exception.what();
} else {
......@@ -159,6 +159,45 @@ TEST(Logging, Syscall) {
EXPECT_TRUE(recovered);
}
TEST(Logging, Context) {
MockExceptionCallback mockCallback;
MockExceptionCallback::ScopedRegistration reg(mockCallback);
{
CONTEXT("foo"); int cline = __LINE__;
EXPECT_THROW(FAIL_CHECK("bar"), MockException); int line = __LINE__;
EXPECT_EQ("fatal exception: " + fileLine(__FILE__, cline) + ": context: foo\n"
+ fileLine(__FILE__, line) + ": bug in code: bar\n",
mockCallback.text);
mockCallback.text.clear();
{
int i = 123;
const char* str = "qux";
CONTEXT("baz", i, "corge", str); int cline2 = __LINE__;
EXPECT_THROW(FAIL_CHECK("bar"), MockException); line = __LINE__;
EXPECT_EQ("fatal exception: " + fileLine(__FILE__, cline) + ": context: foo\n"
+ fileLine(__FILE__, cline2) + ": context: baz; i = 123; corge; str = qux\n"
+ fileLine(__FILE__, line) + ": bug in code: bar\n",
mockCallback.text);
mockCallback.text.clear();
}
{
CONTEXT("grault"); int cline2 = __LINE__;
EXPECT_THROW(FAIL_CHECK("bar"), MockException); line = __LINE__;
EXPECT_EQ("fatal exception: " + fileLine(__FILE__, cline) + ": context: foo\n"
+ fileLine(__FILE__, cline2) + ": context: grault\n"
+ fileLine(__FILE__, line) + ": bug in code: bar\n",
mockCallback.text);
mockCallback.text.clear();
}
}
}
} // namespace
} // namespace internal
} // namespace capnproto
......@@ -230,9 +230,31 @@ void Log::fatalFailedSyscallInternal(
abort();
}
void Log::addContextToInternal(Exception& exception, const char* file, int line,
const char* macroArgs, ArrayPtr<Array<char>> argValues) {
exception.wrapContext(file, line, makeDescription(LOG, nullptr, 0, macroArgs, argValues));
}
int Log::getOsErrorNumber() {
int result = errno;
return result == EINTR ? -1 : result;
}
Log::Context::Context(): next(getExceptionCallback()), registration(*this) {}
Log::Context::~Context() {}
void Log::Context::onRecoverableException(Exception&& exception) {
addTo(exception);
next.onRecoverableException(capnproto::move(exception));
}
void Log::Context::onFatalException(Exception&& exception) {
addTo(exception);
next.onFatalException(capnproto::move(exception));
}
void Log::Context::logMessage(ArrayPtr<const char> text) {
// TODO(someday): We could do something like log the context and then indent all log messages
// written until the end of the context.
next.logMessage(text);
}
} // namespace capnproto
......@@ -74,6 +74,14 @@
// fd = SYSCALL(open("/dev/null", O_RDONLY));
// }
//
// * `CONTEXT(...)`: Notes additional contextual information relevant to any exceptions thrown
// from within the current scope. That is, until control exits the block in which CONTEXT()
// is used, if any exception is generated, it will contain the given information in its context
// chain. This is helpful because it can otherwise be very difficult to come up with error
// messages that make sense within low-level helper code. Note that the parameters to CONTEXT()
// are only evaluated if an exception is thrown. This means that any variables used must remain
// valid until the end of the scope.
//
// Notes:
// * Do not write expressions with side-effects in the message content part of the macro, as the
// message will not necessarily be evaluated.
......@@ -145,6 +153,42 @@ public:
int errorNumber, const char* file, int line, const char* callText,
const char* macroArgs, Params&&... params);
class Context: public ExceptionCallback {
public:
Context();
virtual ~Context();
virtual void addTo(Exception& exception) = 0;
virtual void onRecoverableException(Exception&& exception) override;
virtual void onFatalException(Exception&& exception) override;
virtual void logMessage(ArrayPtr<const char> text) override;
private:
ExceptionCallback& next;
ScopedRegistration registration;
};
template <typename Func>
class ContextImpl: public Context {
public:
inline ContextImpl(Func&& func): func(capnproto::move(func)) {}
void addTo(Exception& exception) override {
func(exception);
}
private:
Func func;
};
template <typename Func>
static ContextImpl<RemoveReference<Func>> context(Func&& func) {
return ContextImpl<RemoveReference<Func>>(capnproto::forward<Func>(func));
}
template <typename... Params>
static void addContextTo(Exception& exception, const char* file,
int line, const char* macroArgs, Params&&... params);
private:
static Severity minSeverity;
......@@ -164,6 +208,8 @@ private:
const char* file, int line, const char* call,
int errorNumber, const char* macroArgs, ArrayPtr<Array<char>> argValues)
CAPNPROTO_NORETURN;
static void addContextToInternal(Exception& exception, const char* file, int line,
const char* macroArgs, ArrayPtr<Array<char>> argValues);
static int getOsErrorNumber();
// Get the error code of the last error (e.g. from errno). Returns -1 on EINTR.
......@@ -224,6 +270,13 @@ ArrayPtr<const char> operator*(const Stringifier&, Log::Severity severity);
_errorNumber, __FILE__, __LINE__, #code, #__VA_ARGS__, ##__VA_ARGS__); \
} while (false)
#define CONTEXT(...) \
auto _capnpLoggingContext = ::capnproto::Log::context( \
[&](::capnproto::Exception& exception) { \
return ::capnproto::Log::addContextTo(exception, \
__FILE__, __LINE__, #__VA_ARGS__, ##__VA_ARGS__); \
})
#ifdef NDEBUG
#define DLOG(...) do {} while (false)
#define DCHECK(...) do {} while (false)
......@@ -314,6 +367,13 @@ void Log::reportFailedSyscall(
arrayPtr(argValues, sizeof...(Params)));
}
template <typename... Params>
void Log::addContextTo(Exception& exception, const char* file, int line,
const char* macroArgs, Params&&... params) {
Array<char> argValues[sizeof...(Params)] = {str(params)...};
addContextToInternal(exception, file, line, macroArgs, arrayPtr(argValues, sizeof...(Params)));
}
} // namespace capnproto
#endif // CAPNPROTO_LOGGING_H_
......@@ -25,6 +25,8 @@
namespace capnproto {
Disposer::~Disposer() {}
String::String(const char* value): content(newArray<char>(strlen(value) + 1)) {
strcpy(content.begin(), value);
}
......
......@@ -126,6 +126,27 @@ public:
constructAt(&value, other.value);
}
}
template <typename U>
Maybe(Maybe<U>&& other) noexcept(noexcept(T(capnproto::move(other.value))))
: isSet(other.isSet) {
if (isSet) {
constructAt(&value, capnproto::move(other.value));
}
}
template <typename U>
Maybe(const Maybe<U>& other)
: isSet(other.isSet) {
if (isSet) {
constructAt(&value, other.value);
}
}
template <typename U>
Maybe(const Maybe<U&>& other)
: isSet(other.isSet) {
if (isSet) {
constructAt(&value, *other.ptr);
}
}
Maybe(std::nullptr_t): isSet(false) {}
~Maybe() {
......@@ -227,6 +248,9 @@ private:
union {
T value;
};
template <typename U>
friend class Maybe;
};
template <typename T>
......@@ -235,6 +259,14 @@ public:
Maybe(): ptr(nullptr) {}
Maybe(T& t): ptr(&t) {}
Maybe(std::nullptr_t): ptr(nullptr) {}
template <typename U>
Maybe(const Maybe<U>& other) {
if (other == nullptr) {
ptr = nullptr;
} else {
ptr = other.operator->();
}
}
~Maybe() noexcept {}
......@@ -250,8 +282,119 @@ public:
private:
T* ptr;
template <typename U>
friend class Maybe;
};
// =======================================================================================
// Own<T> -- An owned pointer.
class Disposer {
// Abstract interface for a thing that disposes of some other object. Often, it makes sense to
// decouple an object from the knowledge of how to dispose of it.
protected:
virtual ~Disposer();
public:
virtual void dispose(void* interiorPointer) = 0;
// Disposes of the object that this Disposer owns, and possibly disposes of the disposer itself.
//
// Callers must assume that the Disposer itself is no longer valid once this returns -- e.g. it
// might delete itself. Callers must in particular be sure not to call the Disposer again even
// when dispose() throws an exception.
//
// `interiorPointer` points somewhere inside of the object -- NOT necessarily at the beginning,
// especially in the presence of multiple inheritance. Most implementations should ignore the
// pointer, though a tricky memory allocator could get away with sharing one Disposer among
// multiple objects if it can figure out how to find the beginning of the object given an
// arbitrary interior pointer.
};
template <typename T>
class Own {
// A transferrable title to a T. When an Own<T> goes out of scope, the object's Disposer is
// called to dispose of it. An Own<T> can be efficiently passed by move, without relocating the
// underlying object; this transfers ownership.
//
// This is much like std::unique_ptr, except:
// - You cannot release(). An owned object is not necessarily allocated with new (see next
// point), so it would be hard to use release() correctly.
// - The deleter is made polymorphic by virtual call rather than by template. This is a much
// more powerful default -- it allows any random module to decide to use a custom allocator.
// This could be accomplished with unique_ptr by forcing everyone to use e.g.
// std::unique_ptr<T, capnproto::Disposer&>, but at that point we've lost basically any benefit
// of interoperating with std::unique_ptr anyway.
public:
Own(const Own& other) = delete;
inline Own(Own&& other) noexcept
: disposer(other.disposer), ptr(other.ptr) { other.ptr = nullptr; }
template <typename U>
inline Own(Own<U>&& other) noexcept
: disposer(other.disposer), ptr(other.ptr) { other.ptr = nullptr; }
inline Own(T* ptr, Disposer* disposer) noexcept: disposer(disposer), ptr(ptr) {}
~Own() noexcept { dispose(); }
inline Own& operator=(Own&& other) {
dispose();
disposer = other.disposer;
ptr = other.ptr;
other.ptr = nullptr;
return *this;
}
inline T* operator->() { return ptr; }
inline const T* operator->() const { return ptr; }
inline T& operator*() { return *ptr; }
inline const T& operator*() const { return *ptr; }
inline T* get() { return ptr; }
inline const T* get() const { return ptr; }
inline operator T*() { return ptr; }
inline operator const T*() const { return ptr; }
private:
Disposer* disposer; // Only valid if ptr != nullptr.
T* ptr;
inline void dispose() {
// Make sure that if an exception is thrown, we are left with a null ptr, so we won't possibly
// dispose again.
void* ptrCopy = ptr;
if (ptrCopy != nullptr) {
ptr = nullptr;
disposer->dispose(ptrCopy);
}
}
};
namespace internal {
template <typename T>
class HeapValue final: public Disposer {
public:
template <typename... Params>
inline HeapValue(Params&&... params): value(capnproto::forward<Params>(params)...) {}
virtual void dispose(void*) override { delete this; }
T value;
};
} // namespace internal
template <typename T, typename... Params>
Own<T> heap(Params&&... params) {
// heap<T>(...) allocates a T on the heap, forwarding the parameters to its constructor. The
// exact heap implementation is unspecified -- for now it is operator new, but you should not
// assume anything.
auto result = new internal::HeapValue<T>(capnproto::forward<Params>(params)...);
return Own<T>(&result->value, result);
}
// =======================================================================================
// ArrayPtr
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment