Hi Daniel,
I wouldn’t recommend either of those two approaches.
async/await won’t solve this because suspension occurs only at explicit coroutine suspension points (await); it doesn’t provide a mechanism for externally suspending arbitrary synchronous Python execution at an arbitrary bytecode boundary.
Swapping native stacks out from under an active interpreter with Boost fibers is also dangerous. CPython’s evaluation machinery maintains C-level execution state associated with the active native call stack-such as the active cframe, recursion state, and C-level exception-unwinding context. Swapping to another native stack and later resuming the interpreter can therefore leave the evaluator in an inconsistent state, particularly across different CPython versions.
If Python has to run on the main thread because scripts directly interact with non-thread-safe Qt/C++ state, a practical approach would be to block inside your trace/monitoring callback with a nested QEventLoop.
The conceptual flow is:
Python execution
↓
CPython frame evaluator
↓
Trace / monitoring callback
↓
Breakpoint detected
↓
QEventLoop::exec()
↓
User clicks Continue
↓
QEventLoop exits
↓
Trace callback returns
↓
CPython continues execution
The important point is that this doesn’t suspend the interpreter by switching its native stack. The Python thread remains inside the trace callback on the same native stack while Qt processes events.
In C++, roughly:
void onBreakpointHit() {
emit debuggerPaused();
QEventLoop pauseLoop;
auto c1 = connect(
ui->continueBtn,
&QPushButton::clicked,
&pauseLoop,
&QEventLoop::quit
);
auto c2 = connect(ui->stepBtn, &QPushButton::clicked, [&]() {
enableSingleStep();
pauseLoop.quit();
});
pauseLoop.exec(QEventLoop::AllEvents);
disconnect(c1);
disconnect(c2);
}
The subtle danger here is re-entrancy and object lifetime. A nested Qt event loop changes the normal control-flow assumptions of the application. You need to ensure that the nested event loop cannot synchronously re-enter the same Python interpreter, start another script, or invalidate/delete C++ objects that the paused Python call stack is still referencing.
If that re-entrancy/lifetime surface is too brittle for your architecture, another option is to move the Python interpreter to a dedicated worker thread, use a QWaitCondition for breakpoint waits, and marshal script-to-widget calls back to the GUI thread with QMetaObject::invokeMethod(..., Qt::BlockingQueuedConnection). That cleanly separates the debugger wait from the UI loop, although it introduces the usual deadlock considerations around blocking queued calls.
Hope that helps!