Skip to content

feat(capi): wrap per-frame hot path + engine error flag (RFC 0009 A2)#38

Merged
willwade merged 2 commits into
mainfrom
fix-capi-hotpath-engine-error
Jun 28, 2026
Merged

feat(capi): wrap per-frame hot path + engine error flag (RFC 0009 A2)#38
willwade merged 2 commits into
mainfrom
fix-capi-hotpath-engine-error

Conversation

@willwade

Copy link
Copy Markdown

What

Wraps the per-frame C-API hot path in try/catch and exposes an engine-fault flag, so a C++ exception inside dasher_frame / dasher_mouse_* / dasher_key_event is routed through the log callback (level 3) before the function returns — giving the frontend's engine-log ring buffer actual fault context instead of an empty tail. Implements the engine-side half of RFC 0009 Amendment 2 (just merged in dasher-project/governance).

Why

#35 standardised the parameter accessors; the remaining — and most valuable — gap is the per-frame hot path: the least-guarded entry points and the most likely to hit engine bugs. Today an exception here propagates across extern "C" and the crash report shows where the frontend was, not what the engine was doing.

Changes

  • Wrap dasher_frame, dasher_mouse_move, dasher_mouse_down, dasher_mouse_up, dasher_key_event in try/catch; caught exceptions go through log_boundary_error() (from fix: harden C API boundary exception handling (issue #34) #35) at level 3.
  • Add bool dasher_ctx::engineError, latched on catch. Once set, those five entry points no-op — the engine is indeterminate after a mid-frame throw, and continuing is what turns a soft exception into the hard SEGV this cannot catch.
  • Expose it via int dasher_has_engine_error(dasher_ctx*) (dasher_ctx is opaque). Frontend contract on true: stop calling frame, surface an error, then dasher_destroy() + dasher_create(). Not cleared by dasher_reset().

What this does not catch

SEGV/SIGBUS, stack overflow, destructor throws during unwinding, and DASHER_ASSERT (a no-op under NDEBUG, not an abort). Those stay with the per-platform signal handlers per the base RFC; the log tail still helps there because the last successful frame's logs precede the signal.

Depends on

#35 (log_boundary_error helper). This branch includes #35's commit so it builds standalone; once #35 merges (likely squash), I'll rebase this onto main and the diff shrinks to just the hot-path changes.

Verification

  • clang-format --dry-run --Werror clean on src/CAPI.cpp and src/dasher.h.
  • clang 19 + clang-tidy build of libdasher.so passes with no tidy findings.
  • ASan/UBSan left to CI.

willwade added 2 commits June 28, 2026 23:32
Collapses the prior fixup chain (helpers + clang-format + placement) into
one commit and drops the unrelated CODEOWNERS commit (landed separately in
#36). Rebased onto current main.

Standardises every C-API entry point that can throw on the log callback:
- Adds a single noexcept, allocation-free helper log_boundary_error() that
  formats "<context>: <detail>" into a fixed stack buffer via snprintf.
  Catch handlers must not throw: a std::string concat hitting bad_alloc
  would re-violate the boundary and, for void setters, cannot recover
  (RFC 0009 Amendment 2 requires this property).
- Guards the four setters (dasher_set_bool/long/string_parameter,
  dasher_set_speed_percent) which call broadcasting observer code.
- Upgrades the three getters to catch std::exception + ... and route
  through the callback, replacing fprintf(stderr,...) (lost on iOS
  keyboard extensions / WASM / embedded) and the bad_variant_access-only
  catch that let other exception types escape.
- Logs dasher_set_screen_size Realize() failures instead of silently
  swallowing them.
- Updates CONTRIBUTING Rule 4 with the void-function policy and a pointer
  to the helper.

Verified: clang-format clean on Src/CAPI.cpp; clang + clang-tidy build of
libdasher.so passes with no tidy findings.

Signed-off-by: will wade <willwade@gmail.com>
… A2)

The parameter accessors were standardised in #35; the remaining and most
valuable gap was the per-frame hot path — the least guarded entry points
and the most likely to hit engine bugs. C++ exceptions escaping
dasher_frame / dasher_mouse_* / dasher_key_event across extern "C" left
the frontend engine-log ring buffer with no fault context, so a crash
report showed where the frontend was, not what the engine was doing
(RFC 0009 Amendment 2).

- Wrap dasher_frame, dasher_mouse_move/down/up, dasher_key_event in
  try/catch; route caught exceptions through log_boundary_error() at
  level 3 (reused from #35) so the fault reaches the frontend ring
  buffer before the function returns.
- Add bool dasher_ctx::engineError, latched on catch. Once set, those
  five entry points no-op — the engine is indeterminate after a
  mid-frame throw and continuing would risk a hard SEGV this cannot
  catch.
- Expose via int dasher_has_engine_error(dasher_ctx*) (the context is
  opaque). Frontend contract: on true, stop calling frame, surface an
  error, then dasher_destroy() + dasher_create(). Not cleared by
  dasher_reset().

What this does NOT catch: SEGV/SIGBUS, stack overflow, destructor throws
during unwinding, and DASHER_ASSERT (a no-op under NDEBUG, not an
abort). Those remain the per-platform signal handlers job per the base
RFC; the log tail still helps because the last successful frame logs
precede the signal.

Depends on #35 (log_boundary_error helper). Verified: clang-format clean
on src/CAPI.cpp and src/dasher.h; clang + clang-tidy build of libdasher.so
passes with no tidy findings.

Signed-off-by: will wade <willwade@gmail.com>
@willwade willwade merged commit ee732b8 into main Jun 28, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant