Add a static context for connecting Qt signals for python extensions

This commit is contained in:
baldurk
2026-08-13 17:46:40 +01:00
parent b1f0b9d310
commit 768d8fa615
5 changed files with 116 additions and 60 deletions
+62 -27
View File
@@ -129,6 +129,7 @@ struct OutputRedirector
uint64_t dummy;
PythonContext *context;
};
bool selfDeleting;
int isStdError;
bool block;
rdcstr extension;
@@ -147,6 +148,7 @@ PyObject *PythonContext::main_dict = NULL;
PyObject *PythonContext::m_DebugPy = NULL;
PyObject *PythonContext::m_CallWrapper = NULL;
PyObject *PythonContext::m_CallWrapperGlobals = NULL;
PythonContext *PythonContext::m_ExtensionContext = NULL;
QMap<rdcstr, PyObject *> PythonContext::extensions;
static PyObject *current_global_handle = NULL;
@@ -550,6 +552,7 @@ void PythonContext::GlobalInit(PersistantConfig &config)
OutputRedirector *output = (OutputRedirector *)redirector;
output->isStdError = 0;
output->selfDeleting = false;
output->context = NULL;
output->block = false;
@@ -558,6 +561,7 @@ void PythonContext::GlobalInit(PersistantConfig &config)
output = (OutputRedirector *)redirector;
output->isStdError = 1;
output->selfDeleting = false;
output->context = NULL;
output->block = false;
}
@@ -897,6 +901,9 @@ except:
// release GIL so that python work can now happen on any thread
PyEval_SaveThread();
// this will leak effectively
m_ExtensionContext = new PythonContext(true, NULL);
}
bool PythonContext::initialised()
@@ -904,7 +911,7 @@ bool PythonContext::initialised()
return main_dict != NULL;
}
PythonContext::PythonContext(QObject *parent) : QObject(parent)
PythonContext::PythonContext(bool extensionContext, QObject *parent) : QObject(parent)
{
if(!initialised())
return;
@@ -915,8 +922,6 @@ PythonContext::PythonContext(QObject *parent) : QObject(parent)
// clone our own local context
context_namespace = PyDict_Copy(main_dict);
PyObject *rlcompleter = PyImport_ImportModule("rlcompleter");
// for compatibility with earlier versions of python that took a char * instead of const char *
char noparams[1] = "";
@@ -929,10 +934,13 @@ PythonContext::PythonContext(QObject *parent) : QObject(parent)
OutputRedirector *output = (OutputRedirector *)redirector;
output->context = this;
output->selfDeleting = !extensionContext;
output->block = false;
Py_DECREF(redirector);
}
PyObject *rlcompleter = PyImport_ImportModule("rlcompleter");
if(rlcompleter)
{
PyObject *Completer = PyObject_SafeGetAttrString(rlcompleter, "Completer");
@@ -1237,6 +1245,7 @@ QString PythonContext::LoadExtension(ICaptureContext &ctx, const rdcstr &extensi
OutputRedirector *redir = (OutputRedirector *)ext_context;
redir->isStdError = 0;
redir->selfDeleting = false;
redir->context = NULL;
redir->block = false;
redir->extension = extension;
@@ -1415,7 +1424,8 @@ void PythonContext::executeString(const QString &filename, const QString &source
if(!initialised())
{
FlushOutput();
emit exception(lit("SystemError"), tr("Python integration failed to initialise."), -1, {});
emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."),
-1, {});
return;
}
@@ -1534,7 +1544,7 @@ void PythonContext::executeString(const QString &filename, const QString &source
if(caughtException)
{
FlushOutput();
emit exception(typeStr, valueStr, finalLine, frames);
emit exception(QString(), typeStr, valueStr, finalLine, frames);
}
}
@@ -1550,8 +1560,8 @@ void PythonContext::executeFile(const QString &filename)
if(!f.exists())
{
FlushOutput();
emit exception(lit("FileNotFoundError"), tr("No such file or directory: %1").arg(filename), -1,
{});
emit exception(QString(), lit("FileNotFoundError"),
tr("No such file or directory: %1").arg(filename), -1, {});
return;
}
@@ -1564,7 +1574,8 @@ void PythonContext::executeFile(const QString &filename)
else
{
FlushOutput();
emit exception(lit("IOError"), QFormatStr("%1: %2").arg(f.errorString()).arg(filename), -1, {});
emit exception(QString(), lit("IOError"),
QFormatStr("%1: %2").arg(f.errorString()).arg(filename), -1, {});
}
}
@@ -1573,7 +1584,8 @@ void PythonContext::setGlobal(const char *varName, const char *typeName, void *o
if(!initialised())
{
FlushOutput();
emit exception(lit("SystemError"), tr("Python integration failed to initialise."), -1, {});
emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."),
-1, {});
return;
}
@@ -1592,7 +1604,7 @@ void PythonContext::setGlobal(const char *varName, const char *typeName, void *o
if(ret != 0)
{
FlushOutput();
emit exception(lit("RuntimeError"),
emit exception(QString(), lit("RuntimeError"),
tr("Failed to set variable '%1' of type '%2'")
.arg(QString::fromUtf8(varName))
.arg(QString::fromUtf8(typeName)),
@@ -1854,28 +1866,33 @@ void PythonContext::outputTick()
{
QMutexLocker lock(&outputMutex);
if(!outstr.isEmpty())
for(QString &extension : outputCaches.keys())
{
emit textOutput(false, outstr);
}
OutputPair &o = outputCaches[extension];
if(!errstr.isEmpty())
{
emit textOutput(true, errstr);
}
if(!o.outstr.isEmpty())
{
emit textOutput(extension, false, o.outstr);
}
outstr.clear();
errstr.clear();
if(!o.errstr.isEmpty())
{
emit textOutput(extension, true, o.errstr);
}
o.outstr.clear();
o.errstr.clear();
}
}
void PythonContext::addText(bool isStdError, const QString &output)
void PythonContext::addText(QString context, bool isStdError, const QString &output)
{
QMutexLocker lock(&outputMutex);
if(isStdError)
errstr += output;
outputCaches[context].errstr += output;
else
outstr += output;
outputCaches[context].outstr += output;
}
void PythonContext::setPyGlobal(const char *varName, PyObject *obj)
@@ -1883,7 +1900,8 @@ void PythonContext::setPyGlobal(const char *varName, PyObject *obj)
if(!initialised())
{
FlushOutput();
emit exception(lit("SystemError"), tr("Python integration failed to initialise."), -1, {});
emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."),
-1, {});
return;
}
@@ -1900,7 +1918,7 @@ void PythonContext::setPyGlobal(const char *varName, PyObject *obj)
return;
FlushOutput();
emit exception(lit("RuntimeError"),
emit exception(QString(), lit("RuntimeError"),
tr("Failed to set variable '%1'").arg(QString::fromUtf8(varName)), -1, {});
}
@@ -1908,7 +1926,7 @@ void PythonContext::outstream_del(PyObject *self)
{
OutputRedirector *redirector = (OutputRedirector *)self;
if(redirector)
if(redirector && redirector->selfDeleting)
{
PythonContext *context = redirector->context;
@@ -1933,6 +1951,7 @@ PyObject *PythonContext::outstream_write(PyObject *self, PyObject *args)
if(redirector)
{
PythonContext *context = redirector->context;
QString extension = redirector->extension;
// most likely this is NULL because the sys.stdout override is static and shared amongst
// contexts. So look up the global variable that stores the context
if(context == NULL)
@@ -1950,7 +1969,10 @@ PyObject *PythonContext::outstream_write(PyObject *self, PyObject *args)
OutputRedirector *global =
(OutputRedirector *)PyDict_GetItemString(globals, "_renderdoc_internal");
if(global)
{
context = global->context;
extension = global->extension;
}
}
Py_XDECREF(globals);
@@ -1974,10 +1996,16 @@ PyObject *PythonContext::outstream_write(PyObject *self, PyObject *args)
if(context)
{
context->addText(redirector->isStdError ? true : false, QString::fromUtf8(text));
context->addText(extension, redirector->isStdError ? true : false, QString::fromUtf8(text));
}
else
{
if(!extension.isEmpty())
{
m_ExtensionContext->addText(extension, redirector->isStdError ? true : false,
QString::fromUtf8(text));
}
// if context is still NULL we're running in the extension context
rdcstr message = text;
@@ -2303,10 +2331,17 @@ extern "C" void HandleException(PyObject *global_handle)
if(redirector && redirector->context)
{
redirector->context->FlushOutput();
emit redirector->context->exception(typeStr, valueStr, finalLine, frames);
emit redirector->context->exception(redirector->extension, typeStr, valueStr, finalLine, frames);
}
else
{
if(redirector && !redirector->extension.empty())
{
PythonContext::GetExtensionContext()->FlushOutput();
emit PythonContext::GetExtensionContext()->exception(redirector->extension, typeStr, valueStr,
finalLine, frames);
}
// if still NULL we're running in the extension context
rdcstr exString;
+20 -6
View File
@@ -47,8 +47,9 @@ private:
// on and needs to finish executing after the external code is done with the context
~PythonContext();
explicit PythonContext(bool extensionContext, QObject *parent);
public:
explicit PythonContext(QObject *parent = NULL);
explicit PythonContext(QObject *parent = NULL) : PythonContext(false, parent) {}
void Finish();
PyThreadState *GetExecutingThreadState() { return m_State; }
@@ -94,7 +95,7 @@ public:
if(obj)
setPyGlobal(varName, obj);
else
emit exception(lit("RuntimeError"),
emit exception(QString(), lit("RuntimeError"),
tr("Failed to set variable '%1' of type '%2'")
.arg(QString::fromUtf8(varName))
.arg(QString::fromUtf8(typeName)),
@@ -114,10 +115,14 @@ public:
int currentLine() { return location.line; }
static void AddDebuggableThread();
static void RemoveDebuggableThread();
static PythonContext *GetExtensionContext() { return m_ExtensionContext; }
signals:
void traceLine(const QString &file, int line);
void exception(const QString &type, const QString &value, int finalLine, QList<QString> frames);
void textOutput(bool isStdError, const QString &output);
void exception(const QString &extension, const QString &type, const QString &value, int finalLine,
QList<QString> frames);
void textOutput(const QString &extension, bool isStdError, const QString &output);
public slots:
void executeString(const QString &source);
@@ -139,6 +144,10 @@ private:
static PyObject *m_CallWrapper;
static PyObject *m_CallWrapperGlobals;
// a statically created PythonContext for extension events/output.
// each extension has its own dictionary but this is used so that users can connect to it and receieve events
static PythonContext *m_ExtensionContext;
// the list of extension objects, to be able to reload them
static QMap<rdcstr, PyObject *> extensions;
@@ -170,10 +179,15 @@ private:
QTimer *outputTicker = NULL;
QMutex outputMutex;
QString outstr, errstr;
struct OutputPair
{
QString outstr, errstr;
};
QMap<QString, OutputPair> outputCaches;
void outputTick();
void addText(bool isStdError, const QString &output);
void addText(QString extension, bool isStdError, const QString &output);
// Python callbacks
static void outstream_del(PyObject *self);
+19 -19
View File
@@ -673,31 +673,31 @@ int main(int argc, char *argv[])
py.ctx().setGlobal("pyrenderdoc", (ICaptureContext *)&ctx);
QObject::connect(
&py.ctx(), &PythonContext::exception,
[&pythonExited](const QString &type, const QString &value, int, QList<QString> frames) {
if(type == lit("SystemExit"))
{
pythonExited = true;
return;
}
QObject::connect(&py.ctx(), &PythonContext::exception,
[&pythonExited](const QString &, const QString &type, const QString &value,
int, QList<QString> frames) {
if(type == lit("SystemExit"))
{
pythonExited = true;
return;
}
QString exString;
QString exString;
if(!frames.isEmpty())
{
exString += tr("Traceback (most recent call last):\n");
for(const QString &f : frames)
exString += QFormatStr(" %1\n").arg(f);
}
if(!frames.isEmpty())
{
exString += tr("Traceback (most recent call last):\n");
for(const QString &f : frames)
exString += QFormatStr(" %1\n").arg(f);
}
exString += QFormatStr("%1: %2\n").arg(type).arg(value);
exString += QFormatStr("%1: %2\n").arg(type).arg(value);
qCritical("%s", exString.toUtf8().data());
});
qCritical("%s", exString.toUtf8().data());
});
QObject::connect(&py.ctx(), &PythonContext::textOutput,
[](bool isStdError, const QString &output) {
[](const QString &, bool isStdError, const QString &output) {
if(isStdError)
qCritical("%s", output.toUtf8().data());
else
+12 -6
View File
@@ -1052,6 +1052,11 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
enableButtons(true);
QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::textOutput, this,
&PythonShell::textOutput);
QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::exception, this,
&PythonShell::exception);
// reset output to default
on_clear_clicked();
on_newScript_clicked();
@@ -1282,8 +1287,8 @@ void PythonShell::traceLine(const QString &file, int line)
scriptEditor->markerAdd(line > 0 ? line - 1 : 0, CURRENT_MARKER + 1);
}
void PythonShell::exception(const QString &type, const QString &value, int finalLine,
QList<QString> frames)
void PythonShell::exception(const QString &extension, const QString &type, const QString &value,
int finalLine, QList<QString> frames)
{
QTextEdit *out = ui->scriptOutput;
if(QObject::sender() == (QObject *)interactiveContext)
@@ -1307,7 +1312,7 @@ void PythonShell::exception(const QString &type, const QString &value, int final
appendText(out, exString);
}
void PythonShell::textOutput(bool isStdError, const QString &output)
void PythonShell::textOutput(const QString &extension, bool isStdError, const QString &output)
{
QTextEdit *out = ui->scriptOutput;
if(QObject::sender() == (QObject *)interactiveContext)
@@ -1431,9 +1436,10 @@ void PythonShell::refreshCurrentHelp()
ui->helpText->clear();
QObject::connect(
context, &PythonContext::textOutput,
[this](bool isStdError, const QString &output) { appendText(ui->helpText, output); });
QObject::connect(context, &PythonContext::textOutput,
[this](const QString &, bool isStdError, const QString &output) {
appendText(ui->helpText, output);
});
context->executeString(lit(R"(
try:
+3 -2
View File
@@ -78,8 +78,9 @@ private slots:
void interactive_keypress(QKeyEvent *e);
void helpSearch_keypress(QKeyEvent *e);
void traceLine(const QString &file, int line);
void exception(const QString &type, const QString &value, int finalLine, QList<QString> frames);
void textOutput(bool isStdError, const QString &output);
void exception(const QString &extension, const QString &type, const QString &value, int finalLine,
QList<QString> frames);
void textOutput(const QString &extension, bool isStdError, const QString &output);
void editor_contextMenu(const QPoint &pos);
private: