Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
callback_boundary.hpp
Go to the documentation of this file.
1#pragma once
2
3// aria::CallbackBoundary — the single, framework-wide reporting channel for
4// exceptions that escape a synchronous user callback at a boundary the
5// framework MUST keep moving past.
6//
7// Why this exists
8// ---------------
9// Several locations inside the framework run user code in contexts where an
10// exception cannot be propagated to a meaningful caller:
11//
12// * worker threads inside a thread-pool executor;
13// * the main-thread executor / dispatcher's drain / pump loops;
14// * the ABI slot trampoline that backs `abi::SignalErased::emit`;
15// * the virtual-time executor's pump (test-side scheduler);
16// * detached coroutines (already routed through `aria::async`).
17//
18// Historically each site ended in a bare `catch (...) { /* swallow */ }`,
19// which made production failures invisible. This header defines a single
20// reporting primitive that those sites all funnel into:
21//
22// aria::report_callback_failure("executor.thread_pool.worker",
23// std::current_exception());
24//
25// The host application installs ONE sink with `set_callback_failure_sink`
26// (typically routed into the logger). Until then the default sink prints a
27// single line to `stderr` so production failures are never invisible.
28//
29// Lifetime / threading
30// --------------------
31// * The sink is a function-pointer-based registration stored in an atomic
32// variable. Installation, replacement, and read use atomic operations.
33// * The reporter is `noexcept`: a sink that throws is itself caught and
34// degraded to a `stderr` fallback so the framework's noexcept boundaries
35// stay honest.
36// * `std::current_exception()` is captured at the boundary, so each report
37// carries a structured exception_ptr the sink can rethrow if it wants
38// typed inspection — ABI / type-erased payloads are not required.
39//
40// Layering
41// --------
42// This lives in `core` because both `async` (`async_error_sink`) and the
43// concrete dispatcher / executor implementations need it. `runtime` injects
44// a default sink that routes into `aria::Logger`. Adapters and binding
45// layers may, but need not, install their own sinks.
46
47#include "aria/abi/export.hpp"
48#include "aria/function_ref.hpp"
49
50#include <atomic>
51#include <cstdio>
52#include <exception>
53#include <string>
54#include <string_view>
55#include <utility>
56
57namespace aria {
58
67 std::string_view category;
68 std::exception_ptr exception;
69 std::string_view message;
70};
71
78using CallbackFailureSink = void (*)(const CallbackFailure&);
79
80namespace detail::callback_boundary {
81
82// Shared builds store this slot in libaria_abi. Static builds link one
83// copy into the host executable; independently linked plugins should use the
84// shared build if they need process-wide diagnostic registration.
85ARIA_ABI_API std::atomic<CallbackFailureSink>& sink_storage() noexcept;
86
87inline std::string render_message_(const CallbackFailure& f) {
88 if (!f.message.empty()) return std::string{f.message};
89 if (f.exception) {
90 try {
91 std::rethrow_exception(f.exception);
92 } catch (const std::exception& e) {
93 return std::string{e.what()};
94 } catch (...) {
95 return "non-std::exception";
96 }
97 }
98 return "(no payload)";
99}
100
101inline void default_sink_(const CallbackFailure& f) noexcept {
102 // Fallback: a single, parseable line on stderr. Host apps that want
103 // structured logging install their own sink during runtime startup.
104 try {
105 const std::string msg = render_message_(f);
106 std::fprintf(stderr, "[aria.callback_failure] %.*s: %s\n",
107 static_cast<int>(f.category.size()), f.category.data(),
108 msg.c_str());
109 std::fflush(stderr);
110 } catch (...) {
111 // We are already a noexcept boundary of last resort. Even formatting
112 // can fail under OOM; in that case we simply give up — the framework
113 // contract is to never raise from a noexcept boundary.
114 }
115}
116
117} // namespace detail::callback_boundary
118
122 return detail::callback_boundary::sink_storage().exchange(sink, std::memory_order_acq_rel);
123}
124
127[[nodiscard]] inline CallbackFailureSink current_callback_failure_sink() noexcept {
128 return detail::callback_boundary::sink_storage().load(std::memory_order_acquire);
129}
130
137ARIA_ABI_API void report_callback_failure(std::string_view category,
138 std::exception_ptr exception,
139 std::string_view message = {}) noexcept;
140
144inline void report_callback_failure(std::string_view category,
145 std::string_view message) noexcept {
146 report_callback_failure(category, std::exception_ptr{}, message);
147}
148
149} // namespace aria
#define ARIA_ABI_API
Definition export.hpp:21
Definition signal.hpp:12
CallbackFailureSink set_callback_failure_sink(CallbackFailureSink sink) noexcept
Install a global callback-failure sink.
Definition callback_boundary.hpp:121
void(*)(const CallbackFailure &) CallbackFailureSink
Sink type.
Definition callback_boundary.hpp:78
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.
CallbackFailureSink current_callback_failure_sink() noexcept
Read the currently installed sink.
Definition callback_boundary.hpp:127
Payload passed to a callback-failure sink.
Definition callback_boundary.hpp:66
std::exception_ptr exception
Definition callback_boundary.hpp:68
std::string_view message
Definition callback_boundary.hpp:69
std::string_view category
Definition callback_boundary.hpp:67