1、概述
源码放在文章末尾
根据上一篇文章回顾下利用Qt+C++实现了一个简易的python编译器,类似pycharm或vsCode这样的编译器,该python编译器目前实现了如下功能:
(1)支持编写python程序
(2)编写代码时有代码补全提示
(3)程序运行到每行时该行高亮显示
(4)可以加载python脚本执行
(5)可以在程序运行的过程中随时中断
(6)有输出窗口实时显示程序执行的状态或执行程序的打印显示等
详细介绍可以看我上一篇文章。
在这篇文章中对上一版代码进行了一些优化和修改,具体修改功能如下:
(1)美化了界面操作,更像一个简易的python编译器
(2)新增了代码断点调试功能
(3)新增了菜单栏,功能分别为一键加载python脚本、运行python脚本、停止运行、单步调试、连续调试、清空所有断点,如下所示
下图为Python编译器的demo演示流程:
1、一键加载python脚本
关键代码如下所示(注意:加载python脚本时不能有中文路径,不然无法识别):
cpp
void pythonRecipeWidget::on_loadScriptPushButton_clicked()
{
QString initialDir;
QString filePath = QFileDialog::getOpenFileName(this, tr("Select Script"), initialDir);
if (filePath.isEmpty())
return;
ui.scriptPlainTextEdit->clear();
std::ifstream file(filePath.toStdString());
std::string script((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
ui.scriptPlainTextEdit->setPlainText(script.c_str());
file.close();
}
2、运行python脚本
给的例子中的python脚本会运行完for循环然后继续往下运行时会正常报错,因为没有导入该第三方工作库
3、停止运行
4、断点单步调试
5、连续调试
6、清除所有断点
主要代码分析:
运行 Python 脚本:使用 PyRun_SimpleString。
设置断点:通过 MyTrace 回调拦截 PyTrace_LINE。
断点调试(断点、暂停、继续、单步执行)。
中断机制(用户手动终止脚本)。
线程安全处理(QtConcurrent::run + PyGILState_Ensure)。
UI 控制启用/禁用(通过 QMetaObject::invokeMethod)。
cpp
QtConcurrent::run([=]()
{
qDebug() << __FUNCTION__ << QThread::currentThreadId() << QThread::currentThread();
QMetaObject::invokeMethod(m_run, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, false));
QMetaObject::invokeMethod(m_stop, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));
QMetaObject::invokeMethod(m_stepOver, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));
QMetaObject::invokeMethod(m_continueExecute, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));
PyGILState_STATE gstate = PyGILState_Ensure();
scriptThreadState = PyThreadState_Get();
PyEval_SetTrace(MyTrace, NULL);
PyRun_SimpleString(GBK_To_UTF8(g_script).c_str());
scriptThreadState = nullptr;
PyGILState_Release(gstate);
QMetaObject::invokeMethod(m_run, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));
QMetaObject::invokeMethod(m_stop, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, false));
QMetaObject::invokeMethod(m_stepOver, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, false));
QMetaObject::invokeMethod(m_continueExecute, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, false));
is_paused = false;
step_once = false;
});
在后台线程中运行 Python 脚本,确保线程间 GIL 安全。
scriptThreadState 保存当前线程状态,供中断使用。
设置了 MyTrace 作为追踪函数,实现在某些行/事件进行控制,MyTrace实现对代码的断点、停止、继续运行等功能
cpp
int MyTrace(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg)
{
if (g_isExection)
return 0;
//如果把中断程序放在这里会导致比如在第二行中断时会在第二行执行完才中断
if ((lineCount == PyFrame_GetLineNumber(frame)) && what == PyTrace_EXCEPTION)
{
g_isExection = true;
int line = PyFrame_GetLineNumber(frame);
return 0;
}
if (what == PyTrace_LINE)
{
char const* fileName = _PyUnicode_AsString(frame->f_code->co_filename);
char const* name = _PyUnicode_AsString(frame->f_code->co_name);
if (strcmp(fileName, "<string>") == 0 && strcmp(name, "__new__") != 0)
{
int line = PyFrame_GetLineNumber(frame);
lineCount = line;
ShowLine(line);
qDebug() << "filename" << fileName << "name" << name << "line" << line << "frame" << frame << "f_back" << frame->f_back;
breakPointAfter = line;
//如果断点不在代码行上就移到下面最近的代码行
for (auto breakPointLine : breakPoints)
{
if (breakPointBefore < breakPointLine && breakPointLine < breakPointAfter)
{
breakPointCallBack_(breakPointBefore, breakPointAfter);
is_paused = true;
step_once = false;
breakPointBefore = line;
break;
}
}
//当前代码行等于断点行就暂停程序
if (breakPointAfter != breakPointBefore)
{
for (auto breakPointLine : breakPoints)
{
if (line == breakPointLine)
{
is_paused = true;
step_once = false;
break;
}
}
}
breakPointBefore = line;
//判断当前是否debugging
if (is_paused && !step_once)
bpDebuggingLineCallBack_(line, true);
else
bpDebuggingLineCallBack_(line, false);
// 暂停执行,等待继续调试信号
while (is_paused && !step_once) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// 单步执行一次后继续暂停
if (step_once) {
is_paused = true;
step_once = false;
}
//如果把中断程序放在这里会导致比如在第二行中断时会在第二行执行前中断
if (g_isAbort)
{
if (!m_isInterrupt)
{
qDebug() << "User abort.";
//PyErr_SetString(PyExc_KeyboardInterrupt, "User abort.");
if (scriptThreadState)
{
PyGILState_STATE gstate = PyGILState_Ensure();
PyThreadState_SetAsyncExc((unsigned long)scriptThreadState->thread_id, PyExc_KeyboardInterrupt);
PyGILState_Release(gstate);
}
m_isInterrupt = true;
}
bpDebuggingLineCallBack_(line, false);
return 0;
}
}
}
return 0;
}
追踪函数 MyTrace
追踪函数通过判断 what == PyTrace_LINE 来对 Python 脚本执行的每一行做拦截,并根据断点及状态决定:
cpp
for (auto breakPointLine : breakPoints)
{
if (line == breakPointLine)
{
is_paused = true;
step_once = false;
break;
}
}
命中断点:暂停程序。
ShowLine(line) 和 bpDebuggingLineCallBack_() 用于 UI 更新。
单步执行逻辑
cpp
while (is_paused && !step_once) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
is_paused 和 step_once 控制主调试循环。
外部通过点击 "继续" 或 "单步" 按钮控制 is_paused 和 step_once 变量。
中断处理(abort)
cpp
if (g_isAbort && !m_isInterrupt)
{
PyThreadState_SetAsyncExc((unsigned long)scriptThreadState->thread_id, PyExc_KeyboardInterrupt);
m_isInterrupt = true;
}
用户点击"停止"按钮时触发中断。
使用 PyThreadState_SetAsyncExc 强行注入 KeyboardInterrupt 异常。
断点跳转优化
cpp
if (breakPointBefore < breakPointLine && breakPointLine < breakPointAfter)