Inspiration

Python and Rust are increasingly paired in AI and systems applications: Python for fast iteration and ecosystem flexibility, Rust for safe, high-performance hot paths. But debugging that boundary is still awkward. Developers often have to mentally stitch together separate Python and native debugger views, especially once threads, child processes, or embedded Python enter the picture. PyRust started from that gap: make mixed CPython 3.14 and Rust/PyO3 execution legible inside VS Code.

What it does

PyRust is a VS Code debugging prototype that presents Python and Rust execution in one debugging workflow. It currently: launches targets through CodeLLDB; merges native Rust frames with recovered CPython 3.14 frames; shows mixed Python/Rust call stacks through DAP; supports Python -> Rust and Rust -> Python -> Rust callback fixtures; handles Python threads, Rust threads, child processes, and async non-nesting cases; adds a PyRust Process Tree view so real processes and native threads are shown honestly; supports opt-in debugpy-backed Python stops for normal Python breakpoints and evaluation; keeps Rust stops owned by CodeLLDB, with read-only Python snapshots at those stops. The prototype is intentionally scoped to Linux x86_64, CPython 3.14, PyO3 fixtures, CodeLLDB, and a local VS Code extension.

How we built it

We used Codex as an implementation partner across research, design, coding, testing, and documentation. The project evolved in slices: Researched CPython 3.14 remote unwinding, LLDB/CodeLLDB, DAP, VS Code extension APIs, and prior art. Built the first Python -> Rust proof where CodeLLDB stops in Rust and PyRust augments the stack with Python frames. Added the reverse Rust -> embedded Python -> Rust callback proof. Added containerized VS Code validation and a local VS Code extension. Expanded from single-thread fixtures into Python threads, Rust worker threads, child processes, and async acceptance cases. Built the PyRust Process Tree to display real process/thread ownership instead of fake nested stacks. Added debugpy coordination so Python-owned stops can use real Python debugging behavior. Added a dual-debug-engine coordinator that routes frame operations to CodeLLDB, debugpy, or the snapshot reader depending on ownership. The repo keeps this work auditable through ADRs, acceptance scripts, fixture-based tests, and ./scripts/verify-submission.sh.

Challenges we ran into

The hardest part was debugger ownership. CodeLLDB and debugpy can each debug their own language well, but they cannot safely own the same stopped process at the same time. That forced PyRust to become a coordinator, not just a visual stack merger. We also had to avoid misleading concurrency displays. Python threads, Rust threads, child processes, asyncio tasks, and Rust futures are different execution models, so the Process Tree deliberately keeps real OS/process relationships honest instead of inventing a fake call hierarchy. Another challenge was keeping the prototype demonstrable and testable. We built fixture-bound acceptance tests for Python -> Rust, Rust -> Python -> Rust, threads, multiprocess behavior, async behavior, debugpy handoff, and VS Code packaging.

Accomplishments that we're proud of

We built a working local VS Code debugger prototype that shows mixed Python/Rust stacks instead of leaving developers to correlate separate tools manually. We are especially proud of: Python -> Rust mixed-stack debugging; Rust -> Python -> Rust callback support; Python and Rust native-thread fixtures; multiprocess and process/thread lifecycle handling; the PyRust Process Tree; opt-in debugpy-backed Python breakpoints; the dual-engine coordinator; reproducible Dev Container validation; a submission gate that verifies the core claims automatically.

What we learned

We learned that “show both languages in one stack” is only the visible part of the problem. The deeper issue is debugger authority: every frame needs a clear owner for scopes, variables, evaluation, stepping, and continuation. We also learned that correctness matters more than a pretty merged stack. A debugger must not imply that a Rust OS thread is a child Python call frame, or that an async task is a native thread. The UI has to preserve the real execution model. Finally, we learned that acceptance fixtures are essential for this kind of tool. Without black-box DAP tests, it would be too easy to demo a convincing stack view while hiding broken routing, stale frame IDs, or incorrect thread ownership.

What's next for python-rust-debugger integration

The next milestone is making every displayed foreign-language frame fully live. Today, owner-language frames are live: debugpy owns Python stops, CodeLLDB owns Rust stops. Foreign frames are still incomplete in some cases, such as Python snapshot frames at Rust-owned stops. Next steps: make the VSIX installable outside this repo without absolute path assumptions; improve source mapping and packaging for real PyO3 projects; expand platform support beyond the pinned Linux x86_64 environment; deepen live frame routing so selected Python and Rust frames always use their real debugger; continue improving process/thread/async presentation inside VS Code; explore a production-ready architecture for mixed Python/Rust debugging beyond fixture-bound prototypes.

Built With

  • codelldb
  • codex
  • dap
  • debugpy
  • docker
  • pyo3
  • python-3.14
  • rust
  • vs-code
Share this project:

Updates