Files
renderdoc/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp
T
baldurk e5f4ca7bb8 Remove use of const char * in public API and OS specific where possible
* This prevents unnecessary conversions back and forth between rdcstr and const
  char * when going through interfaces. In the OS specific layer this is rarely
  an issue because most of the implementations don't convert to rdcstr, but it
  is convenient to be able to pass in an rdcstr directly. The few cases where
  there's an unecessary construction of an rdcstr is acceptable.
* A couple of places in the public API need to return a string from a global
  function, so can't return an rdcstr due to C ABI, so they still return a const
  char *.
* Similarly const char * is kept for logging, to avoid a dependency on rdcstr
  and because that's one place where unnecessary conversions/constructions may
  be impactful.
2020-12-07 17:44:50 +00:00

1393 lines
35 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2020 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#ifdef slots
#undef slots
#define slots_was_defined
#endif
// must be included first
#include <Python.h>
#include <frameobject.h>
#ifdef slots_was_defined
#define slots
#endif
#if PYSIDE2_ENABLED
// PySide Qt integration, must be included before Qt headers
// warning C4522: 'Shiboken::AutoDecRef': multiple assignment operators specified
#pragma warning(disable : 4522)
#include <pyside.h>
#include <shiboken.h>
PyTypeObject **SbkPySide2_QtCoreTypes = NULL;
PyTypeObject **SbkPySide2_QtGuiTypes = NULL;
PyTypeObject **SbkPySide2_QtWidgetsTypes = NULL;
#else
// for non-windows, this message is displayed at CMake time.
#ifdef _MSC_VER
#pragma message( \
"Building without PySide2 - Qt will not be accessible in python scripting. See https://github.com/baldurk/renderdoc/wiki/PySide2")
#endif
#endif
#ifdef _MSC_VER
// for the LoadLibrary call on 32-bit windows
#include <windows.h>
#endif
#include <QApplication>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QThread>
#include <QTimer>
#include "Code/QRDUtils.h"
#include "PythonContext.h"
#include "version.h"
// exported by generated files, used to check interface compliance
bool CheckCoreInterface(rdcstr &log);
bool CheckQtInterface(rdcstr &log);
// defined in SWIG-generated renderdoc_python.cpp
extern "C" PyObject *PyInit_renderdoc(void);
extern "C" PyObject *PassObjectToPython(const char *type, void *obj);
extern "C" PyObject *PassNewObjectToPython(const char *type, void *obj);
// this one is in qrenderdoc_python.cpp
extern "C" PyObject *PyInit_qrenderdoc(void);
extern "C" PyObject *WrapBareQWidget(QWidget *);
extern "C" QWidget *UnwrapBareQWidget(PyObject *);
// little utility function to convert a PyObject * that we know is a string to a QString
static inline QString ToQStr(PyObject *value)
{
if(value)
{
PyObject *repr = PyObject_Str(value);
if(repr == NULL)
return QString();
PyObject *decoded = PyUnicode_AsUTF8String(repr);
if(decoded == NULL)
return QString();
QString ret = QString::fromUtf8(PyBytes_AsString(decoded));
Py_DecRef(decoded);
Py_DecRef(repr);
return ret;
}
return QString();
}
static wchar_t program_name[] = L"qrenderdoc";
static wchar_t python_home[1024] = {0};
struct OutputRedirector
{
PyObject_HEAD;
union
{
// we union with a uint64_t to ensure it's always a ulonglong even on 32-bit
uint64_t dummy;
PythonContext *context;
};
int isStdError;
bool block;
};
static PyTypeObject OutputRedirectorType = {PyVarObject_HEAD_INIT(NULL, 0)};
static PyMethodDef OutputRedirector_methods[] = {
{"write", NULL, METH_VARARGS, "Writes to the output window"},
{"flush", NULL, METH_NOARGS, "Does nothing - only provided for compatibility"},
{NULL}};
PyObject *PythonContext::main_dict = NULL;
QMap<rdcstr, PyObject *> PythonContext::extensions;
static PyObject *current_global_handle = NULL;
static QMutex decrefQueueMutex;
static QList<PyObject *> decrefQueue;
extern "C" void ProcessDecRefQueue();
void FetchException(QString &typeStr, QString &valueStr, int &finalLine, QList<QString> &frames)
{
PyObject *exObj = NULL, *valueObj = NULL, *tracebackObj = NULL;
PyErr_Fetch(&exObj, &valueObj, &tracebackObj);
PyErr_NormalizeException(&exObj, &valueObj, &tracebackObj);
if(exObj && PyType_Check(exObj))
{
PyTypeObject *type = (PyTypeObject *)exObj;
typeStr = QString::fromUtf8(type->tp_name);
}
else
{
typeStr = QString();
}
if(valueObj)
valueStr = ToQStr(valueObj);
if(tracebackObj)
{
PyObject *tracebackModule = PyImport_ImportModule("traceback");
if(tracebackModule)
{
PyObject *func = PyObject_GetAttrString(tracebackModule, "format_tb");
if(func && PyCallable_Check(func))
{
PyObject *args = Py_BuildValue("(O)", tracebackObj);
PyObject *formattedTB = PyObject_CallObject(func, args);
PyTracebackObject *tb = (PyTracebackObject *)tracebackObj;
while(tb->tb_next)
tb = tb->tb_next;
finalLine = tb->tb_lineno;
if(formattedTB)
{
Py_ssize_t size = PyList_Size(formattedTB);
for(Py_ssize_t i = 0; i < size; i++)
{
PyObject *el = PyList_GetItem(formattedTB, i);
frames << ToQStr(el).trimmed();
}
Py_DecRef(formattedTB);
}
Py_DecRef(args);
}
}
}
Py_DecRef(exObj);
Py_DecRef(valueObj);
Py_DecRef(tracebackObj);
}
void PythonContext::GlobalInit()
{
// must happen on the UI thread
if(qApp->thread() != QThread::currentThread())
{
qFatal("PythonContext::GlobalInit MUST be called from the UI thread");
return;
}
// for the exception signal
qRegisterMetaType<QList<QString>>("QList<QString>");
PyImport_AppendInittab("renderdoc", &PyInit_renderdoc);
PyImport_AppendInittab("qrenderdoc", &PyInit_qrenderdoc);
#if defined(STATIC_QRENDERDOC)
// add the location where our libs will be for statically-linked python installs
{
QDir bin = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir();
QString pylibs = QDir::cleanPath(bin.absoluteFilePath(lit("../share/renderdoc/pylibs")));
pylibs.toWCharArray(python_home);
Py_SetPythonHome(python_home);
}
#endif
Py_SetProgramName(program_name);
Py_IgnoreEnvironmentFlag = 1;
Py_Initialize();
PyEval_InitThreads();
OutputRedirectorType.tp_name = "renderdoc_output_redirector";
OutputRedirectorType.tp_basicsize = sizeof(OutputRedirector);
OutputRedirectorType.tp_flags = Py_TPFLAGS_DEFAULT;
OutputRedirectorType.tp_doc =
"Output redirector, to be able to catch output to stdout and stderr";
OutputRedirectorType.tp_new = PyType_GenericNew;
OutputRedirectorType.tp_dealloc = &PythonContext::outstream_del;
OutputRedirectorType.tp_methods = OutputRedirector_methods;
OutputRedirector_methods[0].ml_meth = &PythonContext::outstream_write;
OutputRedirector_methods[1].ml_meth = &PythonContext::outstream_flush;
PyObject *main_module = PyImport_AddModule("__main__");
PyModule_AddObject(main_module, "renderdoc", PyImport_ImportModule("renderdoc"));
PyModule_AddObject(main_module, "qrenderdoc", PyImport_ImportModule("qrenderdoc"));
main_dict = PyModule_GetDict(main_module);
// replace sys.stdout and sys.stderr with our own objects. These have a 'this' pointer of NULL,
// which then indicates they need to forward to a global object
// import sys
PyDict_SetItemString(main_dict, "sys", PyImport_ImportModule("sys"));
PyObject *rlcompleter = PyImport_ImportModule("rlcompleter");
if(rlcompleter)
{
PyDict_SetItemString(main_dict, "rlcompleter", rlcompleter);
}
else
{
// ignore a failed import
PyErr_Clear();
}
// sysobj = sys
PyObject *sysobj = PyDict_GetItemString(main_dict, "sys");
// sysobj.stdout = renderdoc_output_redirector()
// sysobj.stderr = renderdoc_output_redirector()
if(PyType_Ready(&OutputRedirectorType) >= 0)
{
// for compatibility with earlier versions of python that took a char * instead of const char *
char noparams[1] = "";
PyObject *redirector = PyObject_CallFunction((PyObject *)&OutputRedirectorType, noparams);
PyObject_SetAttrString(sysobj, "stdout", redirector);
OutputRedirector *output = (OutputRedirector *)redirector;
output->isStdError = 0;
output->context = NULL;
output->block = false;
redirector = PyObject_CallFunction((PyObject *)&OutputRedirectorType, noparams);
PyObject_SetAttrString(sysobj, "stderr", redirector);
output = (OutputRedirector *)redirector;
output->isStdError = 1;
output->context = NULL;
output->block = false;
}
// if we need to append to sys.path to locate PySide2, do that now
#if defined(PYSIDE2_SYS_PATH)
{
PyObject *syspath = PyObject_GetAttrString(sysobj, "path");
#ifndef STRINGIZE
#define STRINGIZE2(a) #a
#define STRINGIZE(a) STRINGIZE2(a)
#endif
PyObject *str = PyUnicode_FromString(STRINGIZE(PYSIDE2_SYS_PATH));
PyList_Append(syspath, str);
Py_DecRef(str);
Py_DecRef(syspath);
}
#endif
#if RENDERDOC_STABLE_BUILD == 0
// if we're running in the git checkout and we can find the test scripts, add that location to the
// path
{
QDir bin = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir();
QString testpath = QDir::cleanPath(bin.absoluteFilePath(lit("../../util/test")));
if(QDir(testpath).exists(lit("run_tests.py")))
{
PyObject *syspath = PyObject_GetAttrString(sysobj, "path");
PyObject *str = PyUnicode_FromString(testpath.toUtf8().data());
PyList_Append(syspath, str);
Py_DecRef(str);
Py_DecRef(syspath);
}
}
#endif
// set up PySide
#if PYSIDE2_ENABLED
{
// hack for win32 builds, where our pyside2 accidentally depends on Qt5Qml.dll for no good
// reason and we ship a stub to allow the dll to load instead of rebuilding the whole of pyside2
// :S
#if defined(_MSC_VER) && !defined(_M_X64)
QString Qt5QmlStub = QApplication::applicationDirPath() + lit("/PySide2/Qt5Qml.dll");
LoadLibraryA(Qt5QmlStub.toUtf8().data());
#endif
Shiboken::AutoDecRef core(Shiboken::Module::import("PySide2.QtCore"));
if(!core.isNull())
SbkPySide2_QtCoreTypes = Shiboken::Module::getTypes(core);
else
qCritical() << "Failed to load PySide2.QtCore";
Shiboken::AutoDecRef gui(Shiboken::Module::import("PySide2.QtGui"));
if(!gui.isNull())
SbkPySide2_QtGuiTypes = Shiboken::Module::getTypes(gui);
else
qCritical() << "Failed to load PySide2.QtGui";
Shiboken::AutoDecRef widgets(Shiboken::Module::import("PySide2.QtWidgets"));
if(!widgets.isNull())
SbkPySide2_QtWidgetsTypes = Shiboken::Module::getTypes(widgets);
else
qCritical() << "Failed to load PySide2.QtWidgets";
}
#endif
// release GIL so that python work can now happen on any thread
PyEval_SaveThread();
}
bool PythonContext::initialised()
{
return main_dict != NULL;
}
PythonContext::PythonContext(QObject *parent) : QObject(parent)
{
if(!initialised())
return;
// acquire the GIL and make sure this thread is init'd
PyGILState_STATE gil = PyGILState_Ensure();
// clone our own local context
context_namespace = PyDict_Copy(main_dict);
PyObject *rlcompleter = PyDict_GetItemString(main_dict, "rlcompleter");
// for compatibility with earlier versions of python that took a char * instead of const char *
char noparams[1] = "";
// set global output that point to this context. It is responsible for deleting the context when
// it goes out of scope
PyObject *redirector = PyObject_CallFunction((PyObject *)&OutputRedirectorType, noparams);
if(redirector)
{
PyDict_SetItemString(context_namespace, "_renderdoc_internal", redirector);
OutputRedirector *output = (OutputRedirector *)redirector;
output->context = this;
output->block = false;
Py_DECREF(redirector);
}
if(rlcompleter)
{
PyObject *Completer = PyObject_GetAttrString(rlcompleter, "Completer");
if(Completer)
{
// create a completer for our context's namespace
m_Completer = PyObject_CallFunction(Completer, "O", context_namespace);
if(m_Completer)
{
PyDict_SetItemString(context_namespace, "_renderdoc_completer", m_Completer);
}
else
{
QString typeStr;
QString valueStr;
int finalLine = -1;
QList<QString> frames;
FetchException(typeStr, valueStr, finalLine, frames);
// failure is not fatal
qWarning() << "Couldn't create completion object. " << typeStr << ": " << valueStr;
PyErr_Clear();
}
}
Py_DecRef(Completer);
}
else
{
m_Completer = NULL;
}
// release the GIL again
PyGILState_Release(gil);
// every 100ms while running, check for new output
outputTicker = new QTimer(this);
outputTicker->setInterval(100);
QObject::connect(outputTicker, &QTimer::timeout, this, &PythonContext::outputTick);
// we have to start it here, because we can't start on another thread.
outputTicker->start();
}
PythonContext::~PythonContext()
{
PyGILState_STATE gil = PyGILState_Ensure();
if(m_Completer)
Py_DecRef(m_Completer);
PyGILState_Release(gil);
// do a final tick to gather any remaining output
outputTick();
}
bool PythonContext::CheckInterfaces(rdcstr &log)
{
bool errors = false;
PyGILState_STATE gil = PyGILState_Ensure();
errors |= CheckCoreInterface(log);
errors |= CheckQtInterface(log);
for(rdcstr module_name : {"renderdoc", "qrenderdoc"})
{
PyObject *mod = PyImport_ImportModule(module_name.c_str());
PyObject *dict = PyModule_GetDict(mod);
PyObject *key, *value;
Py_ssize_t pos = 0;
while(PyDict_Next(dict, &pos, &key, &value))
{
rdcstr name = ToQStr(key);
if(name.beginsWith("__"))
continue;
if(!PyCallable_Check(value))
{
log += "Non-callable object found: " + module_name + "." + name +
". Expected only classes and functions.\n";
errors = true;
}
}
Py_DECREF(mod);
}
PyGILState_Release(gil);
log.trim();
return errors;
}
void PythonContext::Finish()
{
PyGILState_STATE gil = PyGILState_Ensure();
// release our external handle to globals. It'll now only be ref'd from inside
Py_XDECREF(context_namespace);
PyGILState_Release(gil);
}
void PythonContext::PausePythonThreading()
{
m_SavedThread = PyEval_SaveThread();
}
void PythonContext::ResumePythonThreading()
{
PyEval_RestoreThread((PyThreadState *)m_SavedThread);
m_SavedThread = NULL;
}
void PythonContext::GlobalShutdown()
{
if(!initialised())
return;
// must happen on the UI thread
if(qApp->thread() != QThread::currentThread())
{
qFatal("PythonContext::GlobalShutdown MUST be called from the UI thread");
return;
}
// acquire the GIL, so we can shut down
PyGILState_Ensure();
Py_Finalize();
}
QStringList PythonContext::GetApplicationExtensionsPaths()
{
QStringList ret;
for(QString d : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation))
{
QDir dir(d);
dir.cd(lit("extensions"));
if(dir.exists())
ret.append(dir.absolutePath());
}
return ret;
}
void PythonContext::ProcessExtensionWork(std::function<void()> callback)
{
PyGILState_STATE gil = PyGILState_Ensure();
callback();
PyGILState_Release(gil);
}
bool PythonContext::LoadExtension(ICaptureContext &ctx, const rdcstr &extension)
{
PyObject *sysobj = PyDict_GetItemString(main_dict, "sys");
PyObject *syspath = PyObject_GetAttrString(sysobj, "path");
// add extensions directories in
for(QString p : PythonContext::GetApplicationExtensionsPaths())
{
QDir dir(p);
rdcstr path = dir.absolutePath();
if(dir.exists())
{
PyObject *str = PyUnicode_FromString(path.c_str());
PyList_Append(syspath, str);
Py_DecRef(str);
}
}
PyObject *ext = NULL;
current_global_handle = PyObject_GetAttrString(sysobj, "stdout");
if(extensions[extension] == NULL)
{
qInfo() << "First load of " << QString(extension);
ext = PyImport_ImportModule(extension.c_str());
}
else
{
qInfo() << "Reloading " << QString(extension);
// call unregister() if it exists
PyObject *unregister_func = PyObject_GetAttrString(extensions[extension], "unregister");
if(unregister_func)
{
PyObject *retval = PyObject_CallFunction(unregister_func, "");
// discard the return value, regardless of error we don't abort the reload
Py_XDECREF(retval);
}
// if the extension is a package, we need to manually reload any loaded submodules
PyObject *sysmodules = PyObject_GetAttrString(sysobj, "modules");
PyObject *keys = PyDict_Keys(sysmodules);
QString search = extension + lit(".");
bool reloadSuccess = true;
if(keys)
{
Py_ssize_t len = PyList_Size(keys);
for(Py_ssize_t i = 0; i < len; i++)
{
PyObject *key = PyList_GetItem(keys, i);
PyObject *value = PyDict_GetItem(sysmodules, key);
QString keystr = ToQStr(key);
if(keystr.contains(search))
{
qInfo() << "Reloading submodule " << keystr;
PyObject *mod = PyImport_ReloadModule(value);
if(mod == NULL)
{
qCritical() << "Failed to reload " << keystr;
reloadSuccess = false;
break;
}
// we don't need the reference, we just wanted to reload it
Py_DECREF(mod);
value = PyDict_GetItem(sysmodules, key);
if(value != mod)
qCritical() << "sys.modules[" << keystr << "]"
<< " after reload doesn't match reloaded object";
}
}
Py_DECREF(keys);
}
if(reloadSuccess)
ext = PyImport_ReloadModule(extensions[extension]);
}
// if import succeeded, store this extension module in our map. If import failed, we might have
// failed a reimport in which case the original module is still there and valid, so don't
// overwrite the value.
if(ext)
{
extensions[extension] = ext;
PyModule_AddObject(ext, "_renderdoc_internal", current_global_handle);
}
QString typeStr;
QString valueStr;
int finalLine = -1;
QList<QString> frames;
if(ext)
{
// if import succeeded, call register()
PyObject *register_func = PyObject_GetAttrString(ext, "register");
if(register_func)
{
PyObject *pyctx =
PassObjectToPython((rdcstr(TypeName<ICaptureContext>()) + " *").c_str(), &ctx);
PyObject *retval = NULL;
if(pyctx)
{
retval = PyObject_CallFunction(register_func, "sO", MAJOR_MINOR_VERSION_STRING, pyctx);
}
else
{
qCritical() << "Internal error passing pyrenderdoc to extension register()";
}
if(retval == NULL)
ext = NULL;
Py_XDECREF(retval);
if(ext)
{
int ret = PyModule_AddObject(ext, "pyrenderdoc", pyctx);
if(ret != 0)
ext = NULL;
}
Py_XDECREF(pyctx);
}
else
{
ext = NULL;
}
}
else
{
ext = NULL;
}
if(!ext)
{
FetchException(typeStr, valueStr, finalLine, frames);
qCritical("Error importing extension module. %s: %s", typeStr.toUtf8().data(),
valueStr.toUtf8().data());
if(!frames.isEmpty())
{
qCritical() << "Traceback (most recent call last):";
for(const QString &f : frames)
{
QStringList lines = f.split(QLatin1Char('\n'));
for(const QString &line : lines)
qCritical("%s", line.toUtf8().data());
}
}
}
Py_ssize_t len = PyList_Size(syspath);
PyList_SetSlice(syspath, len - 1, len, NULL);
Py_DecRef(syspath);
current_global_handle = NULL;
return ext != NULL;
}
void PythonContext::ConvertPyArgs(const ExtensionCallbackData &data,
rdcarray<rdcpair<rdcstr, PyObject *>> &args)
{
PyGILState_STATE gil = PyGILState_Ensure();
args.resize(data.size());
for(size_t i = 0; i < data.size(); i++)
{
rdcpair<rdcstr, PyObject *> &a = args[i];
a.first = data[i].first;
// convert QVariant to python object
const QVariant &in = data[i].second;
PyObject *&out = a.second;
// coverity[mixed_enums]
QMetaType::Type type = (QMetaType::Type)in.type();
switch(type)
{
case QMetaType::Bool: out = PyBool_FromLong(in.toBool()); break;
case QMetaType::Short:
case QMetaType::Long:
case QMetaType::Int: out = PyLong_FromLong(in.toInt()); break;
case QMetaType::UShort:
case QMetaType::ULong:
case QMetaType::UInt: out = PyLong_FromUnsignedLong(in.toUInt()); break;
case QMetaType::LongLong: out = PyLong_FromLongLong(in.toLongLong()); break;
case QMetaType::ULongLong: out = PyLong_FromUnsignedLongLong(in.toULongLong()); break;
case QMetaType::Float: out = PyFloat_FromDouble(in.toFloat()); break;
case QMetaType::Double: out = PyFloat_FromDouble(in.toDouble()); break;
case QMetaType::QString: out = PyUnicode_FromString(in.toString().toUtf8().data()); break;
default: break;
}
if(!out)
{
// try various other types
if(in.userType() == qMetaTypeId<ResourceId>())
out = PassNewObjectToPython("ResourceId *", new ResourceId(in.value<ResourceId>()));
}
if(!out)
{
qCritical() << "Couldn't convert" << in << "to python object";
out = Py_None;
Py_XINCREF(out);
}
}
PyGILState_Release(gil);
}
void PythonContext::FreePyArgs(rdcarray<rdcpair<rdcstr, PyObject *>> &args)
{
PyGILState_STATE gil = PyGILState_Ensure();
for(rdcpair<rdcstr, PyObject *> &a : args)
{
Py_XDECREF(a.second);
}
PyGILState_Release(gil);
}
QString PythonContext::versionString()
{
return QFormatStr("%1.%2.%3").arg(PY_MAJOR_VERSION).arg(PY_MINOR_VERSION).arg(PY_MICRO_VERSION);
}
void PythonContext::executeString(const QString &filename, const QString &source)
{
if(!initialised())
{
emit exception(
lit("SystemError"),
tr("Python integration failed to initialise, see diagnostic log for more information."), -1,
{});
return;
}
location.file = filename;
location.line = 1;
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *compiled =
Py_CompileString(source.toUtf8().data(), filename.toUtf8().data(),
source.count(QLatin1Char('\n')) == 0 ? Py_single_input : Py_file_input);
PyObject *ret = NULL;
if(compiled)
{
PyObject *traceContext = PyDict_New();
uintptr_t thisint = (uintptr_t) this;
uint64_t thisuint64 = (uint64_t)thisint;
PyObject *thisobj = PyLong_FromUnsignedLongLong(thisuint64);
PyDict_SetItemString(traceContext, "thisobj", thisobj);
PyDict_SetItemString(traceContext, "compiled", compiled);
PyEval_SetTrace(&PythonContext::traceEvent, traceContext);
m_Abort = false;
m_State = PyGILState_GetThisThreadState();
ret = PyEval_EvalCode(compiled, context_namespace, context_namespace);
m_State = NULL;
// catch any output
outputTick();
PyEval_SetTrace(NULL, NULL);
ProcessDecRefQueue();
Py_XDECREF(thisobj);
Py_XDECREF(traceContext);
}
Py_DecRef(compiled);
QString typeStr;
QString valueStr;
int finalLine = -1;
QList<QString> frames;
bool caughtException = (ret == NULL);
if(caughtException)
FetchException(typeStr, valueStr, finalLine, frames);
Py_XDECREF(ret);
PyGILState_Release(gil);
if(caughtException)
emit exception(typeStr, valueStr, finalLine, frames);
}
void PythonContext::executeString(const QString &source)
{
executeString(lit("<interactive.py>"), source);
}
void PythonContext::executeFile(const QString &filename)
{
QFile f(filename);
if(!f.exists())
{
emit exception(lit("FileNotFoundError"), tr("No such file or directory: %1").arg(filename), -1,
{});
return;
}
if(f.open(QIODevice::ReadOnly | QIODevice::Text))
{
QByteArray py = f.readAll();
executeString(filename, QString::fromUtf8(py));
}
else
{
emit exception(lit("IOError"), QFormatStr("%1: %2").arg(f.errorString()).arg(filename), -1, {});
}
}
void PythonContext::setGlobal(const char *varName, const char *typeName, void *object)
{
if(!initialised())
{
emit exception(
lit("SystemError"),
tr("Python integration failed to initialise, see diagnostic log for more information."), -1,
{});
return;
}
PyGILState_STATE gil = PyGILState_Ensure();
// we don't need separate functions for each module, as they share type info
PyObject *obj = PassObjectToPython(typeName, object);
int ret = -1;
if(obj)
ret = PyDict_SetItemString(context_namespace, varName, obj);
PyGILState_Release(gil);
if(ret != 0)
{
emit exception(lit("RuntimeError"), tr("Failed to set variable '%1' of type '%2'")
.arg(QString::fromUtf8(varName))
.arg(QString::fromUtf8(typeName)),
-1, {});
return;
}
setPyGlobal(varName, obj);
}
template <>
void PythonContext::setGlobal(const char *varName, PyObject *object)
{
setPyGlobal(varName, object);
}
template <>
void PythonContext::setGlobal(const char *varName, QObject *object)
{
setQtGlobal(varName, object);
}
template <>
void PythonContext::setGlobal(const char *varName, QWidget *object)
{
setQtGlobal(varName, object);
}
QWidget *PythonContext::QWidgetFromPy(PyObject *widget)
{
#if PYSIDE2_ENABLED
if(!initialised())
return NULL;
if(widget == Py_None || widget == NULL)
return NULL;
if(!SbkPySide2_QtCoreTypes || !SbkPySide2_QtGuiTypes || !SbkPySide2_QtWidgetsTypes)
return UnwrapBareQWidget(widget);
if(!Shiboken::Object::checkType(widget))
return UnwrapBareQWidget(widget);
return (QWidget *)Shiboken::Object::cppPointer((SbkObject *)widget, Shiboken::SbkType<QWidget>());
#else
return UnwrapBareQWidget(widget);
#endif
}
QStringList PythonContext::completionOptions(QString base)
{
QStringList ret;
if(!m_Completer)
return ret;
QByteArray bytes = base.toUtf8();
const char *input = (const char *)bytes.data();
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *completeFunction = PyObject_GetAttrString(m_Completer, "complete");
int idx = 0;
PyObject *opt = NULL;
do
{
opt = PyObject_CallFunction(completeFunction, "si", input, idx);
if(opt && opt != Py_None)
{
QString optstr = ToQStr(opt);
bool add = true;
// little hack, remove some of the ugly swig template instantiations that we can't avoid.
if(optstr.contains(lit("renderdoc.rdcarray")) || optstr.contains(lit("renderdoc.rdcstr")) ||
optstr.contains(lit("renderdoc.bytebuf")))
add = false;
if(add)
ret << optstr;
}
idx++;
} while(opt && opt != Py_None);
// extra hack, remove the swig object functions/data but ONLY if we find a sure-fire identifier
// (thisown) since otherwise we could remove append from a list object
bool containsSwigInternals = false;
for(const QString &optstr : ret)
{
if(optstr.contains(lit(".thisown")))
{
containsSwigInternals = true;
break;
}
}
if(containsSwigInternals)
{
for(int i = 0; i < ret.count();)
{
if(ret[i].endsWith(lit(".acquire(")) || ret[i].endsWith(lit(".append(")) ||
ret[i].endsWith(lit(".disown(")) || ret[i].endsWith(lit(".next(")) ||
ret[i].endsWith(lit(".own(")) || ret[i].endsWith(lit(".this")) ||
ret[i].endsWith(lit(".thisown")))
ret.removeAt(i);
else
i++;
}
}
Py_DecRef(completeFunction);
PyGILState_Release(gil);
return ret;
}
PyObject *PythonContext::QtObjectToPython(const char *typeName, QObject *object)
{
#if PYSIDE2_ENABLED
if(!initialised())
Py_RETURN_NONE;
if(!SbkPySide2_QtCoreTypes || !SbkPySide2_QtGuiTypes || !SbkPySide2_QtWidgetsTypes)
{
QWidget *w = qobject_cast<QWidget *>(object);
if(w)
return WrapBareQWidget(w);
Py_RETURN_NONE;
}
if(object == NULL)
{
Py_RETURN_NONE;
}
PyObject *obj =
Shiboken::Object::newObject(reinterpret_cast<SbkObjectType *>(Shiboken::SbkType<QObject>()),
object, false, false, typeName);
return obj;
#else
QWidget *w = qobject_cast<QWidget *>(object);
if(w)
return WrapBareQWidget(w);
Py_RETURN_NONE;
#endif
}
// callback to flush output every so often (not constantly, to avoid spamming signals)
void PythonContext::outputTick()
{
QMutexLocker lock(&outputMutex);
if(!outstr.isEmpty())
{
emit textOutput(false, outstr);
}
if(!errstr.isEmpty())
{
emit textOutput(true, errstr);
}
outstr.clear();
errstr.clear();
}
void PythonContext::addText(bool isStdError, const QString &output)
{
QMutexLocker lock(&outputMutex);
if(isStdError)
errstr += output;
else
outstr += output;
}
void PythonContext::setPyGlobal(const char *varName, PyObject *obj)
{
if(!initialised())
{
emit exception(
lit("SystemError"),
tr("Python integration failed to initialise, see diagnostic log for more information."), -1,
{});
return;
}
int ret = -1;
PyGILState_STATE gil = PyGILState_Ensure();
if(obj)
ret = PyDict_SetItemString(context_namespace, varName, obj);
PyGILState_Release(gil);
if(ret == 0)
return;
emit exception(lit("RuntimeError"),
tr("Failed to set variable '%1'").arg(QString::fromUtf8(varName)), -1, {});
}
void PythonContext::outstream_del(PyObject *self)
{
OutputRedirector *redirector = (OutputRedirector *)self;
if(redirector)
{
PythonContext *context = redirector->context;
// delete the context on the UI thread.
GUIInvoke::call(context, [context]() { delete context; });
}
}
PyObject *PythonContext::outstream_write(PyObject *self, PyObject *args)
{
const char *text = NULL;
if(!PyArg_ParseTuple(args, "z:write", &text))
return NULL;
if(PyErr_Occurred())
return NULL;
OutputRedirector *redirector = (OutputRedirector *)self;
if(redirector)
{
PythonContext *context = redirector->context;
// 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)
{
_frame *frame = PyEval_GetFrame();
while(frame)
{
PyObject *globals = frame->f_globals;
if(globals)
{
OutputRedirector *global =
(OutputRedirector *)PyDict_GetItemString(globals, "_renderdoc_internal");
if(global)
context = global->context;
}
if(context)
break;
frame = frame->f_back;
}
}
if(context)
{
context->addText(redirector->isStdError ? true : false, QString::fromUtf8(text));
}
else
{
// if context is still NULL we're running in the extension context
rdcstr message = text;
_frame *frame = PyEval_GetFrame();
while(message.back() == '\n' || message.back() == '\r')
message.pop_back();
QString filename = lit("unknown");
int line = 0;
if(frame)
{
filename = ToQStr(frame->f_code->co_filename);
line = PyFrame_GetLineNumber(frame);
}
if(!message.empty())
RENDERDOC_LogMessage(redirector->isStdError ? LogType::Error : LogType::Comment, "EXTN",
filename, line, message);
}
}
Py_RETURN_NONE;
}
PyObject *PythonContext::outstream_flush(PyObject *self, PyObject *args)
{
if(PyErr_Occurred())
return NULL;
Py_RETURN_NONE;
}
int PythonContext::traceEvent(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg)
{
PyObject *thisobj = PyDict_GetItemString(obj, "thisobj");
uint64_t thisuint64 = PyLong_AsUnsignedLongLong(thisobj);
uintptr_t thisint = (uintptr_t)thisuint64;
PythonContext *context = (PythonContext *)thisint;
PyObject *compiled = PyDict_GetItemString(obj, "compiled");
if(compiled == (PyObject *)frame->f_code && what == PyTrace_LINE)
{
context->location.line = PyFrame_GetLineNumber(frame);
emit context->traceLine(context->location.file, context->location.line);
}
if(context->shouldAbort())
{
PyErr_SetString(PyExc_SystemExit, "Execution aborted.");
return -1;
}
return 0;
}
extern "C" PyThreadState *GetExecutingThreadState(PyObject *global_handle)
{
OutputRedirector *redirector = (OutputRedirector *)global_handle;
if(redirector->context)
return redirector->context->GetExecutingThreadState();
return NULL;
}
extern "C" PyObject *GetCurrentGlobalHandle()
{
return current_global_handle;
}
extern "C" void HandleException(PyObject *global_handle)
{
QString typeStr;
QString valueStr;
int finalLine = -1;
QList<QString> frames;
FetchException(typeStr, valueStr, finalLine, frames);
OutputRedirector *redirector = (OutputRedirector *)global_handle;
if(redirector && redirector->context)
{
emit redirector->context->exception(typeStr, valueStr, finalLine, frames);
}
else if(redirector && !redirector->context)
{
// if still NULL we're running in the extension context
rdcstr exString;
if(!frames.isEmpty())
{
exString += "Traceback (most recent call last):\n";
for(const QString &f : frames)
{
exString += " ";
exString += f;
exString += "\n";
}
}
exString += typeStr;
exString += ": ";
exString += valueStr;
exString += "\n";
_frame *frame = PyEval_GetFrame();
QString filename = lit("unknown");
int linenum = 0;
if(frame)
{
filename = ToQStr(frame->f_code->co_filename);
linenum = PyFrame_GetLineNumber(frame);
}
RENDERDOC_LogMessage(LogType::Error, "EXTN", filename, linenum, exString);
}
}
extern "C" bool IsThreadBlocking(PyObject *global_handle)
{
OutputRedirector *redirector = (OutputRedirector *)global_handle;
if(redirector)
return redirector->block;
return false;
}
extern "C" void SetThreadBlocking(PyObject *global_handle, bool block)
{
OutputRedirector *redirector = (OutputRedirector *)global_handle;
if(redirector)
redirector->block = block;
}
extern "C" void QueueDecRef(PyObject *obj)
{
QMutexLocker lock(&decrefQueueMutex);
decrefQueue.push_back(obj);
}
extern "C" void ProcessDecRefQueue()
{
QMutexLocker lock(&decrefQueueMutex);
if(decrefQueue.isEmpty())
return;
for(PyObject *obj : decrefQueue)
Py_XDECREF(obj);
decrefQueue.clear();
}
extern "C" QWidget *QWidgetFromPy(PyObject *widget)
{
return PythonContext::QWidgetFromPy(widget);
}
extern "C" PyObject *QWidgetToPy(QWidget *widget)
{
return PythonContext::QWidgetToPy(widget);
}