Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,15 @@ all frontends (Swift, C#, WASM, Rust). It must be flawless:
catch all exceptions and return an error code.
- **No C++ types cross the boundary.** Use `const char*`, `int`, `int64_t`,
and raw structs — never `std::string` or `std::vector`.
- **Use the log callback for diagnostics.** When exceptions occur in void
functions or during initialization, log them via `ctx->logCb` if registered,
then return or continue safely. Never use `fprintf(stderr, ...)` — it
doesn't work on embedded targets. Prefer the internal `log_boundary_error()`
helper (noexcept, allocation-free) so a catch handler can never itself throw
across the boundary.

```cpp
// GOOD
// GOOD — function with return value
DASHER_API int dasher_get_offset(dasher_ctx* ctx) {
if (!ctx || !ctx->realized) return -1;
try {
Expand All @@ -108,10 +114,33 @@ DASHER_API int dasher_get_offset(dasher_ctx* ctx) {
}
}

// GOOD — void function: log via callback, then return
DASHER_API void dasher_set_bool_parameter(dasher_ctx* ctx, int key, int value) {
if (!ctx || !ctx->intf) return;
try {
ctx->intf->SetBoolParameter(static_cast<Dasher::Parameter>(key), value != 0);
} catch (const std::exception& e) {
if (ctx->logCb && 3 >= ctx->logCbMinLevel)
ctx->logCb(3, e.what(), ctx->logCbUserData);
} catch (...) {
if (ctx->logCb && 3 >= ctx->logCbMinLevel)
ctx->logCb(3, "unknown exception", ctx->logCbUserData);
}
}

// BAD — exception crosses extern "C"
DASHER_API int dasher_get_offset(dasher_ctx* ctx) {
return ctx->intf->GetModel()->GetOffset(); // Can throw!
}

// BAD — fprintf doesn't work on iOS/WASM/embedded
DASHER_API void dasher_set_bool_parameter(dasher_ctx* ctx, int key, int value) {
try {
ctx->intf->SetBoolParameter(static_cast<Dasher::Parameter>(key), value != 0);
} catch (const std::exception& e) {
fprintf(stderr, "Exception: %s\n", e.what()); // Lost on embedded!
}
}
```

### Rule 5: Zero Compiler Warnings
Expand Down
191 changes: 157 additions & 34 deletions src/CAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,13 @@ struct dasher_ctx {
std::string tlString;
bool realized = false;
bool mouseDown = false;
// Latched true when a C++ exception was caught at the C-API boundary of a
// per-frame entry point (frame/mouse/key). Once set, those entry points
// no-op until the engine is destroyed and recreated — the engine state is
// indeterminate after a mid-frame throw. Frontends query this via
// dasher_has_engine_error(). Per RFC 0009 Amendment 2; not cleared by
// dasher_reset (only by recreating the context).
bool engineError = false;
std::string pendingAlphabet;
std::string dataDir;
std::string userDir;
Expand Down Expand Up @@ -538,6 +545,26 @@ struct dasher_ctx {
};
};

// ── C API Boundary Exception Helpers ────────────────────────────────────────
// Enforces Rule 4: never throw across the C API boundary. All exceptions are
// caught and reported via the log callback at level 3 (ERROR) when registered
// and at/above min_level; otherwise silently discarded.
//
// noexcept and allocation-free (fixed buffer + snprintf). A catch handler that
// itself throws — e.g. a std::string concat hitting bad_alloc — re-violates the
// boundary and, for void setters, cannot recover. RFC 0009 Amendment 2 requires
// this property so engine fault context reliably reaches the frontend ring
// buffer before the function returns.

static void log_boundary_error(dasher_ctx* ctx, const char* context, const char* detail) noexcept {
if (!ctx || !ctx->logCb || 3 /*ERROR*/ < ctx->logCbMinLevel) return;
char buf[256];
const int n = snprintf(buf, sizeof(buf), "%s: %s", context ? context : "", detail ? detail : "");
if (n < 0) return; // encoding error — nothing useful to report
// snprintf always null-terminates (size > 0), so buf is valid even if truncated.
ctx->logCb(3, buf, ctx->logCbUserData);
}

// ── C API implementation ──────────────────────────────────────────────────

// Appearance model helpers (RFC 0007). Defined outside `extern "C"` because they
Expand Down Expand Up @@ -745,7 +772,10 @@ DASHER_API void dasher_set_screen_size(dasher_ctx* ctx, int width, int height) {
if (!ctx->realized) {
try {
ctx->intf->Realize(nowMs());
} catch (...) { // NOLINT(bugprone-empty-catch)
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_set_screen_size: Realize failed", e.what());
} catch (...) {
log_boundary_error(ctx, "dasher_set_screen_size: Realize failed", "unknown exception");
}
ctx->realized = true;

Expand All @@ -764,36 +794,70 @@ DASHER_API void dasher_set_screen_size(dasher_ctx* ctx, int width, int height) {

DASHER_API void dasher_mouse_move(dasher_ctx* ctx, float x, float y) {
if (!ctx || !ctx->input) return;
ctx->input->SetPosition(x, y);
if (ctx->engineError) return;
try {
ctx->input->SetPosition(x, y);
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_mouse_move", e.what());
ctx->engineError = true;
} catch (...) {
log_boundary_error(ctx, "dasher_mouse_move", "unknown exception");
ctx->engineError = true;
}
}

DASHER_API void dasher_mouse_down(dasher_ctx* ctx) {
if (!ctx || !ctx->intf) return;
if (ctx->engineError) return;
if (ctx->mouseDown) return;
ctx->mouseDown = true;

// In circle start mode, clicking should NOT start/stop Dasher —
// only hovering inside the circle should. (Steve Saling feedback)
if (ctx->intf->GetLongParameter(Dasher::LP_START_MODE) == Dasher::Options::StartMode::circle_start) return;

ctx->intf->SetBoolParameter(Dasher::BP_START_MOUSE, true);
ctx->intf->KeyDown(nowMs(), Dasher::Keys::Primary_Input);
try {
// In circle start mode, clicking should NOT start/stop Dasher —
// only hovering inside the circle should. (Steve Saling feedback)
if (ctx->intf->GetLongParameter(Dasher::LP_START_MODE) == Dasher::Options::StartMode::circle_start) return;
ctx->intf->SetBoolParameter(Dasher::BP_START_MOUSE, true);
ctx->intf->KeyDown(nowMs(), Dasher::Keys::Primary_Input);
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_mouse_down", e.what());
ctx->engineError = true;
} catch (...) {
log_boundary_error(ctx, "dasher_mouse_down", "unknown exception");
ctx->engineError = true;
}
}

DASHER_API void dasher_mouse_up(dasher_ctx* ctx) {
if (!ctx || !ctx->intf) return;
if (ctx->engineError) return;
if (!ctx->mouseDown) return;
ctx->mouseDown = false;
ctx->intf->KeyUp(nowMs(), Dasher::Keys::Primary_Input);
try {
ctx->intf->KeyUp(nowMs(), Dasher::Keys::Primary_Input);
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_mouse_up", e.what());
ctx->engineError = true;
} catch (...) {
log_boundary_error(ctx, "dasher_mouse_up", "unknown exception");
ctx->engineError = true;
}
}

DASHER_API void dasher_key_event(dasher_ctx* ctx, int key, int pressed) {
if (!ctx || !ctx->intf) return;
auto vk = static_cast<Dasher::Keys::VirtualKey>(key);
if (pressed) {
ctx->intf->KeyDown(nowMs(), vk);
} else {
ctx->intf->KeyUp(nowMs(), vk);
if (ctx->engineError) return;
try {
auto vk = static_cast<Dasher::Keys::VirtualKey>(key);
if (pressed) {
ctx->intf->KeyDown(nowMs(), vk);
} else {
ctx->intf->KeyUp(nowMs(), vk);
}
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_key_event", e.what());
ctx->engineError = true;
} catch (...) {
log_boundary_error(ctx, "dasher_key_event", "unknown exception");
ctx->engineError = true;
}
}

Expand All @@ -805,15 +869,28 @@ DASHER_API void dasher_frame(dasher_ctx* ctx, int64_t time_ms, int** out_command
if (out_string_count) *out_string_count = 0;

if (!ctx || !ctx->intf || !ctx->screen || !ctx->realized) return;
if (ctx->engineError) return;

ctx->screen->BeginFrame();
ctx->intf->NewFrame(static_cast<unsigned long>((time_ms > 0) ? time_ms : 0), true);
ctx->screen->BuildStringPtrs();
try {
ctx->screen->BeginFrame();
ctx->intf->NewFrame(static_cast<unsigned long>((time_ms > 0) ? time_ms : 0), true);
ctx->screen->BuildStringPtrs();

if (out_commands) *out_commands = const_cast<int*>(reinterpret_cast<const int*>(ctx->screen->GetCommands()));
if (out_command_count) *out_command_count = ctx->screen->GetCommandCount();
if (out_strings) *out_strings = const_cast<char**>(ctx->screen->GetStringPtrs());
if (out_string_count) *out_string_count = ctx->screen->GetStringCount();
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_frame", e.what());
ctx->engineError = true;
} catch (...) {
log_boundary_error(ctx, "dasher_frame", "unknown exception");
ctx->engineError = true;
}
}

if (out_commands) *out_commands = const_cast<int*>(reinterpret_cast<const int*>(ctx->screen->GetCommands()));
if (out_command_count) *out_command_count = ctx->screen->GetCommandCount();
if (out_strings) *out_strings = const_cast<char**>(ctx->screen->GetStringPtrs());
if (out_string_count) *out_string_count = ctx->screen->GetStringCount();
DASHER_API int dasher_has_engine_error(dasher_ctx* ctx) {
return (ctx && ctx->engineError) ? 1 : 0;
}

DASHER_API const char* dasher_get_output_text(dasher_ctx* ctx) {
Expand Down Expand Up @@ -922,56 +999,102 @@ DASHER_API int dasher_get_speed_percent(dasher_ctx* ctx) {

DASHER_API void dasher_set_speed_percent(dasher_ctx* ctx, int percent) {
if (!ctx || !ctx->intf) return;
const double base = 160.0;
const int clamped = (percent < 20) ? 20 : (percent > 400) ? 400 : percent;
long bitrate = static_cast<long>(lround_int(clamped / 100.0 * base));
if (bitrate < 1) bitrate = 1;
ctx->intf->SetLongParameter(Dasher::LP_MAX_BITRATE, bitrate);
try {
const double base = 160.0;
const int clamped = (percent < 20) ? 20 : (percent > 400) ? 400 : percent;
long bitrate = static_cast<long>(lround_int(clamped / 100.0 * base));
if (bitrate < 1) bitrate = 1;
ctx->intf->SetLongParameter(Dasher::LP_MAX_BITRATE, bitrate);
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_set_speed_percent", e.what());
} catch (...) {
log_boundary_error(ctx, "dasher_set_speed_percent", "unknown exception");
}
}

DASHER_API int dasher_get_bool_parameter(dasher_ctx* ctx, int key) {
if (!ctx || !ctx->intf) return 0;
try {
return ctx->intf->GetBoolParameter(static_cast<Dasher::Parameter>(key)) ? 1 : 0;
} catch (const std::bad_variant_access&) {
fprintf(stderr, "DASHER: bad_variant_access in get_bool_parameter key=%d\n", key);
} catch (const std::exception& e) {
char context[96];
snprintf(context, sizeof(context), "dasher_get_bool_parameter key=%d", key);
log_boundary_error(ctx, context, e.what());
return 0;
} catch (...) {
char context[96];
snprintf(context, sizeof(context), "dasher_get_bool_parameter key=%d", key);
log_boundary_error(ctx, context, "unknown exception");
return 0;
}
}

DASHER_API void dasher_set_bool_parameter(dasher_ctx* ctx, int key, int value) {
if (!ctx || !ctx->intf) return;
ctx->intf->SetBoolParameter(static_cast<Dasher::Parameter>(key), value != 0);
try {
ctx->intf->SetBoolParameter(static_cast<Dasher::Parameter>(key), value != 0);
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_set_bool_parameter", e.what());
} catch (...) {
log_boundary_error(ctx, "dasher_set_bool_parameter", "unknown exception");
}
}

DASHER_API long dasher_get_long_parameter(dasher_ctx* ctx, int key) {
if (!ctx || !ctx->intf) return 0;
try {
return ctx->intf->GetLongParameter(static_cast<Dasher::Parameter>(key));
} catch (const std::bad_variant_access&) {
fprintf(stderr, "DASHER: bad_variant_access in get_long_parameter key=%d\n", key);
} catch (const std::exception& e) {
char context[96];
snprintf(context, sizeof(context), "dasher_get_long_parameter key=%d", key);
log_boundary_error(ctx, context, e.what());
return 0;
} catch (...) {
char context[96];
snprintf(context, sizeof(context), "dasher_get_long_parameter key=%d", key);
log_boundary_error(ctx, context, "unknown exception");
return 0;
}
}

DASHER_API void dasher_set_long_parameter(dasher_ctx* ctx, int key, long value) {
if (!ctx || !ctx->intf) return;
ctx->intf->SetLongParameter(static_cast<Dasher::Parameter>(key), value);
try {
ctx->intf->SetLongParameter(static_cast<Dasher::Parameter>(key), value);
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_set_long_parameter", e.what());
} catch (...) {
log_boundary_error(ctx, "dasher_set_long_parameter", "unknown exception");
}
}

DASHER_API const char* dasher_get_string_parameter(dasher_ctx* ctx, int key) {
if (!ctx || !ctx->intf) return "";
try {
ctx->tlString = ctx->intf->GetStringParameter(static_cast<Dasher::Parameter>(key));
} catch (const std::exception& e) {
char context[96];
snprintf(context, sizeof(context), "dasher_get_string_parameter key=%d", key);
log_boundary_error(ctx, context, e.what());
ctx->tlString = "";
} catch (...) {
char context[96];
snprintf(context, sizeof(context), "dasher_get_string_parameter key=%d", key);
log_boundary_error(ctx, context, "unknown exception");
ctx->tlString = "";
}
return ctx->tlString.c_str();
}

DASHER_API void dasher_set_string_parameter(dasher_ctx* ctx, int key, const char* value) {
if (!ctx || !ctx->intf || !value) return;
ctx->intf->SetStringParameter(static_cast<Dasher::Parameter>(key), value);
try {
ctx->intf->SetStringParameter(static_cast<Dasher::Parameter>(key), value);
} catch (const std::exception& e) {
log_boundary_error(ctx, "dasher_set_string_parameter", e.what());
} catch (...) {
log_boundary_error(ctx, "dasher_set_string_parameter", "unknown exception");
}
}

// Color utility functions
Expand Down
8 changes: 8 additions & 0 deletions src/dasher.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ DASHER_API void dasher_key_event(dasher_ctx* ctx, int key, int pressed);
DASHER_API void dasher_frame(dasher_ctx* ctx, int64_t time_ms, int** out_commands, int* out_command_count,
char*** out_strings, int* out_string_count);

// Engine fault flag. Returns 1 if a C++ exception was caught at the boundary
// of dasher_frame / dasher_mouse_* / dasher_key_event, leaving the engine in
// an indeterminate state; 0 otherwise. When true, those per-frame entry points
// no-op and the frontend must stop calling them, surface an error, then
// dasher_destroy() + dasher_create() a fresh context. Not cleared by
// dasher_reset(). See RFC 0009 Amendment 2.
DASHER_API int dasher_has_engine_error(dasher_ctx* ctx);

// Get/set output text (characters entered so far).
// Returned pointer is valid until the next API call on this context.
DASHER_API const char* dasher_get_output_text(dasher_ctx* ctx);
Expand Down
Loading