Skip to content

Mega-batch of 0.8 breaking changes - #198

Merged
daniel5151 merged 13 commits into
dev/0.8from
feat/type-safe-stop-reasons
May 9, 2026
Merged

Mega-batch of 0.8 breaking changes#198
daniel5151 merged 13 commits into
dev/0.8from
feat/type-safe-stop-reasons

Conversation

@daniel5151

Copy link
Copy Markdown
Owner

This is def the sort of thing that should've been multiple PRs, but I got a bit carried away... While I initially intended to split this up, the Tid and StopReason changes proved to be deeply intertwined at the state-machine layer, making a single cohesive PR the most practical approach.

TL;DR of changes:

  • Added a Tid associated type to Target.
  • Replaced StopReason enums with a fluent builder API.
  • Reworked Ctrl-C interrupt handling to be asynchronous.
  • Improved internal error types and state management.

Closes #195
Unblocks #51
Closes #185 (which I ended up implementing in this PR)


Updated Target to include a thread-id associated type

A new type Tid: IsValidTid associated type has been added to the Target trait, and is now used throughout various IDETs, and internal/external gdbstub APIs.

Several things nudged me towards making this change:

  • Future-proofing for multi-process: gdbstub has accumulated a couple non-Base IDETs that plumb thread-id values to the user's Target implementation (namely: ThreadExtraInfo and Wasm). These IDETs currently hard-code crate::common::Tid in their APIs. The jump to multi-process support will require updating their function signatures, and single-threaded targets that otherwise don't need to care about Tid values currently end up having gdbstub's SINGLE_THREAD_TID constant leaked to them.
  • Compile-time safety: While it is possible for particularly dynamic Target implementations to decide to swap between single-thread vs. multi-thread ops at runtime, in practice, a target picks what kind of target it is at compile time and sticks to it. Returning a non-() TID on a single-threaded target could confuse gdbstub internally. This change makes Tid handling significantly more consistent and type-safe across the board.
  • Internal simplification: gdbstub's implementation has been dramatically simplified and tangibly improved by having T::Tid everywhere. I've been able to refactor a surprisingly large amount of Tid conversion code. The new from_fully_qualified_tid adds strong type-level guard rails for validating that the GDB client is sending over thread-id values that the Target is able to operate on.
  • Reducing "generics soup": Plumbing Tid via Target significantly reduces the amount of generics the run_blocking and GdbStubStateMachine family of APIs have to deal with. We no longer need an extra top-level Tid: IsValidTid bound on a bunch of those APIs.

Removed StopReason enums, switched to function-based stop reason reporting

The enum-based API was workable so long as stop reasons remained fixed-length... but as we've implemented more of the GDB RSP, the stop reason API hasn't kept up with the increasingly dynamic data that folks want to report.

Two notable examples: reporting inline register values, and stop reasons with embedded strings. While we were able to squeeze in support for reporting inline register values via #189, things were getting a bit hacky, and would only get hackier as the GDB RSP added more metadata to stop reply packets (e.g: like how they added core to the T reply packet).

This new approach rethinks stop reason reporting from the ground up, moving away from the enum-based approach that was present since the earliest days of gdbstub, and swaps over to a more flexible, extensible, and safer fluent builder approach to reporting stop reasons.

A huge DX win here is compile-time safety for IDETs. The new builder methods (like .swbreak()) have trait bounds that will cause a compile-time failure if you try to use them without actually implementing the corresponding IDET on your target!

So, for example: in the context of the run_blocking infrastructure:

// Before
emu::RunEvent::Event(event) => {
    use gdbstub::target::ext::breakpoints::WatchKind;

    let stop_reason = match event {
        emu::Event::DoneStep => SingleThreadStopReason::DoneStep,
        emu::Event::Halted => SingleThreadStopReason::Terminated(Signal::SIGSTOP),
        // must carefully remember to only report SwBreak if corresponding
        // IDET is implemented
        emu::Event::Break => SingleThreadStopReason::SwBreak(()),
        ...
    };

    // no way to report inline registers :( would have to drop down
    // to the full-blown state machine API to do that
    Ok(run_blocking::Event::TargetStopped(stop_reason))
}

// After
emu::RunEvent::Event(event) => {
    Ok(simple_stub.report_stop(target, |report_stop| {
        use gdbstub::target::ext::breakpoints::WatchKind;

        match event {
            emu::Event::DoneStep => report_stop.done_step(),
            emu::Event::Halted => report_stop.terminated(Signal::SIGSTOP),
            // new APIs allow ergonomically chaining extra metadata
            // (e.g: via `add_reg`, or `add_core`) without having to move on
            // from the blocking event loop!
            // 
            // also, `swbreak` method has trait bounds that would
            // cause a compile-time fail if it was used without implementing
            // the corresponding IDET!
            emu::Event::Break => report_stop.swbreak(())?.done(),
            ...
        }
    }))
}

A major benefit under-the-hood is that there's no longer any point in time where a stop reason exists as a data structure in memory / needs to get "passed around" the codebase. This is a nice little binary size win! Albeit with one consequence...

Rework Ctrl-C interrupt stop-reason reporting

The ctrl-c interrupt APIs now require the implementor to stash the fact the interrupt occurred somewhere on/around their Target implementation, and report the effects of the interrupt as a downstream stop reason the next time they enter the Running state.

This is a direct consequence of the shift to function-based stop reason reporting, as there is no longer an in-memory representation of a stop reason that can be stashed via the deferred_stop_reason infrastructure in the state machine.

While this means the "fast path" is gone, this is actually a solid architectural win. Forcing targets to stash the interrupt and handle it asynchronously in their own execution loops enforces a more robust architecture and prevents weird state desyncs between GDB and the target's actual execution state. In practice, the vast majority of target implementations already maintain some form of internal "pending interrupt" flag or event queue, so setting a simple boolean flag is a trivial adjustment.

Internal Cleanup

  • Improved Error variants + display implementations: The InternalError (and by extension GdbStubError) enum has been revamped. Catch-all errors like TargetMismatch have been replaced with highly specific variants like UnexpectedIntegerSize, UnexpectedReg, and UnexpectedThreadId. The Display implementation clearly delineates between "Client" errors and "Target/Implementation" errors.
  • Decoupled ResponseWriterState: The ResponseWriter has been decoupled slightly by extracting its state into a ResponseWriterState struct. This allows the state to be passed around without needing a continuous mutable borrow over the underlying connection, which heavily facilitates the new StopReasonReporter builder pattern.
  • In ResponseWriterState, collapsed two bools into a bitflag (for a tiny bit of memory gainz)

Here is a draft for that section that matches the tone of the rest of the PR description. You could slot this in right before the "Internal Cleanup" section:

Minor API terminology tweaks (tid -> thread_id)

While going through and updating the various Target extension traits to use the new Self::Tid associated type, I took the opportunity to clean up some of the parameter naming in the API docs.

Across the board, trait methods that previously accepted a tid: Tid parameter (like in ThreadExtraInfo, Wasm, SingleRegisterAccess, etc.) have been updated to use thread_id: Self::Tid.

This is a purely cosmetic change for implementors, but as we enter the world of multi-process support, being more intentional about tid, pid, and thread-id will be important to maintain grokkability of gdbstub's large API surface.


Quick Migration Checklist

What's surprising is that, even through there's a lot of churn in gdbstub itself, the actual end-user APIs that folks interact with aren't changing all that much from 0.7. I'm running out of steam this weekend to write a comprehensive transition guide, but it shouldn't be too painful to move over!

At a high level, this is all that'll really be needed:

  1. Adding type Tid = (); (or type Tid = gdbstub::common::Tid;) to existing Target impls.
  2. Stop reason reporting will switch over to using functions instead of enums (a highly localized change).
  3. Switch ctrl-c handling to set an internal flag + defer reporting the stop to the Running state

@daniel5151

Copy link
Copy Markdown
Owner Author

@jonathanzetier I'm going to merge this big bundle of changes into the dev/0.8 branch - apologies for the churn in whatever unpublished branches you might have 😅

That said - I strongly believe that these changes (especially introduction of the Target::Tid associated type) will dramatically simplify enabling multi-process support, and makes it so that the compiler will play a helpful role in pointing out what parts of the codebase need to be "enlightened" to work in multi-process scenarios.

Please feel free to share any thoughts you have on these changes! I'm fairly certain that these changes are the right call, but if you spot anything that you think I might've blundered on, please let me know!

@daniel5151
daniel5151 merged commit 9b4ac44 into dev/0.8 May 9, 2026
4 checks passed
@daniel5151
daniel5151 deleted the feat/type-safe-stop-reasons branch May 9, 2026 23:35
@daniel5151 daniel5151 mentioned this pull request May 17, 2026
4 tasks
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.

2 participants