diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
commit | c04dcc2e7d834218ef2d4194331e383402495ae1 (patch) | |
tree | 7333e38d10d75386e60f336b80c2443c1166031d /xbmc/interfaces/python | |
parent | Initial commit. (diff) | |
download | kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip |
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
36 files changed, 4580 insertions, 0 deletions
diff --git a/xbmc/interfaces/python/AddonPythonInvoker.cpp b/xbmc/interfaces/python/AddonPythonInvoker.cpp new file mode 100644 index 0000000..b6158a8 --- /dev/null +++ b/xbmc/interfaces/python/AddonPythonInvoker.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +// python.h should always be included first before any other includes +#include "AddonPythonInvoker.h" + +#include <utility> + +#include <Python.h> +#include <osdefs.h> + +#define MODULE "xbmc" + +#define RUNSCRIPT_PREAMBLE \ + "" \ + "import " MODULE "\n" \ + "class xbmcout:\n" \ + " def __init__(self, loglevel=" MODULE ".LOGDEBUG):\n" \ + " self.ll=loglevel\n" \ + " def write(self, data):\n" \ + " " MODULE ".log(data,self.ll)\n" \ + " def close(self):\n" \ + " " MODULE ".log('.')\n" \ + " def flush(self):\n" \ + " " MODULE ".log('.')\n" \ + "import sys\n" \ + "sys.stdout = xbmcout()\n" \ + "sys.stderr = xbmcout(" MODULE ".LOGERROR)\n" \ + "" + +#define RUNSCRIPT_SETUPTOOLS_HACK \ + "" \ + "import types,sys\n" \ + "pkg_resources_code = \\\n" \ + "\"\"\"\n" \ + "def resource_filename(__name__,__path__):\n" \ + " return __path__\n" \ + "\"\"\"\n" \ + "pkg_resources = types.ModuleType('pkg_resources')\n" \ + "exec(pkg_resources_code, pkg_resources.__dict__)\n" \ + "sys.modules['pkg_resources'] = pkg_resources\n" \ + "" + +#define RUNSCRIPT_SETUP_ENVIROMENT_VARIABLES \ + "" \ + "from os import environ\n" \ + "environ['SSL_CERT_FILE'] = 'system/certs/cacert.pem'\n" \ + "" + +#define RUNSCRIPT_POSTSCRIPT \ + "print('-->Python Interpreter Initialized<--')\n" \ + "" + +#if defined(TARGET_ANDROID) + +#define RUNSCRIPT_COMPLIANT \ + RUNSCRIPT_PREAMBLE RUNSCRIPT_SETUPTOOLS_HACK RUNSCRIPT_POSTSCRIPT + +#elif defined(TARGET_WINDOWS_STORE) + +#define RUNSCRIPT_COMPLIANT \ + RUNSCRIPT_PREAMBLE RUNSCRIPT_SETUP_ENVIROMENT_VARIABLES RUNSCRIPT_POSTSCRIPT + +#else + +#define RUNSCRIPT_COMPLIANT \ + RUNSCRIPT_PREAMBLE RUNSCRIPT_POSTSCRIPT + +#endif + +namespace PythonBindings { +PyObject* PyInit_Module_xbmcdrm(void); +PyObject* PyInit_Module_xbmcgui(void); +PyObject* PyInit_Module_xbmc(void); +PyObject* PyInit_Module_xbmcplugin(void); +PyObject* PyInit_Module_xbmcaddon(void); +PyObject* PyInit_Module_xbmcvfs(void); +} + +using namespace PythonBindings; + +typedef struct +{ + const char *name; + CPythonInvoker::PythonModuleInitialization initialization; +} PythonModule; + +static PythonModule PythonModules[] = + { + { "xbmcdrm", PyInit_Module_xbmcdrm }, + { "xbmcgui", PyInit_Module_xbmcgui }, + { "xbmc", PyInit_Module_xbmc }, + { "xbmcplugin", PyInit_Module_xbmcplugin }, + { "xbmcaddon", PyInit_Module_xbmcaddon }, + { "xbmcvfs", PyInit_Module_xbmcvfs } + }; + +CAddonPythonInvoker::CAddonPythonInvoker(ILanguageInvocationHandler *invocationHandler) + : CPythonInvoker(invocationHandler) +{ + PyImport_AppendInittab("xbmcdrm", PyInit_Module_xbmcdrm); + PyImport_AppendInittab("xbmcgui", PyInit_Module_xbmcgui); + PyImport_AppendInittab("xbmc", PyInit_Module_xbmc); + PyImport_AppendInittab("xbmcplugin", PyInit_Module_xbmcplugin); + PyImport_AppendInittab("xbmcaddon", PyInit_Module_xbmcaddon); + PyImport_AppendInittab("xbmcvfs", PyInit_Module_xbmcvfs); +} + +CAddonPythonInvoker::~CAddonPythonInvoker() = default; + +std::map<std::string, CPythonInvoker::PythonModuleInitialization> CAddonPythonInvoker::getModules() const +{ + static std::map<std::string, PythonModuleInitialization> modules; + if (modules.empty()) + { + for (const PythonModule& pythonModule : PythonModules) + modules.insert(std::make_pair(pythonModule.name, pythonModule.initialization)); + } + + return modules; +} + +const char* CAddonPythonInvoker::getInitializationScript() const +{ + return RUNSCRIPT_COMPLIANT; +} diff --git a/xbmc/interfaces/python/AddonPythonInvoker.h b/xbmc/interfaces/python/AddonPythonInvoker.h new file mode 100644 index 0000000..a846071 --- /dev/null +++ b/xbmc/interfaces/python/AddonPythonInvoker.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/python/PythonInvoker.h" + +class CAddonPythonInvoker : public CPythonInvoker +{ +public: + explicit CAddonPythonInvoker(ILanguageInvocationHandler *invocationHandler); + ~CAddonPythonInvoker() override; + +protected: + // overrides of CPythonInvoker + std::map<std::string, PythonModuleInitialization> getModules() const override; + const char* getInitializationScript() const override; +}; diff --git a/xbmc/interfaces/python/CMakeLists.txt b/xbmc/interfaces/python/CMakeLists.txt new file mode 100644 index 0000000..061cc2b --- /dev/null +++ b/xbmc/interfaces/python/CMakeLists.txt @@ -0,0 +1,21 @@ +set(SOURCES AddonPythonInvoker.cpp + CallbackHandler.cpp + ContextItemAddonInvoker.cpp + LanguageHook.cpp + PythonInvoker.cpp + XBPython.cpp + swig.cpp + PyContext.cpp) + +set(HEADERS AddonPythonInvoker.h + CallbackHandler.h + ContextItemAddonInvoker.h + LanguageHook.h + preamble.h + PyContext.h + PythonInvoker.h + pythreadstate.h + swig.h + XBPython.h) + +core_add_library(python_interface) diff --git a/xbmc/interfaces/python/CallbackHandler.cpp b/xbmc/interfaces/python/CallbackHandler.cpp new file mode 100644 index 0000000..8fd31a7 --- /dev/null +++ b/xbmc/interfaces/python/CallbackHandler.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CallbackHandler.h" + +#include "LanguageHook.h" + +namespace XBMCAddon +{ + namespace Python + { + /** + * We are ASS-U-MEing that this construction is happening + * within the context of a Python call. This way we can + * store off the PyThreadState to later verify that we're + * handling callbacks in the appropriate thread. + */ + PythonCallbackHandler::PythonCallbackHandler() + { + XBMC_TRACE; + objectThreadState = PyThreadState_Get(); + } + + /** + * Now we are answering the question as to whether or not we are in the + * PyThreadState that we were in when we started. + */ + bool PythonCallbackHandler::isStateOk(AddonClass* obj) + { + XBMC_TRACE; + PyThreadState* state = PyThreadState_Get(); + if (objectThreadState == state) + { + // make sure the interpreter is still active. + AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook> lh(XBMCAddon::Python::PythonLanguageHook::GetIfExists(state->interp)); + if (lh.isNotNull() && lh->HasRegisteredAddonClassInstance(obj) && lh.get() == obj->GetLanguageHook()) + return true; + } + return false; + } + + /** + * For this method we expect the PyThreadState to be passed as the user + * data for the check. + * + * @todo This is a stupid way to get this information back to the handler. + * there should be a more language neutral means. + */ + bool PythonCallbackHandler::shouldRemoveCallback(AddonClass* obj, void* threadState) + { + XBMC_TRACE; + if (threadState == objectThreadState) + return true; + + // we also want to remove the callback if the language hook no longer exists. + // this is a belt-and-suspenders cleanup mechanism + return ! XBMCAddon::Python::PythonLanguageHook::IsAddonClassInstanceRegistered(obj); + } + } +} diff --git a/xbmc/interfaces/python/CallbackHandler.h b/xbmc/interfaces/python/CallbackHandler.h new file mode 100644 index 0000000..b128b27 --- /dev/null +++ b/xbmc/interfaces/python/CallbackHandler.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/legacy/CallbackHandler.h" + +#include <Python.h> + +namespace XBMCAddon +{ + namespace Python + { + /** + * This class represents a specialization of the callback handler + * that specifically checks to see if we're in an OK thread state + * based on Python specifics. + */ + class PythonCallbackHandler : public RetardedAsyncCallbackHandler + { + PyThreadState* objectThreadState; + public: + + /** + * We are ASS-U-MEing that this construction is happening + * within the context of a Python call. This way we can + * store off the PyThreadState to later verify that we're + * handling callbacks in the appropriate thread. + */ + PythonCallbackHandler(); + bool isStateOk(AddonClass* obj) override; + bool shouldRemoveCallback(AddonClass* obj, void* threadState) override; + }; + } +} diff --git a/xbmc/interfaces/python/ContextItemAddonInvoker.cpp b/xbmc/interfaces/python/ContextItemAddonInvoker.cpp new file mode 100644 index 0000000..734193b --- /dev/null +++ b/xbmc/interfaces/python/ContextItemAddonInvoker.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +// python.h should always be included first before any other includes +#include "ContextItemAddonInvoker.h" + +#include "interfaces/python/swig.h" +#include "utils/log.h" + +#include <Python.h> +#include <osdefs.h> + + +CContextItemAddonInvoker::CContextItemAddonInvoker( + ILanguageInvocationHandler *invocationHandler, + const CFileItemPtr& item) + : CAddonPythonInvoker(invocationHandler), m_item(CFileItemPtr(new CFileItem(*item.get()))) +{ +} + +CContextItemAddonInvoker::~CContextItemAddonInvoker() = default; + +void CContextItemAddonInvoker::onPythonModuleInitialization(void* moduleDict) +{ + CAddonPythonInvoker::onPythonModuleInitialization(moduleDict); + if (m_item) + { + XBMCAddon::xbmcgui::ListItem* arg = new XBMCAddon::xbmcgui::ListItem(m_item); + PyObject* pyItem = PythonBindings::makePythonInstance(arg, true); + if (pyItem == Py_None || PySys_SetObject("listitem", pyItem) == -1) + { + CLog::Log(LOGERROR, "CPythonInvoker({}, {}): Failed to set sys parameter", GetId(), + m_sourceFile); + //FIXME: we should really abort execution + } + } +} diff --git a/xbmc/interfaces/python/ContextItemAddonInvoker.h b/xbmc/interfaces/python/ContextItemAddonInvoker.h new file mode 100644 index 0000000..b2f4d03 --- /dev/null +++ b/xbmc/interfaces/python/ContextItemAddonInvoker.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/python/AddonPythonInvoker.h" + +#include <memory> + +class CFileItem; +typedef std::shared_ptr<CFileItem> CFileItemPtr; + +class CContextItemAddonInvoker : public CAddonPythonInvoker +{ +public: + explicit CContextItemAddonInvoker(ILanguageInvocationHandler *invocationHandler, + const CFileItemPtr& item); + ~CContextItemAddonInvoker() override; + +protected: + void onPythonModuleInitialization(void* moduleDict) override; + +private: + const CFileItemPtr m_item; +}; diff --git a/xbmc/interfaces/python/LanguageHook.cpp b/xbmc/interfaces/python/LanguageHook.cpp new file mode 100644 index 0000000..0d4747f --- /dev/null +++ b/xbmc/interfaces/python/LanguageHook.cpp @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + + +#include "LanguageHook.h" + +#include "CallbackHandler.h" +#include "PyContext.h" +#include "ServiceBroker.h" +#include "XBPython.h" +#include "interfaces/legacy/AddonUtils.h" +#include "utils/log.h" + +#include <mutex> + +namespace XBMCAddon +{ + namespace Python + { + static AddonClass::Ref<PythonLanguageHook> instance; + + static CCriticalSection hooksMutex; + static std::map<PyInterpreterState*,AddonClass::Ref<PythonLanguageHook> > hooks; + + // vtab instantiation + PythonLanguageHook::~PythonLanguageHook() + { + XBMC_TRACE; + XBMCAddon::LanguageHook::deallocating(); + } + + void PythonLanguageHook::MakePendingCalls() + { + XBMC_TRACE; + PythonCallbackHandler::makePendingCalls(); + } + + void PythonLanguageHook::DelayedCallOpen() + { + XBMC_TRACE; + PyGILLock::releaseGil(); + } + + void PythonLanguageHook::DelayedCallClose() + { + XBMC_TRACE; + PyGILLock::acquireGil(); + } + + void PythonLanguageHook::RegisterMe() + { + XBMC_TRACE; + std::unique_lock<CCriticalSection> lock(hooksMutex); + hooks[m_interp] = AddonClass::Ref<PythonLanguageHook>(this); + } + + void PythonLanguageHook::UnregisterMe() + { + XBMC_TRACE; + std::unique_lock<CCriticalSection> lock(hooksMutex); + hooks.erase(m_interp); + } + + static AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook> g_languageHook; + + // Ok ... we're going to get it even if it doesn't exist. If it doesn't exist then + // we're going to assume we're not in control of the interpreter. This (apparently) + // can be the case. E.g. Libspotify manages to call into a script using a ctypes + // extension but under the control of an Interpreter we know nothing about. In + // cases like this we're going to use a global interpreter + AddonClass::Ref<PythonLanguageHook> PythonLanguageHook::GetIfExists(PyInterpreterState* interp) + { + XBMC_TRACE; + std::unique_lock<CCriticalSection> lock(hooksMutex); + std::map<PyInterpreterState*,AddonClass::Ref<PythonLanguageHook> >::iterator iter = hooks.find(interp); + if (iter != hooks.end()) + return iter->second; + + // if we got here then we need to use the global one. + if (g_languageHook.isNull()) + g_languageHook = new XBMCAddon::Python::PythonLanguageHook(); + + return g_languageHook; + } + + bool PythonLanguageHook::IsAddonClassInstanceRegistered(AddonClass* obj) + { + for (const auto& iter : hooks) + { + if (iter.second->HasRegisteredAddonClassInstance(obj)) + return true; + } + return false; + } + + /** + * PythonCallbackHandler expects to be instantiated PER AddonClass instance + * that is to be used as a callback. This is why this cannot be instantiated + * once. + * + * There is an expectation that this method is called from the Python thread + * that instantiated an AddonClass that has the potential for a callback. + * + * See RetardedAsyncCallbackHandler for more details. + * See PythonCallbackHandler for more details + * See PythonCallbackHandler::PythonCallbackHandler for more details + */ + XBMCAddon::CallbackHandler* PythonLanguageHook::GetCallbackHandler() + { + XBMC_TRACE; + return new PythonCallbackHandler(); + } + + String PythonLanguageHook::GetAddonId() + { + XBMC_TRACE; + + // Get a reference to the main module + // and global dictionary + PyObject* main_module = PyImport_AddModule("__main__"); + if (!main_module) + { + CLog::Log(LOGDEBUG, "PythonLanguageHook::{}: __main__ returns null", __FUNCTION__); + return ""; + } + PyObject* global_dict = PyModule_GetDict(main_module); + // Extract a reference to the function "func_name" + // from the global dictionary + PyObject* pyid = PyDict_GetItemString(global_dict, "__xbmcaddonid__"); + if (pyid) + return PyUnicode_AsUTF8(pyid); + return ""; + } + + String PythonLanguageHook::GetAddonVersion() + { + XBMC_TRACE; + // Get a reference to the main module + // and global dictionary + PyObject* main_module = PyImport_AddModule("__main__"); + if (!main_module) + { + CLog::Log(LOGDEBUG, "PythonLanguageHook::{}: __main__ returns null", __FUNCTION__); + return ""; + } + PyObject* global_dict = PyModule_GetDict(main_module); + // Extract a reference to the function "func_name" + // from the global dictionary + PyObject* pyversion = PyDict_GetItemString(global_dict, "__xbmcapiversion__"); + if (pyversion) + return PyUnicode_AsUTF8(pyversion); + return ""; + } + + long PythonLanguageHook::GetInvokerId() + { + XBMC_TRACE; + + // Get a reference to the main module + // and global dictionary + PyObject* main_module = PyImport_AddModule("__main__"); + if (!main_module) + { + CLog::Log(LOGDEBUG, "PythonLanguageHook::{}: __main__ returns null", __FUNCTION__); + return -1; + } + PyObject* global_dict = PyModule_GetDict(main_module); + // Extract a reference to the function "func_name" + // from the global dictionary + PyObject* pyid = PyDict_GetItemString(global_dict, "__xbmcinvokerid__"); + if (pyid) + return PyLong_AsLong(pyid); + return -1; + } + + void PythonLanguageHook::RegisterPlayerCallback(IPlayerCallback* player) + { + XBMC_TRACE; + CServiceBroker::GetXBPython().RegisterPythonPlayerCallBack(player); + } + void PythonLanguageHook::UnregisterPlayerCallback(IPlayerCallback* player) + { + XBMC_TRACE; + CServiceBroker::GetXBPython().UnregisterPythonPlayerCallBack(player); + } + void PythonLanguageHook::RegisterMonitorCallback(XBMCAddon::xbmc::Monitor* monitor) + { + XBMC_TRACE; + CServiceBroker::GetXBPython().RegisterPythonMonitorCallBack(monitor); + } + void PythonLanguageHook::UnregisterMonitorCallback(XBMCAddon::xbmc::Monitor* monitor) + { + XBMC_TRACE; + CServiceBroker::GetXBPython().UnregisterPythonMonitorCallBack(monitor); + } + + bool PythonLanguageHook::WaitForEvent(CEvent& hEvent, unsigned int milliseconds) + { + XBMC_TRACE; + return CServiceBroker::GetXBPython().WaitForEvent(hEvent, milliseconds); + } + + void PythonLanguageHook::RegisterAddonClassInstance(AddonClass* obj) + { + XBMC_TRACE; + std::unique_lock<CCriticalSection> l(*this); + obj->Acquire(); + currentObjects.insert(obj); + } + + void PythonLanguageHook::UnregisterAddonClassInstance(AddonClass* obj) + { + XBMC_TRACE; + std::unique_lock<CCriticalSection> l(*this); + if (currentObjects.erase(obj) > 0) + obj->Release(); + } + + bool PythonLanguageHook::HasRegisteredAddonClassInstance(AddonClass* obj) + { + XBMC_TRACE; + std::unique_lock<CCriticalSection> l(*this); + return currentObjects.find(obj) != currentObjects.end(); + } + } +} diff --git a/xbmc/interfaces/python/LanguageHook.h b/xbmc/interfaces/python/LanguageHook.h new file mode 100644 index 0000000..6a4e0d0 --- /dev/null +++ b/xbmc/interfaces/python/LanguageHook.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/legacy/LanguageHook.h" +#include "threads/Event.h" + +#include <map> +#include <mutex> +#include <set> + +#include <Python.h> + +namespace XBMCAddon +{ + namespace Python + { + struct MutableInteger; + + /** + * This class supplies the python specific functionality for + * plugging into the API. It's got a static only implementation + * and uses the singleton pattern for access. + */ + class PythonLanguageHook : public XBMCAddon::LanguageHook + { + PyInterpreterState* m_interp; + CCriticalSection crit; + std::set<AddonClass*> currentObjects; + + // This constructor is only used to instantiate the global LanguageHook + inline PythonLanguageHook() : m_interp(NULL) { } + + public: + + inline explicit PythonLanguageHook(PyInterpreterState* interp) : m_interp(interp) { } + ~PythonLanguageHook() override; + + void DelayedCallOpen() override; + void DelayedCallClose() override; + void MakePendingCalls() override; + + /** + * PythonCallbackHandler expects to be instantiated PER AddonClass instance + * that is to be used as a callback. This is why this cannot be instantiated + * once. + * + * There is an expectation that this method is called from the Python thread + * that instantiated an AddonClass that has the potential for a callback. + * + * See RetardedAsyncCallbackHandler for more details. + * See PythonCallbackHandler for more details + * See PythonCallbackHandler::PythonCallbackHandler for more details + */ + XBMCAddon::CallbackHandler* GetCallbackHandler() override; + + String GetAddonId() override; + String GetAddonVersion() override; + long GetInvokerId() override; + + void RegisterPlayerCallback(IPlayerCallback* player) override; + void UnregisterPlayerCallback(IPlayerCallback* player) override; + void RegisterMonitorCallback(XBMCAddon::xbmc::Monitor* monitor) override; + void UnregisterMonitorCallback(XBMCAddon::xbmc::Monitor* monitor) override; + bool WaitForEvent(CEvent& hEvent, unsigned int milliseconds) override; + + static AddonClass::Ref<PythonLanguageHook> GetIfExists(PyInterpreterState* interp); + static bool IsAddonClassInstanceRegistered(AddonClass* obj); + + void RegisterAddonClassInstance(AddonClass* obj); + void UnregisterAddonClassInstance(AddonClass* obj); + bool HasRegisteredAddonClassInstance(AddonClass* obj); + inline bool HasRegisteredAddonClasses() + { + std::unique_lock<CCriticalSection> l(*this); + return !currentObjects.empty(); + } + + // You should hold the lock on the LanguageHook itself if you're + // going to do anything with the set that gets returned. + inline std::set<AddonClass*>& GetRegisteredAddonClasses() { return currentObjects; } + + void UnregisterMe(); + void RegisterMe(); + }; + } +} + diff --git a/xbmc/interfaces/python/MethodType.groovy b/xbmc/interfaces/python/MethodType.groovy new file mode 100644 index 0000000..18597fd --- /dev/null +++ b/xbmc/interfaces/python/MethodType.groovy @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +public enum MethodType +{ + constructor, destructor, method +} + + diff --git a/xbmc/interfaces/python/PyContext.cpp b/xbmc/interfaces/python/PyContext.cpp new file mode 100644 index 0000000..3b64ac6 --- /dev/null +++ b/xbmc/interfaces/python/PyContext.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PyContext.h" + +#include "utils/log.h" + +#include <Python.h> + +namespace XBMCAddon +{ + namespace Python + { + struct PyContextState + { + inline explicit PyContextState(bool pcreatedByGilRelease = false) : + state(NULL), createdByGilRelease(pcreatedByGilRelease) {} + + int value = 0; + PyThreadState* state; + int gilReleasedDepth = 0; + bool createdByGilRelease; + }; + + static thread_local PyContextState* tlsPyContextState; + + void* PyContext::enterContext() + { + PyContextState* cur = tlsPyContextState; + if (cur == NULL) + { + cur = new PyContextState(); + tlsPyContextState = cur; + } + + // increment the count + cur->value++; + + return cur; + } + + void PyContext::leaveContext() + { + // here we ASSUME that the constructor was called. + PyContextState* cur = tlsPyContextState; + cur->value--; + int curlevel = cur->value; + + // this is a hack but ... + if (curlevel < 0) + { + CLog::Log(LOGERROR, "FATAL: PyContext closed more than opened"); + curlevel = cur->value = 0; + } + + if (curlevel == 0) + { + // clear the tlsPyContextState + tlsPyContextState = NULL; + delete cur; + } + } + + void PyGILLock::releaseGil() + { + PyContextState* cur = tlsPyContextState; + + // This means we're not within the python context, but + // because we may be in a thread spawned by python itself, + // we need to handle this. + if (!cur) + { + cur = static_cast<PyContextState*>(PyContext::enterContext()); + cur->createdByGilRelease = true; + } + + if (cur->gilReleasedDepth == 0) // true if we are at the outermost + { + PyThreadState* _save; + // this macro sets _save + { + Py_UNBLOCK_THREADS + } + cur->state = _save; + } + cur->gilReleasedDepth++; // the first time this goes to 1 + } + + void PyGILLock::acquireGil() + { + PyContextState* cur = tlsPyContextState; + + // it's not possible for cur to be NULL (and if it is, we want to fail anyway). + + // decrement the depth and make sure we're in the right place. + cur->gilReleasedDepth--; + if (cur->gilReleasedDepth == 0) // are we back to zero? + { + PyThreadState* _save = cur->state; + // This macros uses _save + { + Py_BLOCK_THREADS + } + cur->state = NULL; // clear the state to indicate we've reacquired the gil + + // we clear it only if we created it on this level. + if (cur->createdByGilRelease) + PyContext::leaveContext(); + } + } + } +} diff --git a/xbmc/interfaces/python/PyContext.h b/xbmc/interfaces/python/PyContext.h new file mode 100644 index 0000000..216a45f --- /dev/null +++ b/xbmc/interfaces/python/PyContext.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +namespace XBMCAddon +{ + namespace Python + { + class PyGILLock; + + /** + * These classes should NOT be used with 'new'. They are expected to reside + * as stack instances and they act as "Guard" classes that track the + * current context. + */ + class PyContext + { + protected: + friend class PyGILLock; + static void* enterContext(); + static void leaveContext(); + public: + + inline PyContext() { enterContext(); } + inline ~PyContext() { leaveContext(); } + }; + + /** + * This class supports recursive locking of the GIL. It assumes that + * all Python GIL manipulation is done through this class so that it + * can monitor the current owner. + */ + class PyGILLock + { + public: + static void releaseGil(); + static void acquireGil(); + + inline PyGILLock() { releaseGil(); } + inline ~PyGILLock() { acquireGil(); } + }; + } +} diff --git a/xbmc/interfaces/python/PythonInvoker.cpp b/xbmc/interfaces/python/PythonInvoker.cpp new file mode 100644 index 0000000..1e9d344 --- /dev/null +++ b/xbmc/interfaces/python/PythonInvoker.cpp @@ -0,0 +1,724 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +// clang-format off +// python.h should always be included first before any other includes +#include <mutex> +#include <Python.h> +// clang-format on + +#include "PythonInvoker.h" + +#include "ServiceBroker.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonInfo.h" +#include "addons/addoninfo/AddonType.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "filesystem/SpecialProtocol.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "interfaces/python/PyContext.h" +#include "interfaces/python/pythreadstate.h" +#include "interfaces/python/swig.h" +#include "messaging/ApplicationMessenger.h" +#include "threads/SingleLock.h" +#include "threads/SystemClock.h" +#include "utils/CharsetConverter.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +// clang-format off +// This breaks fmt because of SEP define, don't include +// before anything that includes logging +#include <osdefs.h> +// clang-format on + +#include <cassert> +#include <iterator> + +#ifdef TARGET_WINDOWS +extern "C" FILE* fopen_utf8(const char* _Filename, const char* _Mode); +#else +#define fopen_utf8 fopen +#endif + +#define GC_SCRIPT \ + "import gc\n" \ + "gc.collect(2)\n" + +#define PY_PATH_SEP DELIM + +// Time before ill-behaved scripts are terminated +#define PYTHON_SCRIPT_TIMEOUT 5000ms // ms + +using namespace XFILE; +using namespace std::chrono_literals; + +#define PythonModulesSize sizeof(PythonModules) / sizeof(PythonModule) + +CCriticalSection CPythonInvoker::s_critical; + +static const std::string getListOfAddonClassesAsString( + XBMCAddon::AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook>& languageHook) +{ + std::string message; + std::unique_lock<CCriticalSection> l(*(languageHook.get())); + const std::set<XBMCAddon::AddonClass*>& acs = languageHook->GetRegisteredAddonClasses(); + bool firstTime = true; + for (const auto& iter : acs) + { + if (!firstTime) + message += ","; + else + firstTime = false; + message += iter->GetClassname(); + } + + return message; +} + +CPythonInvoker::CPythonInvoker(ILanguageInvocationHandler* invocationHandler) + : ILanguageInvoker(invocationHandler), m_threadState(NULL), m_stop(false) +{ +} + +CPythonInvoker::~CPythonInvoker() +{ + // nothing to do for the default invoker used for registration with the + // CScriptInvocationManager + if (GetId() < 0) + return; + + if (GetState() < InvokerStateExecutionDone) + CLog::Log(LOGDEBUG, "CPythonInvoker({}): waiting for python thread \"{}\" to stop", GetId(), + (!m_sourceFile.empty() ? m_sourceFile : "unknown script")); + Stop(true); + pulseGlobalEvent(); + + onExecutionFinalized(); +} + +bool CPythonInvoker::Execute( + const std::string& script, + const std::vector<std::string>& arguments /* = std::vector<std::string>() */) +{ + if (script.empty()) + return false; + + if (!CFileUtils::Exists(script)) + { + CLog::Log(LOGERROR, "CPythonInvoker({}): python script \"{}\" does not exist", GetId(), + CSpecialProtocol::TranslatePath(script)); + return false; + } + + if (!onExecutionInitialized()) + return false; + + return ILanguageInvoker::Execute(script, arguments); +} + +bool CPythonInvoker::execute(const std::string& script, const std::vector<std::string>& arguments) +{ + std::vector<std::wstring> w_arguments; + for (const auto& argument : arguments) + { + std::wstring w_argument; + g_charsetConverter.utf8ToW(argument, w_argument); + w_arguments.push_back(w_argument); + } + return execute(script, w_arguments); +} + +bool CPythonInvoker::execute(const std::string& script, std::vector<std::wstring>& arguments) +{ + // copy the code/script into a local string buffer + m_sourceFile = script; + std::set<std::string> pythonPath; + + CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): start processing", GetId(), m_sourceFile); + + std::string realFilename(CSpecialProtocol::TranslatePath(m_sourceFile)); + std::string scriptDir = URIUtils::GetDirectory(realFilename); + URIUtils::RemoveSlashAtEnd(scriptDir); + + // set m_threadState if it's not set. + PyThreadState* l_threadState = nullptr; + bool newInterp = false; + { + if (!m_threadState) + { +#if PY_VERSION_HEX < 0x03070000 + // this is a TOTAL hack. We need the GIL but we need to borrow a PyThreadState in order to get it + // as of Python 3.2 since PyEval_AcquireLock is deprecated + extern PyThreadState* savestate; + PyEval_RestoreThread(savestate); +#else + PyThreadState* ts = PyInterpreterState_ThreadHead(PyInterpreterState_Main()); + PyEval_RestoreThread(ts); +#endif + l_threadState = Py_NewInterpreter(); + PyEval_ReleaseThread(l_threadState); + if (l_threadState == NULL) + { + CLog::Log(LOGERROR, "CPythonInvoker({}, {}): FAILED to get thread m_threadState!", GetId(), + m_sourceFile); + return false; + } + newInterp = true; + } + else + l_threadState = m_threadState; + } + + // get the GIL + PyEval_RestoreThread(l_threadState); + if (newInterp) + { + m_languageHook = new XBMCAddon::Python::PythonLanguageHook(l_threadState->interp); + m_languageHook->RegisterMe(); + + onInitialization(); + setState(InvokerStateInitialized); + + if (realFilename == m_sourceFile) + CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): the source file to load is \"{}\"", GetId(), + m_sourceFile, m_sourceFile); + else + CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): the source file to load is \"{}\" (\"{}\")", + GetId(), m_sourceFile, m_sourceFile, realFilename); + + // get path from script file name and add python path's + // this is used for python so it will search modules from script path first + pythonPath.emplace(scriptDir); + + // add all addon module dependencies to path + if (m_addon) + { + std::set<std::string> paths; + getAddonModuleDeps(m_addon, paths); + for (const auto& it : paths) + pythonPath.emplace(it); + } + else + { // for backwards compatibility. + // we don't have any addon so just add all addon modules installed + CLog::Log( + LOGWARNING, + "CPythonInvoker({}): Script invoked without an addon. Adding all addon " + "modules installed to python path as fallback. This behaviour will be removed in future " + "version.", + GetId()); + ADDON::VECADDONS addons; + CServiceBroker::GetAddonMgr().GetAddons(addons, ADDON::AddonType::SCRIPT_MODULE); + for (unsigned int i = 0; i < addons.size(); ++i) + pythonPath.emplace(CSpecialProtocol::TranslatePath(addons[i]->LibPath())); + } + + PyObject* sysPath = PySys_GetObject("path"); + + std::for_each(pythonPath.crbegin(), pythonPath.crend(), + [&sysPath](const auto& path) + { + PyObject* pyPath = PyUnicode_FromString(path.c_str()); + PyList_Insert(sysPath, 0, pyPath); + + Py_DECREF(pyPath); + }); + + CLog::Log(LOGDEBUG, "CPythonInvoker({}): full python path:", GetId()); + + Py_ssize_t pathListSize = PyList_Size(sysPath); + + for (Py_ssize_t index = 0; index < pathListSize; index++) + { + if (index == 0 && !pythonPath.empty()) + CLog::Log(LOGDEBUG, "CPythonInvoker({}): custom python path:", GetId()); + + if (index == static_cast<ssize_t>(pythonPath.size())) + CLog::Log(LOGDEBUG, "CPythonInvoker({}): default python path:", GetId()); + + PyObject* pyPath = PyList_GetItem(sysPath, index); + CLog::Log(LOGDEBUG, "CPythonInvoker({}): {}", GetId(), PyUnicode_AsUTF8(pyPath)); + } + + { // set the m_threadState to this new interp + std::unique_lock<CCriticalSection> lockMe(m_critical); + m_threadState = l_threadState; + } + } + else + // swap in my thread m_threadState + PyThreadState_Swap(m_threadState); + + PyObject* sysArgv = PyList_New(0); + + if (arguments.empty()) + arguments.emplace_back(L""); + + CLog::Log(LOGDEBUG, "CPythonInvoker({}): adding args:", GetId()); + + for (const auto& arg : arguments) + { + PyObject* pyArg = PyUnicode_FromWideChar(arg.c_str(), arg.length()); + PyList_Append(sysArgv, pyArg); + CLog::Log(LOGDEBUG, "CPythonInvoker({}): {}", GetId(), PyUnicode_AsUTF8(pyArg)); + + Py_DECREF(pyArg); + } + + PySys_SetObject("argv", sysArgv); + + CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): entering source directory {}", GetId(), m_sourceFile, + scriptDir); + PyObject* module = PyImport_AddModule("__main__"); + PyObject* moduleDict = PyModule_GetDict(module); + + // we need to check if we was asked to abort before we had inited + bool stopping = false; + { + GilSafeSingleLock lock(m_critical); + stopping = m_stop; + } + + bool failed = false; + std::string exceptionType, exceptionValue, exceptionTraceback; + if (!stopping) + { + try + { + // run script from file + // We need to have python open the file because on Windows the DLL that python + // is linked against may not be the DLL that xbmc is linked against so + // passing a FILE* to python from an fopen has the potential to crash. + + PyObject* pyRealFilename = Py_BuildValue("s", realFilename.c_str()); + FILE* fp = _Py_fopen_obj(pyRealFilename, "rb"); + Py_DECREF(pyRealFilename); + + if (fp != NULL) + { + PyObject* f = PyUnicode_FromString(realFilename.c_str()); + PyDict_SetItemString(moduleDict, "__file__", f); + + onPythonModuleInitialization(moduleDict); + + Py_DECREF(f); + setState(InvokerStateRunning); + XBMCAddon::Python::PyContext + pycontext; // this is a guard class that marks this callstack as being in a python context + executeScript(fp, realFilename, moduleDict); + } + else + CLog::Log(LOGERROR, "CPythonInvoker({}, {}): {} not found!", GetId(), m_sourceFile, + m_sourceFile); + } + catch (const XbmcCommons::Exception& e) + { + setState(InvokerStateFailed); + e.LogThrowMessage(); + failed = true; + } + catch (...) + { + setState(InvokerStateFailed); + CLog::Log(LOGERROR, "CPythonInvoker({}, {}): failure in script", GetId(), m_sourceFile); + failed = true; + } + } + + m_systemExitThrown = false; + InvokerState stateToSet; + if (!failed && !PyErr_Occurred()) + { + CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): script successfully run", GetId(), m_sourceFile); + stateToSet = InvokerStateScriptDone; + onSuccess(); + } + else if (PyErr_ExceptionMatches(PyExc_SystemExit)) + { + m_systemExitThrown = true; + CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): script aborted", GetId(), m_sourceFile); + stateToSet = InvokerStateFailed; + onAbort(); + } + else + { + stateToSet = InvokerStateFailed; + + // if it failed with an exception we already logged the details + if (!failed) + { + PythonBindings::PythonToCppException* e = NULL; + if (PythonBindings::PythonToCppException::ParsePythonException(exceptionType, exceptionValue, + exceptionTraceback)) + e = new PythonBindings::PythonToCppException(exceptionType, exceptionValue, + exceptionTraceback); + else + e = new PythonBindings::PythonToCppException(); + + e->LogThrowMessage(); + delete e; + } + + onError(exceptionType, exceptionValue, exceptionTraceback); + } + + std::unique_lock<CCriticalSection> lock(m_critical); + // no need to do anything else because the script has already stopped + if (failed) + { + setState(stateToSet); + return true; + } + + if (m_threadState) + { + // make sure all sub threads have finished + for (PyThreadState* old = nullptr; m_threadState != nullptr;) + { + PyThreadState* s = PyInterpreterState_ThreadHead(m_threadState->interp); + for (; s && s == m_threadState;) + s = PyThreadState_Next(s); + + if (!s) + break; + + if (old != s) + { + CLog::Log(LOGINFO, "CPythonInvoker({}, {}): waiting on thread {}", GetId(), m_sourceFile, + (uint64_t)s->thread_id); + old = s; + } + + lock.unlock(); + CPyThreadState pyState; + KODI::TIME::Sleep(100ms); + pyState.Restore(); + lock.lock(); + } + } + + // pending calls must be cleared out + XBMCAddon::RetardedAsyncCallbackHandler::clearPendingCalls(m_threadState); + + assert(m_threadState != nullptr); + PyEval_ReleaseThread(m_threadState); + + setState(stateToSet); + + return true; +} + +void CPythonInvoker::executeScript(FILE* fp, const std::string& script, PyObject* moduleDict) +{ + if (fp == NULL || script.empty() || moduleDict == NULL) + return; + + int m_Py_file_input = Py_file_input; + PyRun_FileExFlags(fp, script.c_str(), m_Py_file_input, moduleDict, moduleDict, 1, NULL); +} + +FILE* CPythonInvoker::PyFile_AsFileWithMode(PyObject* py_file, const char* mode) +{ + PyObject* ret = PyObject_CallMethod(py_file, "flush", ""); + if (ret == NULL) + return NULL; + Py_DECREF(ret); + + int fd = PyObject_AsFileDescriptor(py_file); + if (fd == -1) + return NULL; + + FILE* f = fdopen(fd, mode); + if (f == NULL) + { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return f; +} + +bool CPythonInvoker::stop(bool abort) +{ + std::unique_lock<CCriticalSection> lock(m_critical); + m_stop = true; + + if (!IsRunning() && !m_threadState) + return false; + + if (m_threadState != NULL) + { + if (IsRunning()) + { + setState(InvokerStateStopping); + lock.unlock(); + + PyEval_RestoreThread((PyThreadState*)m_threadState); + + //tell xbmc.Monitor to call onAbortRequested() + if (m_addon) + { + CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): trigger Monitor abort request", GetId(), + m_sourceFile); + AbortNotification(); + } + + PyEval_ReleaseThread(m_threadState); + } + else + //Release the lock while waiting for threads to finish + lock.unlock(); + + XbmcThreads::EndTime<> timeout(PYTHON_SCRIPT_TIMEOUT); + while (!m_stoppedEvent.Wait(15ms)) + { + if (timeout.IsTimePast()) + { + CLog::Log(LOGERROR, + "CPythonInvoker({}, {}): script didn't stop in {} seconds - let's kill it", + GetId(), m_sourceFile, + std::chrono::duration_cast<std::chrono::seconds>(PYTHON_SCRIPT_TIMEOUT).count()); + break; + } + + // We can't empty-spin in the main thread and expect scripts to be able to + // dismantle themselves. Python dialogs aren't normal XBMC dialogs, they rely + // on TMSG_GUI_PYTHON_DIALOG messages, so pump the message loop. + if (CServiceBroker::GetAppMessenger()->IsProcessThread()) + { + CServiceBroker::GetAppMessenger()->ProcessMessages(); + } + } + + lock.lock(); + + setState(InvokerStateExecutionDone); + + // Useful for add-on performance metrics + if (!timeout.IsTimePast()) + CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): script termination took {}ms", GetId(), + m_sourceFile, (PYTHON_SCRIPT_TIMEOUT - timeout.GetTimeLeft()).count()); + + // Since we released the m_critical it's possible that the state is cleaned up + // so we need to recheck for m_threadState == NULL + if (m_threadState != NULL) + { + { + // grabbing the PyLock while holding the m_critical is asking for a deadlock + CSingleExit ex2(m_critical); + PyEval_RestoreThread((PyThreadState*)m_threadState); + } + + + PyThreadState* state = PyInterpreterState_ThreadHead(m_threadState->interp); + while (state) + { + // Raise a SystemExit exception in python threads + Py_XDECREF(state->async_exc); + state->async_exc = PyExc_SystemExit; + Py_XINCREF(state->async_exc); + state = PyThreadState_Next(state); + } + + // If a dialog entered its doModal(), we need to wake it to see the exception + pulseGlobalEvent(); + + PyEval_ReleaseThread(m_threadState); + } + lock.unlock(); + + setState(InvokerStateFailed); + } + + return true; +} + +// Always called from Invoker thread +void CPythonInvoker::onExecutionDone() +{ + std::unique_lock<CCriticalSection> lock(m_critical); + if (m_threadState != NULL) + { + CLog::Log(LOGDEBUG, "{}({}, {})", __FUNCTION__, GetId(), m_sourceFile); + + PyEval_RestoreThread(m_threadState); + + onDeinitialization(); + + // run the gc before finishing + // + // if the script exited by throwing a SystemExit exception then going back + // into the interpreter causes this python bug to get hit: + // http://bugs.python.org/issue10582 + // and that causes major failures. So we are not going to go back in + // to run the GC if that's the case. + if (!m_stop && m_languageHook->HasRegisteredAddonClasses() && !m_systemExitThrown && + PyRun_SimpleString(GC_SCRIPT) == -1) + CLog::Log(LOGERROR, + "CPythonInvoker({}, {}): failed to run the gc to clean up after running prior to " + "shutting down the Interpreter", + GetId(), m_sourceFile); + + Py_EndInterpreter(m_threadState); + + // If we still have objects left around, produce an error message detailing what's been left behind + if (m_languageHook->HasRegisteredAddonClasses()) + CLog::Log(LOGWARNING, + "CPythonInvoker({}, {}): the python script \"{}\" has left several " + "classes in memory that we couldn't clean up. The classes include: {}", + GetId(), m_sourceFile, m_sourceFile, getListOfAddonClassesAsString(m_languageHook)); + + // unregister the language hook + m_languageHook->UnregisterMe(); + +#if PY_VERSION_HEX < 0x03070000 + PyEval_ReleaseLock(); +#else + PyThreadState_Swap(PyInterpreterState_ThreadHead(PyInterpreterState_Main())); + PyEval_SaveThread(); +#endif + + // set stopped event - this allows ::stop to run and kill remaining threads + // this event has to be fired without holding m_critical + // also the GIL (PyEval_AcquireLock) must not be held + // if not obeyed there is still no deadlock because ::stop waits with timeout (smart one!) + m_stoppedEvent.Set(); + + m_threadState = nullptr; + + setState(InvokerStateExecutionDone); + } + ILanguageInvoker::onExecutionDone(); +} + +void CPythonInvoker::onExecutionFailed() +{ + PyEval_SaveThread(); + + setState(InvokerStateFailed); + CLog::Log(LOGERROR, "CPythonInvoker({}, {}): abnormally terminating python thread", GetId(), + m_sourceFile); + + std::unique_lock<CCriticalSection> lock(m_critical); + m_threadState = NULL; + + ILanguageInvoker::onExecutionFailed(); +} + +void CPythonInvoker::onInitialization() +{ + XBMC_TRACE; + { + GilSafeSingleLock lock(s_critical); + initializeModules(getModules()); + } + + // get a possible initialization script + const char* runscript = getInitializationScript(); + if (runscript != NULL && strlen(runscript) > 0) + { + // redirecting default output to debug console + if (PyRun_SimpleString(runscript) == -1) + CLog::Log(LOGFATAL, "CPythonInvoker({}, {}): initialize error", GetId(), m_sourceFile); + } +} + +void CPythonInvoker::onPythonModuleInitialization(void* moduleDict) +{ + if (m_addon.get() == NULL || moduleDict == NULL) + return; + + PyObject* moduleDictionary = (PyObject*)moduleDict; + + PyObject* pyaddonid = PyUnicode_FromString(m_addon->ID().c_str()); + PyDict_SetItemString(moduleDictionary, "__xbmcaddonid__", pyaddonid); + + ADDON::CAddonVersion version = m_addon->GetDependencyVersion("xbmc.python"); + PyObject* pyxbmcapiversion = PyUnicode_FromString(version.asString().c_str()); + PyDict_SetItemString(moduleDictionary, "__xbmcapiversion__", pyxbmcapiversion); + + PyObject* pyinvokerid = PyLong_FromLong(GetId()); + PyDict_SetItemString(moduleDictionary, "__xbmcinvokerid__", pyinvokerid); + + CLog::Log(LOGDEBUG, + "CPythonInvoker({}, {}): instantiating addon using automatically obtained id of \"{}\" " + "dependent on version {} of the xbmc.python api", + GetId(), m_sourceFile, m_addon->ID(), version.asString()); +} + +void CPythonInvoker::onDeinitialization() +{ + XBMC_TRACE; +} + +void CPythonInvoker::onError(const std::string& exceptionType /* = "" */, + const std::string& exceptionValue /* = "" */, + const std::string& exceptionTraceback /* = "" */) +{ + CPyThreadState releaseGil; + std::unique_lock<CCriticalSection> gc(CServiceBroker::GetWinSystem()->GetGfxContext()); + + CGUIDialogKaiToast* pDlgToast = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKaiToast>( + WINDOW_DIALOG_KAI_TOAST); + if (pDlgToast != NULL) + { + std::string message; + if (m_addon && !m_addon->Name().empty()) + message = StringUtils::Format(g_localizeStrings.Get(2102), m_addon->Name()); + else + message = g_localizeStrings.Get(2103); + pDlgToast->QueueNotification(CGUIDialogKaiToast::Error, message, g_localizeStrings.Get(2104)); + } +} + +void CPythonInvoker::initializeModules( + const std::map<std::string, PythonModuleInitialization>& modules) +{ + for (const auto& module : modules) + { + if (!initializeModule(module.second)) + CLog::Log(LOGWARNING, "CPythonInvoker({}, {}): unable to initialize python module \"{}\"", + GetId(), m_sourceFile, module.first); + } +} + +bool CPythonInvoker::initializeModule(PythonModuleInitialization module) +{ + if (module == NULL) + return false; + + return module() != nullptr; +} + +void CPythonInvoker::getAddonModuleDeps(const ADDON::AddonPtr& addon, std::set<std::string>& paths) +{ + for (const auto& it : addon->GetDependencies()) + { + //Check if dependency is a module addon + ADDON::AddonPtr dependency; + if (CServiceBroker::GetAddonMgr().GetAddon(it.id, dependency, ADDON::AddonType::SCRIPT_MODULE, + ADDON::OnlyEnabled::CHOICE_YES)) + { + std::string path = CSpecialProtocol::TranslatePath(dependency->LibPath()); + if (paths.find(path) == paths.end()) + { + // add it and its dependencies + paths.insert(path); + getAddonModuleDeps(dependency, paths); + } + } + } +} diff --git a/xbmc/interfaces/python/PythonInvoker.h b/xbmc/interfaces/python/PythonInvoker.h new file mode 100644 index 0000000..dd093ed --- /dev/null +++ b/xbmc/interfaces/python/PythonInvoker.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/generic/ILanguageInvoker.h" +#include "interfaces/legacy/Addon.h" +#include "interfaces/python/LanguageHook.h" +#include "threads/CriticalSection.h" +#include "threads/Event.h" + +#include <map> +#include <string> +#include <vector> + +typedef struct _object PyObject; + +class CPythonInvoker : public ILanguageInvoker +{ +public: + explicit CPythonInvoker(ILanguageInvocationHandler* invocationHandler); + ~CPythonInvoker() override; + + bool Execute(const std::string& script, + const std::vector<std::string>& arguments = std::vector<std::string>()) override; + + bool IsStopping() const override { return m_stop || ILanguageInvoker::IsStopping(); } + + typedef PyObject* (*PythonModuleInitialization)(); + +protected: + // implementation of ILanguageInvoker + bool execute(const std::string& script, const std::vector<std::string>& arguments) override; + virtual void executeScript(FILE* fp, const std::string& script, PyObject* moduleDict); + bool stop(bool abort) override; + void onExecutionDone() override; + void onExecutionFailed() override; + + // custom virtual methods + virtual std::map<std::string, PythonModuleInitialization> getModules() const = 0; + virtual const char* getInitializationScript() const = 0; + virtual void onInitialization(); + // actually a PyObject* but don't wanna draw Python.h include into the header + virtual void onPythonModuleInitialization(void* moduleDict); + virtual void onDeinitialization(); + + virtual void onSuccess() {} + virtual void onAbort() {} + virtual void onError(const std::string& exceptionType = "", + const std::string& exceptionValue = "", + const std::string& exceptionTraceback = ""); + + std::string m_sourceFile; + CCriticalSection m_critical; + +private: + void initializeModules(const std::map<std::string, PythonModuleInitialization>& modules); + bool initializeModule(PythonModuleInitialization module); + void getAddonModuleDeps(const ADDON::AddonPtr& addon, std::set<std::string>& paths); + bool execute(const std::string& script, std::vector<std::wstring>& arguments); + FILE* PyFile_AsFileWithMode(PyObject* py_file, const char* mode); + + PyThreadState* m_threadState; + bool m_stop; + CEvent m_stoppedEvent; + + XBMCAddon::AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook> m_languageHook; + bool m_systemExitThrown = false; + + static CCriticalSection s_critical; +}; diff --git a/xbmc/interfaces/python/PythonSwig.cpp.template b/xbmc/interfaces/python/PythonSwig.cpp.template new file mode 100644 index 0000000..24756ea --- /dev/null +++ b/xbmc/interfaces/python/PythonSwig.cpp.template @@ -0,0 +1,942 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +%> +<% +import Helper +import SwigTypeParser +import PythonTools + +import groovy.xml.XmlUtil +import groovy.text.SimpleTemplateEngine +import java.util.regex.Pattern + +/** + * All of the method nodes and all of the class nodes are used several + * times over, so they are pulled out once here. + */ + +// --------------------------------------------------------- +// initialize the SwigTypeParser with the module's typetables +module.findAll( { it.name() == 'typetab' } ).each { SwigTypeParser.appendTypeTable(it) } +// --------------------------------------------------------- + +// --------------------------------------------------------- +// Flatten out all of the method/function nodes, whether inside +// classes or not, into 'methods' +List methods = module.depthFirst().findAll { it.name() == 'function' || it.name() == 'constructor' || it.name() == 'destructor' } +// --------------------------------------------------------- + +// --------------------------------------------------------- +// Flatten out all of the class nodes into 'classes' +List classes = module.depthFirst().findAll { it.name() == 'class' } +// --------------------------------------------------------- + +// --------------------------------------------------------- +// Initialize the Helper with the type conversions +Helper.setup(this,classes, + /** + * This is meant to contain mini-templates for converting the return type + * of the native call to be returned to the python caller. + */ + [ 'void' : 'Py_INCREF(Py_None);\n ${result} = Py_None;', + 'long': '${result} = PyLong_FromLong(${api});', + 'unsigned long': '${result} = PyLong_FromLong(${api});', + 'bool': '${result} = ${api} ? Py_True : Py_False; Py_INCREF(${result});', + 'long long': '${result} = Py_BuildValue("L", ${api});', + 'int': '${result} = Py_BuildValue("i", ${api});', + 'unsigned int': '${result} = Py_BuildValue("I", ${api});', + 'double': '${result} = PyFloat_FromDouble(${api});', + 'float': '${result} = Py_BuildValue("f", static_cast<double>(${api}));', + 'std::string' : new File('typemaps/python.string.outtm'), + 'p.q(const).char' : '${result} = PyUnicode_FromString(${api});', + (Pattern.compile('''(p.){0,1}XbmcCommons::Buffer''')) : new File('typemaps/python.buffer.outtm'), + (Pattern.compile('''std::shared_ptr<\\(.*\\)>''')) : new File('typemaps/python.smart_ptr.outtm'), + (Pattern.compile('''std::unique_ptr<\\(.*\\)>''')) : new File('typemaps/python.smart_ptr.outtm'), + (Pattern.compile('''(p.){0,1}std::vector<\\(.*\\)>''')) : new File('typemaps/python.vector.outtm'), + (Pattern.compile('''(p.){0,1}Tuple<\\(.*\\)>''')) : new File('typemaps/python.Tuple.outtm'), + (Pattern.compile('''(p.){0,1}Alternative<\\(.*\\)>''')) : new File('typemaps/python.Alternative.outtm') + ], '${result} = makePythonInstance(${api},true);', + /** + * This is meant to contain mini-templates for converting the parameter types + * of the native call to be converted from the python types provided by the caller. + * + * Note: if the type can be handled by PythonTools.ltypeToFormatChar then it wont + * appear here as it gets converted directly within the PyArg_ParseTupleAndKeywords + * call. + */ + [ + 'std::string' : 'if (${slarg}) PyXBMCGetUnicodeString(${api},${slarg},false,"${api}","${method.@name}");', + (Pattern.compile('''(p.){0,1}std::vector<\\(.*\\)>''')) : new File('typemaps/python.vector.intm'), + (Pattern.compile('''(p.){0,1}Tuple(3){0,1}<\\(.*\\)>''')) : new File('typemaps/python.Tuple.intm'), + (Pattern.compile('''(p.){0,1}Alternative<\\(.*\\)>''')) : new File('typemaps/python.Alternative.intm'), + (Pattern.compile('''(r.){0,1}XbmcCommons::Buffer''')) : new File('typemaps/python.buffer.intm'), + (Pattern.compile('''(p.){0,1}std::map<\\(.*\\)>''')) : new File('typemaps/python.map.intm'), + (Pattern.compile('''(r.){0,1}XBMCAddon::Dictionary<\\(.*\\)>''')) : new File('typemaps/python.dict.intm'), + (Pattern.compile('''p.void''')) : '${api} = (void*)${slarg};', + 'bool' : '${api} = (PyLong_AsLong(${slarg}) == 0L ? false : true);', + 'long' : '${api} = PyLong_AsLong(${slarg});', + 'unsigned long' : '${api} = PyLong_AsUnsignedLong(${slarg});', + 'long long' : '${api} = PyLong_AsLongLong(${slarg});', + 'unsigned long long' : '${api} = PyLong_AsUnsignedLongLong(${slarg});', + 'int' : '${api} = (int)PyLong_AsLong(${slarg});', + 'double' : '${api} = PyFloat_AsDouble(${slarg});', + 'float' : '${api} = (float)PyFloat_AsDouble(${slarg});', + 'XBMCAddon::StringOrInt' : 'if (${slarg}) PyXBMCGetUnicodeString(${api},${slarg},PyLong_Check(${slarg}) || PyFloat_Check(${slarg}),"${api}","${method.@name}");' + ], '${api} = (${swigTypeParser.SwigType_str(ltype)})retrieveApiInstance(${slarg},"${ltype}","${helper.findNamespace(method)}","${helper.callingName(method)}");') +// --------------------------------------------------------- + +/*******************************************************************************/ +/** + * The doMethod will actually write out the CPython method call for + * the method/function represented by the provided Node ('method'). + */ +void doMethod(Node method, MethodType methodType) +{ + boolean isOperator = method.@name.startsWith("operator ") + boolean doAsMappingIndex = false + boolean doAsCallable = false + + if (isOperator) + { + if("[]" == method.@name.substring(9)) + doAsMappingIndex = true + else if("()" == method.@name.substring(9)) + doAsCallable = true + else + return; + } + + boolean constructor = methodType == MethodType.constructor + + // if we're a constructor, but we're private, then we're outta here + if (constructor && method.@access != null && method.@access != "public") + return + + boolean destructor = methodType == MethodType.destructor + List params = method?.parm + int numParams = params?.size() + String clazz = Helper.findFullClassName(method) + String returns = constructor ? 'p.' + clazz : (destructor ? 'void' : Helper.getReturnSwigType(method)) + Node classnode = Helper.findClassNode(method) + String classNameAsVariable = clazz == null ? null : PythonTools.getClassNameAsVariable(classnode) + boolean useKeywordParsing = !('true' == classnode?.@feature_python_nokwds || 'true' == method?.@feature_python_nokwds) + + // do the docs + if (!constructor && !destructor) + { + if (Helper.hasDoc(method)) + { +%> + PyDoc_STRVAR(${PythonTools.getPyMethodName(method,methodType)}__doc__, + ${PythonTools.makeDocString(method.doc[0])}); +<% } + } +%> + static <% if(methodType == MethodType.destructor) { %>void<% } else { %>PyObject*<% } %> ${module.@name}_${PythonTools.getPyMethodName(method,methodType)} (<%= ((clazz == null) ? "PyObject" : + (constructor ? "PyTypeObject" : 'PyHolder')) %>* ${constructor ? 'pytype' : 'self'} <% + if (doAsMappingIndex) { %>, PyObject* py${params[0].@name}<% } + else if (methodType != MethodType.destructor) { %> , PyObject *args, PyObject *kwds <%} %> ) + { + XBMC_TRACE; +<% if (numParams > 0) + { + if (useKeywordParsing && !doAsMappingIndex) + { %> + static const char *keywords[] = {<% + params.each { %> + "${it.@name}",<% } %> + NULL}; +<% } + params.each { +%> + ${SwigTypeParser.SwigType_str(SwigTypeParser.convertTypeToLTypeForParam(it.@type))} ${it.@name} ${it.@value != null ? ' = ' + it.@value : SwigTypeParser.SwigType_ispointer(it.@type) ? ' = nullptr' : ''};<% + if (!PythonTools.parameterCanBeUsedDirectly(it) && !doAsMappingIndex) + { %> + PyObject* py${it.@name} = NULL;<% + } + } + if (!doAsMappingIndex) + { %> + if (!${useKeywordParsing ? 'PyArg_ParseTupleAndKeywords' : 'PyArg_ParseTuple'}( + args, + <% if (useKeywordParsing) { %>kwds,<% } %> + "<%= PythonTools.makeFormatStringFromParameters(method) %>", + <% if (useKeywordParsing) { %>const_cast<char**>(keywords),<% } %><% params.eachWithIndex { param,i -> %> + &${PythonTools.parameterCanBeUsedDirectly(param) ? '' : 'py'}${param.@name}${i < params.size() - 1 ? "," : ""}<% } %> + )) + { + return NULL; + } + +<% } + } + // now actually invoke the method + if (returns != "void") { %> ${SwigTypeParser.SwigType_str(returns)} apiResult;<% } +%> + try + { +<% + // now do the input conversion if any are necessary + params.findAll({ !PythonTools.parameterCanBeUsedDirectly(it) || doAsMappingIndex }).each { %> ${Helper.getInConversion(it.@type, it.@name, 'py' + it.@name, method)} <% println() } +%> +<% + // check to see if this method is a call to a virtual function on a director class. + boolean isDirectorCall = Helper.isDirector(method) + if (isDirectorCall) + { +%> // This is a director call coming from python so it explicitly calls the base class method. +<% + } + // now do the method call itself + if (!destructor) { + if (constructor || !clazz) { %> XBMCAddon::SetLanguageHookGuard slhg(XBMCAddon::Python::PythonLanguageHook::GetIfExists(PyThreadState_Get()->interp).get());<% println() } +%> <% + if (returns != "void") { %>apiResult = <% } + if (clazz && !constructor) { + %>((${clazz}*)retrieveApiInstance((PyObject*)self,&Ty${classNameAsVariable}_Type,"${Helper.callingName(method)}","${clazz}"))-> <% + } + if (constructor && classnode.@feature_director) { + %>(&(Ty${classNameAsVariable}_Type.pythonType) != pytype) ? new ${classNameAsVariable}_Director(<% params.eachWithIndex { param, i -> %> ${param.@name}${i < params.size() - 1 ? "," : ""} <% } %>) : <% } + + // Here is the actual call ... if this is a Director we need to do an upCall (from Python) + if (isDirectorCall){ %>${clazz}::<% } + %>${Helper.callingName(method)}( <% params.eachWithIndex { param, i -> %> ${param.@name}${i < params.size() - 1 ? "," : ""} <% } %> ); +<% + if (constructor) { %> prepareForReturn(apiResult);<% } + } // close the 'if method is not a destructor' + else { // it is a destructor +%> + ${clazz}* theObj = (${clazz}*)retrieveApiInstance((PyObject*)self,&Ty${classNameAsVariable}_Type,"~${Helper.callingName(method)}","${clazz}"); + cleanForDealloc(theObj); +<% + } +%> + } + catch (const XBMCAddon::WrongTypeException& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_TypeError, e.GetExMessage()); <% + if (!destructor) { %> + return NULL; <% + } %> + } + catch (const XbmcCommons::Exception& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_RuntimeError, e.GetExMessage()); <% + if (!destructor) { %> + return NULL; <% + } %> + } + catch (...) + { + CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${Helper.callingName(method)}\""); + PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${Helper.callingName(method)}\""); <% + if (!destructor) { %> + return NULL; <% + } %> + } +<% + if (!destructor) { %> + PyObject* result = Py_None; + + // transform the result +<% + if (constructor) { + %> result = makePythonInstance(apiResult,pytype,false);<% + } + else { +%> ${Helper.getOutConversion(returns,'result',method)}<% + } + if (constructor && method.@feature_director) { %> + if (&(Ty${classNameAsVariable}_Type.pythonType) != pytype) + ((${classNameAsVariable}_Director*)apiResult)->setPyObjectForDirector(result);<% + } + %> + + return result; <% } + else { %> + (((PyObject*)(self))->ob_type)->tp_free((PyObject*)self); + <% + } + %> + } <% +} +/*******************************************************************************/ + +/** + * This method writes out the instance of a TypeInfo (which includes + * The PyTypeObject as a member) for the class node provided. + * + * If classNameAsVariable is not null then the class name as a + * variable will be appended to it. + */ +void doClassTypeInfo(Node clazz, List classNameAsVariables = null) +{ + String classNameAsVariable = PythonTools.getClassNameAsVariable(clazz) + String fullClassName = Helper.findFullClassName(clazz) + classNameAsVariables?.add(classNameAsVariable) +%> + //========================================================================= + // These variables will hold the Python Type information for ${fullClassName} + TypeInfo Ty${classNameAsVariable}_Type(typeid(${fullClassName}));<% +%> + //========================================================================= +<% +} + +/** + * This method will take the name of an API class from another module and + * create an external reference to its TypeInfo instance. + */ +void doExternClassTypeInfo(String knownType) +{ + String classNameAsVariable = knownType.replaceAll('::','_') +%> + //========================================================================= + // These variables define the type ${knownType} from another module + extern TypeInfo Ty${classNameAsVariable}_Type; + //========================================================================= +<% +} + +/*******************************************************************************/ +/** + * This method takes the class node and outputs all of the python meta-data + * and class oddities (like comparators, as_mapping, etc.). These include: + * + * 1) comparator *_cmp python method as long as there's an operator==, an + * operator>, AND an operator<. + * 2) it will create a python "as_mapping" method as long as there's both + * an operator[], AND a .size() method on the class. + * 3) it will handle the explicitly defined rich compare (_rcmp) if the + * feature is included in the .i file using %feature("python:rcmp") + * 4) The array of PyMethodDefs for the class definition + * 5) It will handle public fields as if the were python properties by: + * a) Creating a get/set_member if there are read/write properties. + * b) Creating only a get if there are only read-only properties. + * 6) It will write the init[Classname] method for the class which will + * initialize the TypeInfo and PyTypeObject structs. + * + * If initTypeCalls is not null then the method name for the generated init + * method (see #6 above) will be appended to it. + */ +void doClassMethodInfo(Node clazz, List initTypeCalls) +{ + String classNameAsVariable = PythonTools.getClassNameAsVariable(clazz) + String fullClassName = Helper.findFullClassName(clazz) + String initTypeCall = "initPy${classNameAsVariable}_Type" + initTypeCalls?.add(initTypeCall) + + // see if we have any valid (or invalid) operators + boolean doComparator = false + boolean doAsMapping = false + boolean hasEquivalenceOp = false + boolean hasLtOp = false + boolean hasGtOp = false + Node indexOp = null + Node callableOp = null + Node sizeNode = null + + List normalMethods = clazz.function.findAll { !it.@name.startsWith("operator ") } + List operators = clazz.function.findAll { it.@name.startsWith("operator ") } + List properties = clazz.variable.findAll { it.@access != null && it.@access == "public" } + List properties_set = properties.findAll { it.@feature_immutable == null || it.@feature_immutable == 0 } + + operators.each { + // we have an operator. The only one we can handle is == + if (it.@name.substring(9).startsWith("==")) + hasEquivalenceOp = true + else if (it.@name.substring(9) == "<") + hasLtOp = true + else if (it.@name.substring(9) == ">") + hasGtOp = true + else if (it.@name.substring(9) == "[]") + indexOp = it + else if (it.@name.substring(9) == "()") + callableOp = it + else + System.err.println ("Warning: class ${fullClassName} has an operator \"${it.@name}\" that is being ignored."); + } + + if (hasGtOp || hasLtOp || hasEquivalenceOp) + { + if (!(hasLtOp && hasGtOp && hasEquivalenceOp)) + System.err.println ("Warning: class ${fullClassName} has an inconsistent operator set. To get a comparator you must implement all 3 operators >,<,==.") + else + doComparator = true + } + + if (indexOp) + { + sizeNode = clazz.function.find { it.@name == "size" } + if (sizeNode) + doAsMapping = true + else + System.err.println ("Warning: class ${fullClassName} has an inconsistent operator set. To get a as_mapping you must implement 'size' as well as operator[]") + } + + if (doAsMapping) + { +%> + static Py_ssize_t ${module.@name}_${classNameAsVariable}_size_(PyObject* self) + { + return (Py_ssize_t)((${fullClassName}*)retrieveApiInstance(self,&Ty${classNameAsVariable}_Type,"${Helper.callingName(indexOp)}","${fullClassName}"))-> size(); + } + + //========================================================================= + // tp_as_mapping struct for ${fullClassName} + //========================================================================= + PyMappingMethods ${module.@name}_${classNameAsVariable}_as_mapping = { + ${module.@name}_${classNameAsVariable}_size_, /* inquiry mp_length; __len__ */ + (PyCFunction)${module.@name}_${PythonTools.getPyMethodName(indexOp,MethodType.method)}, /* binaryfunc mp_subscript __getitem__ */ + 0, /* objargproc mp_ass_subscript; __setitem__ */ + }; +<% + } + + if (clazz.@feature_python_rcmp) + { %> + static PyObject* ${module.@name}_${classNameAsVariable}_rcmp(PyObject* obj1, PyObject *obj2, int method) + ${Helper.unescape(clazz.@feature_python_rcmp)} +<% + } +%> + //========================================================================= + // This section contains the initialization for the + // Python extension for the Api class ${fullClassName} + //========================================================================= + // All of the methods on this class + static PyMethodDef ${classNameAsVariable}_methods[] = { <% + normalMethods.each { %> + {"${it.@sym_name}", (PyCFunction)${module.@name}_${PythonTools.getPyMethodName(it,MethodType.method)}, METH_VARARGS|METH_KEYWORDS, ${Helper.hasDoc(it) ? PythonTools.getPyMethodName(it,MethodType.method) + '__doc__' : 'NULL'} }, <% } + + // now do all of the explicit feature:python:method's that may be in this class + List tmpl = [] + tmpl.addAll(clazz.attributes().keySet()) + List newMethodKeys = tmpl.findAll { it.startsWith('feature_python_method_') } + newMethodKeys.each { key -> + String featureEntry = clazz.attribute(key) + String methodName = key.substring('feature_python_method_'.length()) %> + {"${methodName}", (PyCFunction)${module.@name}_${PythonTools.getClassNameAsVariable(clazz)}_${methodName}, METH_VARARGS|METH_KEYWORDS, NULL}, +<% + } +%> + {NULL, NULL, 0, NULL} + }; + +<% + if (properties.size() > 0) { +%> static PyObject* ${classNameAsVariable}_getMember(PyHolder *self, void *name) + { + if (self == NULL) + return NULL; +<% + String clazzName = Helper.findFullClassName(properties[0]) +%> + try + { + ${clazzName}* theObj = (${clazzName}*)retrieveApiInstance((PyObject*)self, &Ty${classNameAsVariable}_Type, "${classNameAsVariable}_getMember()", "${clazzName}"); + + PyObject* result = NULL; + <% + properties.each { + String returns = Helper.getPropertyReturnSwigType(it); +%> if (strcmp((char*)name, "${it.@sym_name}") == 0) + { + ${SwigTypeParser.SwigType_lstr(returns)} apiResult = theObj->${it.@sym_name}; + ${Helper.getOutConversion(returns, 'result', it)} + } + else<% + } %> + { + Py_INCREF(Py_None); + return Py_None; + } + + return result; + } + catch (const XBMCAddon::WrongTypeException& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_TypeError, e.GetExMessage()); + return NULL; + } + catch (const XbmcCommons::Exception& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_RuntimeError, e.GetExMessage()); + return NULL; + } + catch (...) + { + CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${classNameAsVariable}_getMember()\""); + PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${classNameAsVariable}_getMember()\""); + return NULL; + } + + return NULL; + } + +<% + if (properties_set.size() > 0) { +%> int ${classNameAsVariable}_setMember(PyHolder *self, PyObject *value, void *name) + { + if (self == NULL) + return -1; + + ${clazzName}* theObj = NULL; + try + { + theObj = (${clazzName}*)retrieveApiInstance((PyObject*)self, &Ty${classNameAsVariable}_Type, "${classNameAsVariable}_getMember()", "${clazzName}"); + } + catch (const XBMCAddon::WrongTypeException& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_TypeError, e.GetExMessage()); + return -1; + } + catch (const XbmcCommons::Exception& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_RuntimeError, e.GetExMessage()); + return -1; + } + catch (...) + { + CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${classNameAsVariable}_getMember()\""); + PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${classNameAsVariable}_getMember()\""); + return -1; + } + +<% + properties_set.each { + String returns = Helper.getPropertyReturnSwigType(it); +%> if (strcmp((char*)name, "${it.@sym_name}") == 0) + { + ${SwigTypeParser.SwigType_lstr(returns)} tmp; + ${Helper.getInConversion(returns, 'tmp', 'value', it)} + if (PyErr_Occurred()) + throw PythonBindings::PythonToCppException(); + + theObj->${it.@sym_name} = tmp; + } + else<% + } %> + return -1; + + return 0; + } <% + } +%> + + // All of the methods on this class + static PyGetSetDef ${classNameAsVariable}_getsets[] = { <% + properties.each { %> + {(char*)"${it.@sym_name}", (getter)${classNameAsVariable}_getMember, ${(it.@feature_immutable == null || it.@feature_immutable == 0) ? '(setter)' + classNameAsVariable + '_setMember' : 'NULL'}, (char*)${Helper.hasDoc(it) ? PythonTools.makeDocString(it.doc[0]) : 'NULL'}, (char*)"${it.@sym_name}" }, <% } +%> + {NULL} + }; +<% + } + + if ((clazz.@feature_iterator && clazz.@feature_iterator != '') || + (clazz.@feature_iterable && clazz.@feature_iterable != '')) { %> + static PyObject* ${module.@name}_${classNameAsVariable}_iter(PyObject* self) + { <% + if (clazz.@feature_iterator) { %> + return self; <% + } + else { %> + PyObject* result = NULL; + try + { + ${clazz.@feature_iterable}* apiResult = ((${fullClassName}*)retrieveApiInstance(self,&Ty${classNameAsVariable}_Type,"${module.@name}_${classNameAsVariable}_iternext","${fullClassName}"))->begin(); + + ${Helper.getOutConversion('p.' + clazz.@feature_iterable,'result',clazz)} + } + catch (const XBMCAddon::WrongTypeException& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_TypeError, e.GetExMessage()); + return NULL; + } + catch (const XbmcCommons::Exception& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_RuntimeError, e.GetExMessage()); + return NULL; + } + catch (...) + { + CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${module.@name}_${classNameAsVariable}_iternext\""); + PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${module.@name}_${classNameAsVariable}_iternext\""); + return NULL; + } + + return result; <% + } %> + } +<% + + if (clazz.@feature_iterator) { %> + static PyObject* ${module.@name}_${classNameAsVariable}_iternext(PyObject* self) + { + PyObject* result = NULL; + try + { + ${fullClassName}* iter = (${fullClassName}*)retrieveApiInstance(self,&Ty${classNameAsVariable}_Type,"${module.@name}_${classNameAsVariable}_iternext","${fullClassName}"); + + // check if we have reached the end + if (!iter->end()) + { + ++(*iter); + + ${clazz.@feature_iterator} apiResult = **iter; + ${Helper.getOutConversion(clazz.@feature_iterator,'result',clazz)} + } + } + catch (const XBMCAddon::WrongTypeException& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_TypeError, e.GetExMessage()); + return NULL; + } + catch (const XbmcCommons::Exception& e) + { + CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage()); + PyErr_SetString(PyExc_RuntimeError, e.GetExMessage()); + return NULL; + } + catch (...) + { + CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${module.@name}_${classNameAsVariable}_iternext\""); + PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${module.@name}_${classNameAsVariable}_iternext\""); + return NULL; + } + + return result; + } +<% + } + } +%> + + // This method initializes the above mentioned Python Type structure + static void ${initTypeCall}() + { +<% + if (Helper.hasDoc(clazz)) + { +%> + PyDoc_STRVAR(${classNameAsVariable}__doc__, + ${PythonTools.makeDocString(clazz.doc[0])} + ); +<% } %> + + PyTypeObject& pythonType = Ty${classNameAsVariable}_Type.pythonType; + pythonType.tp_name = "${module.@name}.${clazz.@sym_name}"; + pythonType.tp_basicsize = sizeof(PyHolder); + pythonType.tp_dealloc = (destructor)${module.@name}_${classNameAsVariable}_Dealloc; <% + + if (clazz.@feature_python_rcmp) { %> + pythonType.tp_richcompare=(richcmpfunc)${module.@name}_${classNameAsVariable}_rcmp;<% + } %> + + pythonType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + + pythonType.tp_doc = ${Helper.hasDoc(clazz) ? (classNameAsVariable + '__doc__') : 'NULL' }; + pythonType.tp_methods = ${classNameAsVariable}_methods; <% + if (properties.size() > 0) { %> + pythonType.tp_getset = ${classNameAsVariable}_getsets; +<% + } + if (callableOp) { %> + pythonType.tp_call = (ternaryfunc)${module.@name}_${PythonTools.getPyMethodName(callableOp,MethodType.method)}; +<% + } + if (doAsMapping) { %> + pythonType.tp_as_mapping = &${module.@name}_${classNameAsVariable}_as_mapping; +<% + } + + if (clazz.@feature_iterator) { %> + pythonType.tp_iter = (getiterfunc)${module.@name}_${classNameAsVariable}_iter; + pythonType.tp_iternext = (iternextfunc)${module.@name}_${classNameAsVariable}_iternext; +<% + } + else if (clazz.@feature_iterable && clazz.@feature_iterable != '') { %> + pythonType.tp_iter = (getiterfunc)${module.@name}_${classNameAsVariable}_iter; +<% + } + + Node baseclass = PythonTools.findValidBaseClass(clazz, module) +%> + + pythonType.tp_base = ${baseclass ? ('&(Ty' + PythonTools.getClassNameAsVariable(baseclass) + '_Type.pythonType)') : "NULL"}; + pythonType.tp_new = <% !Helper.hasDefinedConstructor(clazz) || Helper.hasHiddenConstructor(clazz) ? print('NULL') : print("${module.@name}_${classNameAsVariable}_New") %>; + pythonType.tp_init = dummy_tp_init; + + Ty${classNameAsVariable}_Type.swigType="p.${fullClassName}";<% + if (baseclass) { %> + Ty${classNameAsVariable}_Type.parentType=&Ty${PythonTools.getClassNameAsVariable(baseclass)}_Type; +<%} + + if (!Helper.hasHiddenConstructor(clazz)) { %> + registerAddonClassTypeInformation(&Ty${classNameAsVariable}_Type); +<%} %> + } + //========================================================================= +<% +} +/*******************************************************************************/ + + +List getAllVirtualMethods(Node clazz) +{ + List ret = [] + ret.addAll(clazz.findAll({ it.name() == 'function' && it.@storage && it.@storage == 'virtual' })) + if (clazz.baselist) { + if (clazz.baselist[0].base) clazz.baselist[0].base.each { + Node baseclassnode = Helper.findClassNodeByName(module,it.@name,clazz) + if (baseclassnode && baseclassnode.@feature_director) ret.addAll(getAllVirtualMethods(baseclassnode)) + } + } + return ret; +} + +%> +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +// ************************************************************************ +// This file was generated by xbmc compile process. DO NOT EDIT!! +// It was created by running the code generator on the spec file for +// the module "${module.@name}" on the template file PythonSwig.template.cpp +// ************************************************************************ + +<% +Helper.getInsertNodes(module, 'begin').each { %>${Helper.unescape(it)}<% } +%> + +#include <Python.h> +#include <string> +#include "CompileInfo.h" +#include "interfaces/python/LanguageHook.h" +#include "interfaces/python/swig.h" +#include "interfaces/python/PyContext.h" + +<% +Helper.getInsertNodes(module, 'header').each { %>${Helper.unescape(it)}<% } +%> + +namespace PythonBindings +{ +<% + // initTypeCalls is the + List initTypeCalls = [] + List classNameAsVariables = [] + + classes.each { clazz -> doClassTypeInfo(clazz, classNameAsVariables) } + + // make sure known api types are declared as externs + + // first, find all of the declared known api types + Set<String> knownApiTypes = new HashSet<String>() + module.depthFirst().each + { + String attr = it.attribute('feature_knownapitypes') + if (attr != null) + { + attr.trim().split(',').each { knownApiTypes.add(it) } + } + } + + // now declare an extern for each one + knownApiTypes.each { doExternClassTypeInfo(it) } + +%> + +<% +//========================================================================= +// Do the directors. For every class that can be extended in python, we +// need to create a Director instance with bridging calls. This chunk of +// code will generate those classes. + classes.findAll({ it.@feature_director != null }).each { clazz -> + // find the constructor for this class + constructor = clazz.constructor[0] +%> + //========================================================================= + // This class is the Director for ${Helper.findFullClassName(clazz)}. + // It provides the "reverse bridge" from C++ to Python to support + // cross-language polymorphism. + //========================================================================= + class ${PythonTools.getClassNameAsVariable(clazz)}_Director : public Director, public ${clazz.@name} + { + public: +<% + if (constructor) + {%> + inline ${PythonTools.getClassNameAsVariable(clazz)}_Director(<% + List params = constructor?.parm + params.eachWithIndex { param, i -> %>${SwigTypeParser.SwigType_str(param.@type)} ${param.@name}${i < params.size() - 1 ? "," : ""} <% } + %>) : ${Helper.findFullClassName(constructor)}(<% + params.eachWithIndex { param, i -> %> ${param.@name}${i < params.size() - 1 ? "," : ""} <% } %>) { } <% + } +%> +<% + getAllVirtualMethods(clazz).each + { %> + virtual ${SwigTypeParser.SwigType_str(Helper.getReturnSwigType(it))} ${Helper.callingName(it)}( <% + List params = it?.parm + String paramFormatStr = '' + params.each { paramFormatStr += 'O' } + params.eachWithIndex { param, i -> %> ${SwigTypeParser.SwigType_str(param.@type)} ${param.@name}${i < params.size() - 1 ? "," : ""} <% } + %> ) + { <% + params.each + { param -> + %> + PyObject* py${param.@name} = NULL; + ${Helper.getOutConversion(param.@type,'result',it,['result' : 'py' + param.@name, 'api' : param.@name])}<% + } +%> + XBMCAddon::Python::PyContext pyContext; + PyObject_CallMethod(self,"${Helper.callingName(it)}","(${paramFormatStr})"<% + params.each { + %>, py${it.@name} <% + } + %>); + if (PyErr_Occurred()) + throw PythonBindings::PythonToCppException(); + } +<% } + +%> + }; +<% + } +//========================================================================= + + // types used as method parameter or return values need to be declared + // as extern if they are unknown types. + methods.each { if (it.name() != 'destructor') { doMethod(it, (it.name() == 'constructor' ? MethodType.constructor : MethodType.method)); println(); } } + classes.each { clazz -> doMethod(clazz, MethodType.destructor) } + + // now find any methods that have been added explicitly + classes.each { node -> + List tmpl = [] + tmpl.addAll(node.attributes().keySet()) + List newMethodKeys = tmpl.findAll { it.startsWith('feature_python_method_') } + newMethodKeys.each { key -> + String featureEntry = node.attribute(key) + String methodName = key.substring('feature_python_method_'.length()) %> + static PyObject* ${module.@name}_${PythonTools.getClassNameAsVariable(node)}_${methodName}(PyObject* self, PyObject *args, PyObject *kwds) + ${Helper.unescape(featureEntry)} +<% + } + } + + classes.each { clazz -> doClassMethodInfo(clazz, initTypeCalls) } + +%> + + static PyMethodDef ${module.@name}_methods[] = { <% + module.depthFirst().findAll({ it.name() == 'function' && Helper.parents(it, { Node lnode -> lnode.name() == 'class'}).size() == 0 }).each { %> + {"${it.@sym_name}", (PyCFunction)${module.@name}_${PythonTools.getPyMethodName(it,MethodType.method)}, METH_VARARGS|METH_KEYWORDS, ${Helper.hasDoc(it) ? PythonTools.getPyMethodName(it,MethodType.method) + '__doc__' : 'NULL'} }, <% } +%> + {NULL, NULL, 0, NULL} + }; + + // This is the call that will call all of the other initializes + // for all of the classes in this module + static void initTypes() + { + static bool typesAlreadyInitialized = false; + if (!typesAlreadyInitialized) + { + typesAlreadyInitialized = true; +<% + initTypeCalls.each { %> + ${it}();<% + } + + classNameAsVariables.each { %> + if (PyType_Ready(&(Ty${it}_Type.pythonType)) < 0) + return;<% + }%> + } + } + + static struct PyModuleDef createModule + { + PyModuleDef_HEAD_INIT, + "${module.@name}", + "", + -1, + ${module.@name}_methods, + nullptr, + nullptr, + nullptr, + nullptr, + }; + + PyObject *PyInit_Module_${module.@name}() + { + initTypes(); + + // init general ${module.@name} modules + PyObject* module; + +<% classNameAsVariables.each { %> + Py_INCREF(&(Ty${it}_Type.pythonType));<% + }%> + + module = PyModule_Create(&createModule); + if (module == NULL) return NULL; + +<% classes.each { clazz -> %> + PyModule_AddObject(module, "${clazz.@sym_name}", (PyObject*)(&(Ty${PythonTools.getClassNameAsVariable(clazz)}_Type.pythonType)));<% + }%> + + // constants + PyModule_AddStringConstant(module, "__author__", "Team Kodi <http://kodi.tv>"); + PyModule_AddStringConstant(module, "__date__", CCompileInfo::GetBuildDate().c_str()); + PyModule_AddStringConstant(module, "__version__", "3.0.1"); + PyModule_AddStringConstant(module, "__credits__", "Team Kodi"); + PyModule_AddStringConstant(module, "__platform__", "ALL"); + + // need to handle constants + // #define constants +<% module.depthFirst().findAll( { it.name() == 'constant'} ).each { + String pyCall = + (it.@type == 'int' || it.@type == 'long' || it.@type == 'unsigned int' || it.@type == 'unsigned long' || it.@type == 'bool') ? + 'PyModule_AddIntConstant' : 'PyModule_AddStringConstant' %> + ${pyCall}(module,"${it.@sym_name}",${it.@value}); <% + } %> + // constexpr constants +<% module.depthFirst().findAll( { it.name() == 'variable' && it.@storage && it.@storage == "constexpr" && !it.@error } ).each { + String pyCall = + ( it.@type == 'q(const).int' || it.@type == 'q(const).long' || it.@type == 'q(const).unsigned int' || it.@type == 'q(const).unsigned long' || it.@type == 'q(const).bool' ) ? + 'PyModule_AddIntConstant' : 'PyModule_AddStringConstant' %> + ${pyCall}(module,"${it.@sym_name}",${it.@value}); <% + } %> + return module; + } + +} // end PythonBindings namespace for python type definitions + +<% +Helper.getInsertNodes(module, 'footer').each { %>${Helper.unescape(it)}<% } +%> diff --git a/xbmc/interfaces/python/PythonTools.groovy b/xbmc/interfaces/python/PythonTools.groovy new file mode 100644 index 0000000..e41db6c --- /dev/null +++ b/xbmc/interfaces/python/PythonTools.groovy @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +import Helper +import SwigTypeParser + +public class PythonTools +{ + /** + * This array defines a mapping of the api spec type to the python parse format character. + * By default, if a lookup here results in 'null' then the format char is 'O' + */ + private static Map ltypeToFormatChar = [ + 'p.char':"s", bool:"b", + int:"i", 'unsigned int' : 'I', + long:"l", 'unsigned long' : 'k', + 'double':"d", 'float':"f", + 'long long' : "L" + ] + + /** + * if the parameter can be directly read from python then its type should be in the ltypeToFormatChar + * otherwise we need an intermediate pyobject + */ + public static boolean parameterCanBeUsedDirectly(Node param) { return ltypeToFormatChar[SwigTypeParser.convertTypeToLTypeForParam(param.@type)] != null } + + /** + * This method will take the parameter list from the method node passed + * and will convert it to a Python argument string for PyArg_ParseTupleAndKeywords + */ + public static String makeFormatStringFromParameters(Node method) + { + if (!method) + return '' + List params = method.parm + String format = "" + boolean previousDefaulted = false + params.eachWithIndex { param, i -> + String defaultValue = param.@value + String paramtype = SwigTypeParser.convertTypeToLTypeForParam(param.@type) + String curFormat = ltypeToFormatChar[paramtype]; + if (curFormat == null) // then we will assume it's an object + curFormat = "O"; + + if (defaultValue != null && !previousDefaulted) + { + format +="|" + previousDefaulted = true + } + format += curFormat + } + return format; + } + + /** + * This method gets the FULL class name as a variable including the + * namespace. If converts all of the '::' references to '_' so + * that the result can be used in part, or in whole, as a variable name + */ + public static String getClassNameAsVariable(Node clazz) { return Helper.findFullClassName(clazz).replaceAll('::','_') } + + public static String getPyMethodName(Node method, MethodType methodType) + { + String clazz = Helper.findFullClassName(method)?.replaceAll('::','_') + + // if we're not in a class then this must be a method node + assert (clazz != null || methodType == MethodType.method), 'Cannot use a non-class function as a constructor or destructor ' + method + + // it's ok to pass a 'class' node if the methodType is either constructor or destructor + assert (method.name() != 'class' || (methodType == MethodType.constructor || methodType == MethodType.destructor)) + + // if this is a constructor node then the methodtype best reflect that + assert (method.name() != 'constructor' || methodType == MethodType.constructor), 'Cannot use a constructor node and not identify the type as a constructor' + method + + // if this is a destructor node then the methodtype best reflect that + assert (method.name() != 'destructor' || methodType == MethodType.destructor), 'Cannot use a destructor node and not identify the type as a destructor' + method + + if (clazz == null) + return method.@sym_name + + if (methodType == MethodType.constructor) + return clazz + "_New" + + if (methodType == MethodType.destructor) + return clazz + "_Dealloc" + + if (method.@name.startsWith("operator ")) + { + if ("[]" == method.@name.substring(9)) + return clazz + "_operatorIndex_" + + if ("()" == method.@name.substring(9)) + return clazz + "_callable_" + } + + return clazz + "_" + method.@sym_name; + } + + public static String makeDocString(Node docnode) + { + if (docnode?.name() != 'doc') + throw new RuntimeException("Invalid doc Node passed to PythonTools.makeDocString (" + docnode + ")") + + String[] lines = (docnode.@value).split(Helper.newline) + def ret = '' + lines.eachWithIndex { val, index -> + val = ((val =~ /\\n/).replaceAll('')) // remove extraneous \n's + val = val.replaceAll("\\\\","\\\\\\\\") // escape backslash + val = ((val =~ /\"/).replaceAll("\\\\\"")) // escape quotes + ret += ('"' + val + '\\n"' + (index != lines.length - 1 ? Helper.newline : '')) + } + + return ret + } + + public static Node findValidBaseClass(Node clazz, Node module, boolean warn = false) + { + // I need to find the base type if there is a known class with it + assert clazz.baselist.size() < 2, "${clazz} has multiple baselists - need to write code to separate out the public one." + String baseclass = 'NULL' + List knownbases = [] + if (clazz.baselist) + { + if (clazz.baselist[0].base) clazz.baselist[0].base.each { + Node baseclassnode = Helper.findClassNodeByName(module,it.@name,clazz) + if (baseclassnode) knownbases.add(baseclassnode) + else if (warn && !Helper.isKnownBaseType(it.@name,clazz)) + System.out.println("WARNING: the base class ${it.@name} for ${Helper.findFullClassName(clazz)} is unrecognized within ${module.@name}.") + } + } + assert knownbases.size() < 2, + "The class ${Helper.findFullClassName(clazz)} has too many known base classes. Multiple inheritance isn't supported in the code generator. Please \"#ifdef SWIG\" out all but one." + return knownbases.size() > 0 ? knownbases[0] : null + } +} diff --git a/xbmc/interfaces/python/XBPython.cpp b/xbmc/interfaces/python/XBPython.cpp new file mode 100644 index 0000000..ee8ed93 --- /dev/null +++ b/xbmc/interfaces/python/XBPython.cpp @@ -0,0 +1,627 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +// clang-format off +// python.h should always be included first before any other includes +#include <mutex> +#include <Python.h> +// clang-format on + +#include "XBPython.h" + +#include "ServiceBroker.h" +#include "Util.h" +#include "cores/DllLoader/DllLoaderContainer.h" +#include "filesystem/SpecialProtocol.h" +#include "interfaces/AnnouncementManager.h" +#include "interfaces/legacy/AddonUtils.h" +#include "interfaces/legacy/Monitor.h" +#include "interfaces/python/AddonPythonInvoker.h" +#include "interfaces/python/PythonInvoker.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/JSONVariantWriter.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "utils/CharsetConverter.h" + +#ifdef TARGET_WINDOWS +#include "platform/Environment.h" +#endif + +#include <algorithm> + +// Only required for Py3 < 3.7 +PyThreadState* savestate; + +bool XBPython::m_bInitialized = false; + +XBPython::XBPython() +{ + CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this); +} + +XBPython::~XBPython() +{ + XBMC_TRACE; + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); +} + +#define LOCK_AND_COPY(type, dest, src) \ + if (!m_bInitialized) \ + return; \ + std::unique_lock<CCriticalSection> lock(src); \ + src.hadSomethingRemoved = false; \ + type dest; \ + dest = src + +#define CHECK_FOR_ENTRY(l, v) \ + (l.hadSomethingRemoved ? (std::find(l.begin(), l.end(), v) != l.end()) : true) + +void XBPython::Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) +{ + if (flag & ANNOUNCEMENT::VideoLibrary) + { + if (message == "OnScanFinished") + OnScanFinished("video"); + else if (message == "OnScanStarted") + OnScanStarted("video"); + else if (message == "OnCleanStarted") + OnCleanStarted("video"); + else if (message == "OnCleanFinished") + OnCleanFinished("video"); + } + else if (flag & ANNOUNCEMENT::AudioLibrary) + { + if (message == "OnScanFinished") + OnScanFinished("music"); + else if (message == "OnScanStarted") + OnScanStarted("music"); + else if (message == "OnCleanStarted") + OnCleanStarted("music"); + else if (message == "OnCleanFinished") + OnCleanFinished("music"); + } + else if (flag & ANNOUNCEMENT::GUI) + { + if (message == "OnScreensaverDeactivated") + OnScreensaverDeactivated(); + else if (message == "OnScreensaverActivated") + OnScreensaverActivated(); + else if (message == "OnDPMSDeactivated") + OnDPMSDeactivated(); + else if (message == "OnDPMSActivated") + OnDPMSActivated(); + } + + std::string jsonData; + if (CJSONVariantWriter::Write( + data, jsonData, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonOutputCompact)) + OnNotification(sender, + std::string(ANNOUNCEMENT::AnnouncementFlagToString(flag)) + "." + + std::string(message), + jsonData); +} + +// message all registered callbacks that we started playing +void XBPython::OnPlayBackStarted(const CFileItem& file) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackStarted(file); + } +} + +// message all registered callbacks that we changed stream +void XBPython::OnAVStarted(const CFileItem& file) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnAVStarted(file); + } +} + +// message all registered callbacks that we changed stream +void XBPython::OnAVChange() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnAVChange(); + } +} + +// message all registered callbacks that we paused playing +void XBPython::OnPlayBackPaused() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackPaused(); + } +} + +// message all registered callbacks that we resumed playing +void XBPython::OnPlayBackResumed() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackResumed(); + } +} + +// message all registered callbacks that xbmc stopped playing +void XBPython::OnPlayBackEnded() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackEnded(); + } +} + +// message all registered callbacks that user stopped playing +void XBPython::OnPlayBackStopped() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackStopped(); + } +} + +// message all registered callbacks that playback stopped due to error +void XBPython::OnPlayBackError() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackError(); + } +} + +// message all registered callbacks that playback speed changed (FF/RW) +void XBPython::OnPlayBackSpeedChanged(int iSpeed) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackSpeedChanged(iSpeed); + } +} + +// message all registered callbacks that player is seeking +void XBPython::OnPlayBackSeek(int64_t iTime, int64_t seekOffset) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackSeek(iTime, seekOffset); + } +} + +// message all registered callbacks that player chapter seeked +void XBPython::OnPlayBackSeekChapter(int iChapter) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnPlayBackSeekChapter(iChapter); + } +} + +// message all registered callbacks that next item has been queued +void XBPython::OnQueueNextItem() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it)) + ((IPlayerCallback*)it)->OnQueueNextItem(); + } +} + +void XBPython::RegisterPythonPlayerCallBack(IPlayerCallback* pCallback) +{ + XBMC_TRACE; + std::unique_lock<CCriticalSection> lock(m_vecPlayerCallbackList); + m_vecPlayerCallbackList.push_back(pCallback); +} + +void XBPython::UnregisterPythonPlayerCallBack(IPlayerCallback* pCallback) +{ + XBMC_TRACE; + std::unique_lock<CCriticalSection> lock(m_vecPlayerCallbackList); + PlayerCallbackList::iterator it = m_vecPlayerCallbackList.begin(); + while (it != m_vecPlayerCallbackList.end()) + { + if (*it == pCallback) + { + it = m_vecPlayerCallbackList.erase(it); + m_vecPlayerCallbackList.hadSomethingRemoved = true; + } + else + ++it; + } +} + +void XBPython::RegisterPythonMonitorCallBack(XBMCAddon::xbmc::Monitor* pCallback) +{ + XBMC_TRACE; + std::unique_lock<CCriticalSection> lock(m_vecMonitorCallbackList); + m_vecMonitorCallbackList.push_back(pCallback); +} + +void XBPython::UnregisterPythonMonitorCallBack(XBMCAddon::xbmc::Monitor* pCallback) +{ + XBMC_TRACE; + std::unique_lock<CCriticalSection> lock(m_vecMonitorCallbackList); + MonitorCallbackList::iterator it = m_vecMonitorCallbackList.begin(); + while (it != m_vecMonitorCallbackList.end()) + { + if (*it == pCallback) + { + it = m_vecMonitorCallbackList.erase(it); + m_vecMonitorCallbackList.hadSomethingRemoved = true; + } + else + ++it; + } +} + +void XBPython::OnSettingsChanged(const std::string& ID) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it) && (it->GetId() == ID)) + it->OnSettingsChanged(); + } +} + +void XBPython::OnScreensaverActivated() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnScreensaverActivated(); + } +} + +void XBPython::OnScreensaverDeactivated() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnScreensaverDeactivated(); + } +} + +void XBPython::OnDPMSActivated() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnDPMSActivated(); + } +} + +void XBPython::OnDPMSDeactivated() +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnDPMSDeactivated(); + } +} + +void XBPython::OnScanStarted(const std::string& library) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnScanStarted(library); + } +} + +void XBPython::OnScanFinished(const std::string& library) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnScanFinished(library); + } +} + +void XBPython::OnCleanStarted(const std::string& library) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnCleanStarted(library); + } +} + +void XBPython::OnCleanFinished(const std::string& library) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnCleanFinished(library); + } +} + +void XBPython::OnNotification(const std::string& sender, + const std::string& method, + const std::string& data) +{ + XBMC_TRACE; + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + it->OnNotification(sender, method, data); + } +} + +void XBPython::Uninitialize() +{ + // don't handle any more announcements as most scripts are probably already + // stopped and executing a callback on one of their already destroyed classes + // would lead to a crash + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + + LOCK_AND_COPY(std::vector<PyElem>, tmpvec, m_vecPyList); + m_vecPyList.clear(); + m_vecPyList.hadSomethingRemoved = true; + + lock.unlock(); //unlock here because the python thread might lock when it exits + + // cleanup threads that are still running + tmpvec.clear(); +} + +void XBPython::Process() +{ + if (m_bInitialized) + { + PyList tmpvec; + std::unique_lock<CCriticalSection> lock(m_vecPyList); + for (PyList::iterator it = m_vecPyList.begin(); it != m_vecPyList.end();) + { + if (it->bDone) + { + tmpvec.push_back(*it); + it = m_vecPyList.erase(it); + m_vecPyList.hadSomethingRemoved = true; + } + else + ++it; + } + lock.unlock(); + + //delete scripts which are done + tmpvec.clear(); + } +} + +bool XBPython::OnScriptInitialized(ILanguageInvoker* invoker) +{ + if (invoker == NULL) + return false; + + XBMC_TRACE; + CLog::Log(LOGDEBUG, "initializing python engine."); + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iDllScriptCounter++; + if (!m_bInitialized) + { + // Darwin packs .pyo files, we need PYTHONOPTIMIZE on in order to load them. + // linux built with unified builds only packages the pyo files so need it +#if defined(TARGET_DARWIN) || defined(TARGET_LINUX) + setenv("PYTHONOPTIMIZE", "1", 1); +#endif + // Info about interesting python envvars available + // at http://docs.python.org/using/cmdline.html#environment-variables + +#if !defined(TARGET_WINDOWS) && !defined(TARGET_ANDROID) + // check if we are running as real xbmc.app or just binary + if (!CUtil::GetFrameworksPath(true).empty()) + { + // using external python, it's build looking for xxx/lib/python3.8 + // so point it to frameworks which is where python3.8 is located + setenv("PYTHONHOME", CSpecialProtocol::TranslatePath("special://frameworks").c_str(), 1); + setenv("PYTHONPATH", CSpecialProtocol::TranslatePath("special://frameworks").c_str(), 1); + CLog::Log(LOGDEBUG, "PYTHONHOME -> {}", + CSpecialProtocol::TranslatePath("special://frameworks")); + CLog::Log(LOGDEBUG, "PYTHONPATH -> {}", + CSpecialProtocol::TranslatePath("special://frameworks")); + } +#elif defined(TARGET_WINDOWS) + +#ifdef TARGET_WINDOWS_STORE +#ifdef _DEBUG + CEnvironment::putenv("PYTHONCASEOK=1"); +#endif + CEnvironment::putenv("OS=win10"); +#else // TARGET_WINDOWS_DESKTOP + CEnvironment::putenv("OS=win32"); +#endif + + std::wstring pythonHomeW; + CCharsetConverter::utf8ToW(CSpecialProtocol::TranslatePath("special://xbmc/system/python"), + pythonHomeW); + Py_SetPythonHome(pythonHomeW.c_str()); + + std::string pythonPath = CSpecialProtocol::TranslatePath("special://xbmc/system/python/DLLs"); + pythonPath += ";"; + pythonPath += CSpecialProtocol::TranslatePath("special://xbmc/system/python/Lib"); + pythonPath += ";"; + pythonPath += CSpecialProtocol::TranslatePath("special://xbmc/system/python/Lib/site-packages"); + std::wstring pythonPathW; + CCharsetConverter::utf8ToW(pythonPath, pythonPathW); + + Py_SetPath(pythonPathW.c_str()); + + Py_OptimizeFlag = 1; +#endif + + Py_Initialize(); + +#if PY_VERSION_HEX < 0x03070000 + // Python >= 3.7 Py_Initialize implicitly calls PyEval_InitThreads + // Python < 3.7 we have to manually call initthreads. + // PyEval_InitThreads is a no-op on subsequent calls, No need to wrap in + // PyEval_ThreadsInitialized() check + PyEval_InitThreads(); +#endif + + // Acquire GIL if thread doesn't currently hold. + if (!PyGILState_Check()) + PyEval_RestoreThread((PyThreadState*)m_mainThreadState); + + if (!(m_mainThreadState = PyThreadState_Get())) + CLog::Log(LOGERROR, "Python threadstate is NULL."); + savestate = PyEval_SaveThread(); + + m_bInitialized = true; + } + + return m_bInitialized; +} + +void XBPython::OnScriptStarted(ILanguageInvoker* invoker) +{ + if (invoker == NULL) + return; + + if (!m_bInitialized) + return; + + PyElem inf; + inf.id = invoker->GetId(); + inf.bDone = false; + inf.pyThread = static_cast<CPythonInvoker*>(invoker); + std::unique_lock<CCriticalSection> lock(m_vecPyList); + m_vecPyList.push_back(inf); +} + +void XBPython::NotifyScriptAborting(ILanguageInvoker* invoker) +{ + XBMC_TRACE; + + long invokerId(-1); + if (invoker != NULL) + invokerId = invoker->GetId(); + + LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList); + for (auto& it : tmp) + { + if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it)) + { + if (invokerId < 0 || it->GetInvokerId() == invokerId) + it->AbortNotify(); + } + } +} + +void XBPython::OnExecutionEnded(ILanguageInvoker* invoker) +{ + std::unique_lock<CCriticalSection> lock(m_vecPyList); + PyList::iterator it = m_vecPyList.begin(); + while (it != m_vecPyList.end()) + { + if (it->id == invoker->GetId()) + { + if (it->pyThread->IsStopping()) + CLog::Log(LOGDEBUG, "Python interpreter interrupted by user"); + else + CLog::Log(LOGDEBUG, "Python interpreter stopped"); + it->bDone = true; + } + ++it; + } +} + +void XBPython::OnScriptFinalized(ILanguageInvoker* invoker) +{ + XBMC_TRACE; + std::unique_lock<CCriticalSection> lock(m_critSection); + // for linux - we never release the library. its loaded and stays in memory. + if (m_iDllScriptCounter) + m_iDllScriptCounter--; + else + CLog::Log(LOGERROR, "Python script counter attempted to become negative"); +} + +ILanguageInvoker* XBPython::CreateInvoker() +{ + return new CAddonPythonInvoker(this); +} + +void XBPython::PulseGlobalEvent() +{ + m_globalEvent.Set(); +} + +bool XBPython::WaitForEvent(CEvent& hEvent, unsigned int milliseconds) +{ + // wait for either this event our our global event + XbmcThreads::CEventGroup eventGroup{&hEvent, &m_globalEvent}; + CEvent* ret = eventGroup.wait(std::chrono::milliseconds(milliseconds)); + if (ret) + m_globalEvent.Reset(); + return ret != NULL; +} diff --git a/xbmc/interfaces/python/XBPython.h b/xbmc/interfaces/python/XBPython.h new file mode 100644 index 0000000..e54b4a2 --- /dev/null +++ b/xbmc/interfaces/python/XBPython.h @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "cores/IPlayerCallback.h" +#include "interfaces/IAnnouncer.h" +#include "interfaces/generic/ILanguageInvocationHandler.h" +#include "threads/CriticalSection.h" +#include "threads/Event.h" +#include "threads/Thread.h" + +#include <memory> +#include <vector> + +class CPythonInvoker; +class CVariant; + +typedef struct +{ + int id; + bool bDone; + CPythonInvoker* pyThread; +} PyElem; + +class LibraryLoader; + +namespace XBMCAddon +{ +namespace xbmc +{ +class Monitor; +} +} // namespace XBMCAddon + +template<class T> +struct LockableType : public T, public CCriticalSection +{ + bool hadSomethingRemoved; +}; + +typedef LockableType<std::vector<void*>> PlayerCallbackList; +typedef LockableType<std::vector<XBMCAddon::xbmc::Monitor*>> MonitorCallbackList; +typedef LockableType<std::vector<PyElem>> PyList; +typedef std::vector<LibraryLoader*> PythonExtensionLibraries; + +class XBPython : public IPlayerCallback, + public ANNOUNCEMENT::IAnnouncer, + public ILanguageInvocationHandler +{ +public: + XBPython(); + ~XBPython() override; + void OnPlayBackEnded() override; + void OnPlayBackStarted(const CFileItem& file) override; + void OnAVStarted(const CFileItem& file) override; + void OnAVChange() override; + void OnPlayBackPaused() override; + void OnPlayBackResumed() override; + void OnPlayBackStopped() override; + void OnPlayBackError() override; + void OnPlayBackSpeedChanged(int iSpeed) override; + void OnPlayBackSeek(int64_t iTime, int64_t seekOffset) override; + void OnPlayBackSeekChapter(int iChapter) override; + void OnQueueNextItem() override; + + void Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) override; + void RegisterPythonPlayerCallBack(IPlayerCallback* pCallback); + void UnregisterPythonPlayerCallBack(IPlayerCallback* pCallback); + void RegisterPythonMonitorCallBack(XBMCAddon::xbmc::Monitor* pCallback); + void UnregisterPythonMonitorCallBack(XBMCAddon::xbmc::Monitor* pCallback); + void OnSettingsChanged(const std::string& strings); + void OnScreensaverActivated(); + void OnScreensaverDeactivated(); + void OnDPMSActivated(); + void OnDPMSDeactivated(); + void OnScanStarted(const std::string& library); + void OnScanFinished(const std::string& library); + void OnCleanStarted(const std::string& library); + void OnCleanFinished(const std::string& library); + void OnNotification(const std::string& sender, + const std::string& method, + const std::string& data); + + void Process() override; + void PulseGlobalEvent() override; + void Uninitialize() override; + bool OnScriptInitialized(ILanguageInvoker* invoker) override; + void OnScriptStarted(ILanguageInvoker* invoker) override; + void NotifyScriptAborting(ILanguageInvoker* invoker) override; + void OnExecutionEnded(ILanguageInvoker* invoker) override; + void OnScriptFinalized(ILanguageInvoker* invoker) override; + ILanguageInvoker* CreateInvoker() override; + + bool WaitForEvent(CEvent& hEvent, unsigned int milliseconds); + +private: + static bool m_bInitialized; // whether global python runtime was already initialized + + CCriticalSection m_critSection; + void* m_mainThreadState{nullptr}; + int m_iDllScriptCounter{0}; // to keep track of the total scripts running that need the dll + + //Vector with list of threads used for running scripts + PyList m_vecPyList; + PlayerCallbackList m_vecPlayerCallbackList; + MonitorCallbackList m_vecMonitorCallbackList; + + // any global events that scripts should be using + CEvent m_globalEvent; + + // in order to finalize and unload the python library, need to save all the extension libraries that are + // loaded by it and unload them first (not done by finalize) + PythonExtensionLibraries m_extensions; +}; diff --git a/xbmc/interfaces/python/preamble.h b/xbmc/interfaces/python/preamble.h new file mode 100644 index 0000000..080a4fb --- /dev/null +++ b/xbmc/interfaces/python/preamble.h @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#ifdef SWIGPYTHON + +#include <Python.h> + +#endif diff --git a/xbmc/interfaces/python/pythreadstate.h b/xbmc/interfaces/python/pythreadstate.h new file mode 100644 index 0000000..2cae5a7 --- /dev/null +++ b/xbmc/interfaces/python/pythreadstate.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + + +//WARNING: since this will unlock/lock the python global interpreter lock, +// it will not work recursively + +//this is basically a scoped version of a Py_BEGIN_ALLOW_THREADS .. Py_END_ALLOW_THREADS block +class CPyThreadState +{ + public: + explicit CPyThreadState(bool save = true) + { + m_threadState = NULL; + + if (save) + Save(); + } + + ~CPyThreadState() + { + Restore(); + } + + void Save() + { + if (!m_threadState) + m_threadState = PyEval_SaveThread(); //same as Py_BEGIN_ALLOW_THREADS + } + + void Restore() + { + if (m_threadState) + { + PyEval_RestoreThread(m_threadState); //same as Py_END_ALLOW_THREADS + m_threadState = NULL; + } + } + + private: + PyThreadState* m_threadState; +}; + +/** + * A std::unique_lock<CCriticalSection> that will relinquish the GIL during the time + * it takes to obtain the CriticalSection + */ +class GilSafeSingleLock : public CPyThreadState, public std::unique_lock<CCriticalSection> +{ +public: + explicit GilSafeSingleLock(CCriticalSection& critSec) + : CPyThreadState(true), std::unique_lock<CCriticalSection>(critSec) + { + CPyThreadState::Restore(); + } +}; + diff --git a/xbmc/interfaces/python/swig.cpp b/xbmc/interfaces/python/swig.cpp new file mode 100644 index 0000000..0c49f87 --- /dev/null +++ b/xbmc/interfaces/python/swig.cpp @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "swig.h" + +#include "LanguageHook.h" +#include "interfaces/legacy/AddonString.h" +#include "utils/StringUtils.h" + +#include <string> + +namespace PythonBindings +{ + TypeInfo::TypeInfo(const std::type_info& ti) : swigType(NULL), parentType(NULL), typeIndex(ti) + { + static PyTypeObject py_type_object_header = { + PyVarObject_HEAD_INIT(nullptr, 0) 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, +#if PY_VERSION_HEX > 0x03080000 + 0, + 0, +#endif +#if PY_VERSION_HEX < 0x03090000 + 0, +#endif +#if PY_VERSION_HEX >= 0x030C00A1 + 0, +#endif + }; + + static int size = (long*)&(py_type_object_header.tp_name) - (long*)&py_type_object_header; + memcpy(&(this->pythonType), &py_type_object_header, size); + } + + class PyObjectDecrementor + { + PyObject* obj; + public: + inline explicit PyObjectDecrementor(PyObject* pyobj) : obj(pyobj) {} + inline ~PyObjectDecrementor() { Py_XDECREF(obj); } + + inline PyObject* get() { return obj; } + }; + + void PyXBMCGetUnicodeString(std::string& buf, PyObject* pObject, bool coerceToString, + const char* argumentName, const char* methodname) + { + // It's okay for a string to be "None". In this case the buf returned + // will be the emptyString. + if (pObject == Py_None) + { + buf = XBMCAddon::emptyString; + return; + } + + //! @todo UTF-8: Does python use UTF-16? + //! Do we need to convert from the string charset to UTF-8 + //! for non-unicode data? + if (PyUnicode_Check(pObject)) + { + // Python unicode objects are UCS2 or UCS4 depending on compilation + // options, wchar_t is 16-bit or 32-bit depending on platform. + // Avoid the complexity by just letting python convert the string. + + buf = PyUnicode_AsUTF8(pObject); + return; + } + + if (PyBytes_Check(pObject)) // If pobject is of type Bytes + { + buf = PyBytes_AsString(pObject); + return; + } + + // if we got here then we need to coerce the value to a string + if (coerceToString) + { + PyObjectDecrementor dec(PyObject_Str(pObject)); + PyObject* pyStrCast = dec.get(); + if (pyStrCast) + { + PyXBMCGetUnicodeString(buf,pyStrCast,false,argumentName,methodname); + return; + } + } + + // Object is not a unicode or a normal string. + buf = ""; + throw XBMCAddon::WrongTypeException("argument \"%s\" for method \"%s\" must be unicode or str", argumentName, methodname); + } + + // need to compare the typestring + bool isParameterRightType(const char* passedType, const char* expectedType, const char* methodNamespacePrefix, bool tryReverse) + { + if (strcmp(expectedType,passedType) == 0) + return true; + + // well now things are a bit more complicated. We need to see if the passed type + // is a subset of the overall type + std::string et(expectedType); + bool isPointer = (et[0] == 'p' && et[1] == '.'); + std::string baseType(et,(isPointer ? 2 : 0)); // this may contain a namespace + + std::string ns(methodNamespacePrefix); + // cut off trailing '::' + if (ns.size() > 2 && ns[ns.size() - 1] == ':' && ns[ns.size() - 2] == ':') + ns = ns.substr(0,ns.size()-2); + + bool done = false; + while(! done) + { + done = true; + + // now we need to see if the expected type can be munged + // into the passed type by tacking on the namespace of + // of the method. + std::string check(isPointer ? "p." : ""); + check += ns; + check += "::"; + check += baseType; + + if (strcmp(check.c_str(),passedType) == 0) + return true; + + // see if the namespace is nested. + int posOfScopeOp = ns.find("::"); + if (posOfScopeOp >= 0) + { + done = false; + // cur off the outermost namespace + ns = ns.substr(posOfScopeOp + 2); + } + } + + // so far we applied the namespace to the expected type. Now lets try + // the reverse if we haven't already. + if (tryReverse) + return isParameterRightType(expectedType, passedType, methodNamespacePrefix, false); + + return false; + } + + PythonToCppException::PythonToCppException() : XbmcCommons::UncheckedException(" ") + { + setClassname("PythonToCppException"); + + std::string msg; + std::string type, value, traceback; + if (!ParsePythonException(type, value, traceback)) + UncheckedException::SetMessage("Strange: No Python exception occurred"); + else + SetMessage(type, value, traceback); + } + + PythonToCppException::PythonToCppException(const std::string &exceptionType, const std::string &exceptionValue, const std::string &exceptionTraceback) : XbmcCommons::UncheckedException(" ") + { + setClassname("PythonToCppException"); + + SetMessage(exceptionType, exceptionValue, exceptionTraceback); + } + + bool PythonToCppException::ParsePythonException(std::string &exceptionType, std::string &exceptionValue, std::string &exceptionTraceback) + { + PyObject* exc_type; + PyObject* exc_value; + PyObject* exc_traceback; + PyObject* pystring = NULL; + + PyErr_Fetch(&exc_type, &exc_value, &exc_traceback); + if (exc_type == NULL && exc_value == NULL && exc_traceback == NULL) + return false; + + // See https://docs.python.org/3/c-api/exceptions.html#c.PyErr_NormalizeException + PyErr_NormalizeException(&exc_type, &exc_value, &exc_traceback); + if (exc_traceback != NULL) { + PyException_SetTraceback(exc_value, exc_traceback); + } + + exceptionType.clear(); + exceptionValue.clear(); + exceptionTraceback.clear(); + + if (exc_type != NULL && (pystring = PyObject_Str(exc_type)) != NULL && PyUnicode_Check(pystring)) + { + const char* str = PyUnicode_AsUTF8(pystring); + if (str != NULL) + exceptionType = str; + + pystring = PyObject_Str(exc_value); + if (pystring != NULL) + { + str = PyUnicode_AsUTF8(pystring); + exceptionValue = str; + } + + PyObject *tracebackModule = PyImport_ImportModule("traceback"); + if (tracebackModule != NULL) + { + char method[] = "format_exception"; + char format[] = "OOO"; + PyObject *tbList = PyObject_CallMethod(tracebackModule, method, format, exc_type, exc_value == NULL ? Py_None : exc_value, exc_traceback == NULL ? Py_None : exc_traceback); + + if (tbList) + { + PyObject* emptyString = PyUnicode_FromString(""); + char method[] = "join"; + char format[] = "O"; + PyObject *strRetval = PyObject_CallMethod(emptyString, method, format, tbList); + Py_DECREF(emptyString); + + if (strRetval) + { + str = PyUnicode_AsUTF8(strRetval); + if (str != NULL) + exceptionTraceback = str; + Py_DECREF(strRetval); + } + Py_DECREF(tbList); + } + Py_DECREF(tracebackModule); + + } + } + + Py_XDECREF(exc_type); + Py_XDECREF(exc_value); + Py_XDECREF(exc_traceback); + Py_XDECREF(pystring); + + return true; + } + + void PythonToCppException::SetMessage(const std::string &exceptionType, const std::string &exceptionValue, const std::string &exceptionTraceback) + { + std::string msg = "-->Python callback/script returned the following error<--\n"; + msg += " - NOTE: IGNORING THIS CAN LEAD TO MEMORY LEAKS!\n"; + + if (!exceptionType.empty()) + { + msg += StringUtils::Format("Error Type: {}\n", exceptionType); + + if (!exceptionValue.empty()) + msg += StringUtils::Format("Error Contents: {}\n", exceptionValue); + + if (!exceptionTraceback.empty()) + msg += exceptionTraceback; + + msg += "-->End of Python script error report<--\n"; + } + else + msg += "<unknown exception type>"; + + UncheckedException::SetMessage("%s", msg.c_str()); + } + + XBMCAddon::AddonClass* doretrieveApiInstance(const PyHolder* pythonObj, const TypeInfo* typeInfo, const char* expectedType, + const char* methodNamespacePrefix, const char* methodNameForErrorString) + { + if (pythonObj->magicNumber != XBMC_PYTHON_TYPE_MAGIC_NUMBER) + throw XBMCAddon::WrongTypeException("Non api type passed to \"%s\" in place of the expected type \"%s.\"", + methodNameForErrorString, expectedType); + if (!isParameterRightType(typeInfo->swigType,expectedType,methodNamespacePrefix)) + { + // maybe it's a child class + if (typeInfo->parentType) + return doretrieveApiInstance(pythonObj, typeInfo->parentType,expectedType, + methodNamespacePrefix, methodNameForErrorString); + else + throw XBMCAddon::WrongTypeException("Incorrect type passed to \"%s\", was expecting a \"%s\" but received a \"%s\"", + methodNameForErrorString,expectedType,typeInfo->swigType); + } + return const_cast<XBMCAddon::AddonClass*>(pythonObj->pSelf); + } + + /** + * This method is a helper for the generated API. It's called prior to any API + * class constructor being returned from the generated code to Python + */ + void prepareForReturn(XBMCAddon::AddonClass* c) + { + XBMC_TRACE; + if(c) { + c->Acquire(); + PyThreadState* state = PyThreadState_Get(); + XBMCAddon::Python::PythonLanguageHook::GetIfExists(state->interp)->RegisterAddonClassInstance(c); + } + } + + static bool handleInterpRegistrationForClean(XBMCAddon::AddonClass* c) + { + XBMC_TRACE; + if(c){ + XBMCAddon::AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook> lh = + XBMCAddon::AddonClass::Ref<XBMCAddon::AddonClass>(c->GetLanguageHook()); + + if (lh.isNotNull()) + { + lh->UnregisterAddonClassInstance(c); + return true; + } + else + { + PyThreadState* state = PyThreadState_Get(); + lh = XBMCAddon::Python::PythonLanguageHook::GetIfExists(state->interp); + if (lh.isNotNull()) lh->UnregisterAddonClassInstance(c); + return true; + } + } + return false; + } + + /** + * This method is a helper for the generated API. It's called prior to any API + * class destructor being dealloc-ed from the generated code from Python + */ + void cleanForDealloc(XBMCAddon::AddonClass* c) + { + XBMC_TRACE; + if (handleInterpRegistrationForClean(c)) + c->Release(); + } + + /** + * This method is a helper for the generated API. It's called prior to any API + * class destructor being dealloc-ed from the generated code from Python + * + * There is a Catch-22 in the destruction of a Window. 'dispose' needs to be + * called on destruction but cannot be called from the destructor. + * This overrides the default cleanForDealloc to resolve that. + */ + void cleanForDealloc(XBMCAddon::xbmcgui::Window* c) + { + XBMC_TRACE; + if (handleInterpRegistrationForClean(c)) + { + c->dispose(); + c->Release(); + } + } + + /** + * This method allows for conversion of the native api Type to the Python type. + * + * When this form of the call is used (and pytype isn't NULL) then the + * passed type is used in the instance. This is for classes that extend API + * classes in python. The type passed may not be the same type that's stored + * in the class metadata of the AddonClass of which 'api' is an instance, + * it can be a subclass in python. + * + * if pytype is NULL then the type is inferred using the class metadata + * stored in the AddonClass instance 'api'. + */ + PyObject* makePythonInstance(XBMCAddon::AddonClass* api, PyTypeObject* pytype, bool incrementRefCount) + { + // null api types result in Py_None + if (!api) + { + Py_INCREF(Py_None); + return Py_None; + } + + // retrieve the TypeInfo from the api class + const TypeInfo* typeInfo = getTypeInfoForInstance(api); + PyTypeObject* typeObj = pytype == NULL ? const_cast<PyTypeObject*>(&(typeInfo->pythonType)) : pytype; + + PyHolder* self = reinterpret_cast<PyHolder*>(typeObj->tp_alloc(typeObj,0)); + if (!self) return NULL; + self->magicNumber = XBMC_PYTHON_TYPE_MAGIC_NUMBER; + self->typeInfo = typeInfo; + self->pSelf = api; + if (incrementRefCount) + Py_INCREF((PyObject*)self); + return (PyObject*)self; + } + + std::map<std::type_index, const TypeInfo*> typeInfoLookup; + + void registerAddonClassTypeInformation(const TypeInfo* classInfo) + { + typeInfoLookup[classInfo->typeIndex] = classInfo; + } + + const TypeInfo* getTypeInfoForInstance(XBMCAddon::AddonClass* obj) + { + std::type_index ti(typeid(*obj)); + return typeInfoLookup[ti]; + } + + int dummy_tp_init(PyObject* self, PyObject* args, PyObject* kwds) + { + return 0; + } +} + diff --git a/xbmc/interfaces/python/swig.h b/xbmc/interfaces/python/swig.h new file mode 100644 index 0000000..353d968 --- /dev/null +++ b/xbmc/interfaces/python/swig.h @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/legacy/AddonClass.h" +#include "interfaces/legacy/Exception.h" +#include "interfaces/legacy/Window.h" + +#include <stdint.h> +#include <string> +#include <typeindex> + +#include <Python.h> + +namespace PythonBindings +{ + /** + * This call will convert the python object passed to a string. The object + * passed must be a python str or unicode object unless coerceToString is + * true. If coerceToString is true then the type must be castable to a string + * using the python call str(pObject). + * + * This method will handle a 'None' that's passed in. If 'None' is passed then + * the resulting buf will contain the value of XBMCAddon::emptyString (which + * is simply a std::string instantiated with the default constructor. + */ + void PyXBMCGetUnicodeString(std::string& buf, PyObject* pObject, bool coerceToString = false, + const char* pos = "unknown", + const char* methodname = "unknown"); + + struct TypeInfo + { + const char* swigType; + TypeInfo* parentType; + PyTypeObject pythonType; + const std::type_index typeIndex; + + explicit TypeInfo(const std::type_info& ti); + }; + + // This will hold the pointer to the api type, whether known or unknown + struct PyHolder + { + PyObject_HEAD + int32_t magicNumber; + const TypeInfo* typeInfo; + XBMCAddon::AddonClass* pSelf; + }; + +#define XBMC_PYTHON_TYPE_MAGIC_NUMBER 0x58626D63 + + /** + * This method retrieves the pointer from the PyHolder. The return value should + * be cast to the appropriate type. + * + * Since the calls to this are generated there's no NULL pointer checks + */ + inline XBMCAddon::AddonClass* retrieveApiInstance(PyObject* pythonObj, const TypeInfo* typeToCheck, + const char* methodNameForErrorString, + const char* typenameForErrorString) + { + if (pythonObj == NULL || pythonObj == Py_None) + return NULL; + if (reinterpret_cast<PyHolder*>(pythonObj)->magicNumber != XBMC_PYTHON_TYPE_MAGIC_NUMBER || !PyObject_TypeCheck(pythonObj, const_cast<PyTypeObject*>((&(typeToCheck->pythonType))))) + throw XBMCAddon::WrongTypeException("Incorrect type passed to \"%s\", was expecting a \"%s\".",methodNameForErrorString,typenameForErrorString); + return reinterpret_cast<PyHolder*>(pythonObj)->pSelf; + } + + bool isParameterRightType(const char* passedType, const char* expectedType, const char* methodNamespacePrefix, bool tryReverse = true); + + XBMCAddon::AddonClass* doretrieveApiInstance(const PyHolder* pythonObj, const TypeInfo* typeInfo, const char* expectedType, + const char* methodNamespacePrefix, const char* methodNameForErrorString); + + /** + * This method retrieves the pointer from the PyHolder. The return value should + * be cast to the appropriate type. + * + * Since the calls to this are generated there's no NULL pointer checks + * + * This method will return NULL if either the pythonObj is NULL or the + * pythonObj is Py_None. + */ + inline XBMCAddon::AddonClass* retrieveApiInstance(const PyObject* pythonObj, const char* expectedType, const char* methodNamespacePrefix, + const char* methodNameForErrorString) + { + return (pythonObj == NULL || pythonObj == Py_None) ? NULL : + doretrieveApiInstance(reinterpret_cast<const PyHolder*>(pythonObj),reinterpret_cast<const PyHolder*>(pythonObj)->typeInfo, expectedType, methodNamespacePrefix, methodNameForErrorString); + } + + /** + * This method is a helper for the generated API. It's called prior to any API + * class constructor being returned from the generated code to Python + */ + void prepareForReturn(XBMCAddon::AddonClass* c); + + /** + * This method is a helper for the generated API. It's called prior to any API + * class destructor being dealloc-ed from the generated code from Python + */ + void cleanForDealloc(XBMCAddon::AddonClass* c); + + /** + * This method is a helper for the generated API. It's called prior to any API + * class destructor being dealloc-ed from the generated code from Python + * + * There is a Catch-22 in the destruction of a Window. 'dispose' needs to be + * called on destruction but cannot be called from the destructor. + * This overrides the default cleanForDealloc to resolve that. + */ + void cleanForDealloc(XBMCAddon::xbmcgui::Window* c); + + /** + * This method allows for conversion of the native api Type to the Python type. + * + * When this form of the call is used (and pythonType isn't NULL) then the + * passed type is used in the instance. This is for classes that extend API + * classes in python. The type passed may not be the same type that's stored + * in the class metadata of the AddonClass of which 'api' is an instance, + * it can be a subclass in python. + * + * if pythonType is NULL then the type is inferred using the class metadata + * stored in the AddonClass instance 'api'. + */ + PyObject* makePythonInstance(XBMCAddon::AddonClass* api, PyTypeObject* pythonType, bool incrementRefCount); + + /** + * This method allows for conversion of the native api Type to the Python type. + * + * When this form of the call is used then the python type constructed will be the + * type given by the class metadata in the AddonClass instance 'api'. + * + * This is just a helper inline to call the other makePythonInstance with NULL as + * the pythonType. + */ + inline PyObject* makePythonInstance(XBMCAddon::AddonClass* api, bool incrementRefCount) + { + return makePythonInstance(api,NULL,incrementRefCount); + } + + void registerAddonClassTypeInformation(const TypeInfo* classInfo); + const TypeInfo* getTypeInfoForInstance(XBMCAddon::AddonClass* obj); + + int dummy_tp_init(PyObject* self, PyObject* args, PyObject* kwds); + + class Director + { + protected: + PyObject* self; + public: + inline Director() : self(NULL) {} + inline void setPyObjectForDirector(PyObject* pyargself) { self = pyargself; } + }; + + /** + * This exception is thrown from Director calls that call into python when the + * Python error is + */ + class PythonToCppException : public XbmcCommons::UncheckedException + { + public: + /** + * Assuming a PyErr_Occurred, this will fill the exception message with all + * of the appropriate information including the traceback if it can be + * obtained. It will also clear the python message. + */ + PythonToCppException(); + PythonToCppException(const std::string &exceptionType, const std::string &exceptionValue, const std::string &exceptionTraceback); + + static bool ParsePythonException(std::string &exceptionType, std::string &exceptionValue, std::string &exceptionTraceback); + + protected: + void SetMessage(const std::string &exceptionType, const std::string &exceptionValue, const std::string &exceptionTraceback); + }; + + template<class T> struct PythonCompare + { + static inline int compare(PyObject* obj1, PyObject* obj2, const char* swigType, const char* methodNamespacePrefix, const char* methodNameForErrorString) + { + XBMC_TRACE; + try + { + T* o1 = (T*)retrieveApiInstance(obj1, swigType, methodNamespacePrefix, methodNameForErrorString); + T* o2 = (T*)retrieveApiInstance(obj2, swigType, methodNamespacePrefix, methodNameForErrorString); + + return ((*o1) < (*o2) ? -1 : + ((*o1) > (*o2) ? 1 : 0)); + } + catch (const XBMCAddon::WrongTypeException& e) + { + CLog::Log(LOGERROR, "EXCEPTION: {}", e.GetExMessage()); + PyErr_SetString(PyExc_RuntimeError, e.GetExMessage()); + } + return -1; + } + }; +} diff --git a/xbmc/interfaces/python/test/CMakeLists.txt b/xbmc/interfaces/python/test/CMakeLists.txt new file mode 100644 index 0000000..ec38a51 --- /dev/null +++ b/xbmc/interfaces/python/test/CMakeLists.txt @@ -0,0 +1,5 @@ +if(PYTHON_FOUND) + set(SOURCES TestSwig.cpp) + + core_add_test_library(python_test) +endif() diff --git a/xbmc/interfaces/python/test/TestSwig.cpp b/xbmc/interfaces/python/test/TestSwig.cpp new file mode 100644 index 0000000..463bb98 --- /dev/null +++ b/xbmc/interfaces/python/test/TestSwig.cpp @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "../swig.h" + +#include <gtest/gtest.h> + +using namespace PythonBindings; + +TEST(TestSwig, TypeConversion) +{ + EXPECT_TRUE(isParameterRightType("p.XBMCAddon::xbmcgui::ListItem","p.XBMCAddon::xbmcgui::ListItem","XBMCAddon::xbmc::")); + EXPECT_TRUE(isParameterRightType("p.XBMCAddon::xbmc::PlayList","p.PlayList","XBMCAddon::xbmc::")); + EXPECT_TRUE(isParameterRightType("p.PlayList","p.XBMCAddon::xbmc::PlayList","XBMCAddon::xbmc::")); +} + diff --git a/xbmc/interfaces/python/typemaps/python.Alternative.intm b/xbmc/interfaces/python/typemaps/python.Alternative.intm new file mode 100644 index 0000000..12a5e65 --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.Alternative.intm @@ -0,0 +1,43 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + boolean ispointer = swigTypeParser.SwigType_ispointer(ltype) + String accessor = ispointer ? '->' : '.' + int seq = sequence.increment() + altAccess = [ 'former', 'later' ] + altSwitch = [ 'first', 'second' ] + + List types = swigTypeParser.SwigType_templateparmlist(ltype) +%> + { + // we need to check the parameter type and see if it matches + PyObject *pyentry_${seq} = ${slarg}; + try + { + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(types[0]))} entry0_${seq}; + ${helper.getInConversion(types[0], 'entry0' + '_' + seq, 'pyentry' + '_' + seq, method, + [ 'sequence' : sequence ])} + ${api}${accessor}${altAccess[0]}() = entry0_${seq}; + } + catch (const XBMCAddon::WrongTypeException&) + { + try + { + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(types[1]))} entry1_${seq}; + ${helper.getInConversion(types[1], 'entry1' + '_' + seq, 'pyentry' + '_' + seq, method, + [ 'sequence' : sequence ])} + ${api}${accessor}${altAccess[1]}() = entry1_${seq}; + } + catch (const XBMCAddon::WrongTypeException&) + { + throw XBMCAddon::WrongTypeException("Failed to convert to input type to either a " + "${swigTypeParser.SwigType_ltype(types[0])} or a " + "${swigTypeParser.SwigType_ltype(types[1])}" ); + } + } + }
\ No newline at end of file diff --git a/xbmc/interfaces/python/typemaps/python.Alternative.outtm b/xbmc/interfaces/python/typemaps/python.Alternative.outtm new file mode 100644 index 0000000..31f4205 --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.Alternative.outtm @@ -0,0 +1,34 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + List types = swigTypeParser.SwigType_templateparmlist(type) + boolean ispointer = swigTypeParser.SwigType_ispointer(type) + int seq = sequence.increment() + String accessor = ispointer ? '->' : '.' + altAccess = [ 'former', 'later' ] + altSwitch = [ 'first', 'second' ] +%> + WhichAlternative pos = ${api}${accessor}which(); + + if (<%if (ispointer) { %>${api} != NULL && <%}%>pos != XBMCAddon::none) + { <% + types.eachWithIndex { curType, entryIndex -> +%> + if (pos == XBMCAddon::${altSwitch[entryIndex]}) + { + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_lrtype(curType))} entry${seq} = ${api}${accessor}${altAccess[entryIndex]}(); + { + ${helper.getOutConversion(curType,result,method,[ 'api' : 'entry' + seq, 'sequence' : sequence ])} + } + } +<% + } +%> + } + else + ${result} = Py_None;
\ No newline at end of file diff --git a/xbmc/interfaces/python/typemaps/python.Tuple.intm b/xbmc/interfaces/python/typemaps/python.Tuple.intm new file mode 100644 index 0000000..c426856 --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.Tuple.intm @@ -0,0 +1,35 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + List types = swigTypeParser.SwigType_templateparmlist(ltype) + boolean ispointer = swigTypeParser.SwigType_ispointer(type) + String accessor = ispointer ? '->' : '.' + int seq = sequence.increment() + tupleAccess = [ 'first', 'second', 'third', 'fourth' ] +%> + if(${slarg}) + { + bool isTuple = PyObject_TypeCheck(${slarg},&PyTuple_Type); + if (!isTuple && !PyObject_TypeCheck(${slarg},&PyList_Type)) + throw WrongTypeException("The parameter \"${api}\" must be either a Tuple or a List."); + Py_ssize_t vecSize = (isTuple ? PyTuple_Size(${slarg}) : PyList_Size(${slarg})); +<% + types.eachWithIndex { curType, entryIndex -> +%> + if (vecSize > ${entryIndex}) + { + PyObject *pyentry${entryIndex}_${seq} = NULL; + pyentry${entryIndex}_${seq} = (isTuple ? PyTuple_GetItem(${slarg}, ${entryIndex}) : PyList_GetItem(${slarg}, ${entryIndex})); + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(curType))} entry${entryIndex}_${seq}; + ${helper.getInConversion(curType, 'entry' + entryIndex + '_' + seq, 'pyentry' + entryIndex + '_' + seq, method,[ 'sequence' : sequence ])} + ${api}${accessor}${tupleAccess[entryIndex]}() = entry${entryIndex}_${seq}; + } +<% + } +%> + } diff --git a/xbmc/interfaces/python/typemaps/python.Tuple.outtm b/xbmc/interfaces/python/typemaps/python.Tuple.outtm new file mode 100644 index 0000000..18655fe --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.Tuple.outtm @@ -0,0 +1,39 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + List types = swigTypeParser.SwigType_templateparmlist(type) + boolean ispointer = swigTypeParser.SwigType_ispointer(type) + int seq = sequence.increment() + String accessor = ispointer ? '->' : '.' + tupleAccess = [ 'first', 'second', 'third', 'fourth' ] +%> + int vecSize = ${api}${accessor}GetNumValuesSet(); + ${result} = PyTuple_New(vecSize); +<% + if (ispointer) + { +%> + if (${api} != NULL) +<% } // this ends the if (ispointer) +%> { + PyObject* pyentry${seq}; <% + types.eachWithIndex { curType, entryIndex -> +%> + + if (vecSize > ${entryIndex}) + { + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_lrtype(curType))} entry${seq} = ${api}${accessor}${tupleAccess[entryIndex]}(); + { + ${helper.getOutConversion(curType,'result',method,[ 'result' : 'pyentry' + seq, 'api' : 'entry' + seq, 'sequence' : sequence ])} + } + PyTuple_SetItem(${result}, ${entryIndex}, pyentry${seq}); + } +<% + } +%> + }
\ No newline at end of file diff --git a/xbmc/interfaces/python/typemaps/python.buffer.intm b/xbmc/interfaces/python/typemaps/python.buffer.intm new file mode 100644 index 0000000..f074b2b --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.buffer.intm @@ -0,0 +1,36 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +%> + if (PyUnicode_Check(${slarg})) + { + Py_ssize_t pysize; + const char* str = PyUnicode_AsUTF8AndSize(${slarg}, &pysize); + size_t size = static_cast<size_t>(pysize); + ${api}.allocate(size); + ${api}.put(str, size); + ${api}.flip(); // prepare the buffer for reading from + } + else if (PyBytes_Check(${slarg})) + { + Py_ssize_t pysize = PyBytes_GET_SIZE(${slarg}); + const char* str = PyBytes_AS_STRING(${slarg}); + size_t size = static_cast<size_t>(pysize); + ${api}.allocate(size); + ${api}.put(str, size); + ${api}.flip(); // prepare the buffer for reading from + } + else if (PyByteArray_Check(${slarg})) + { + size_t size = PyByteArray_Size(${slarg}); + ${api}.allocate(size); + ${api}.put(PyByteArray_AsString(${slarg}),size); + ${api}.flip(); // prepare the buffer for reading from + } + else + throw XBMCAddon::WrongTypeException("argument \"%s\" for \"%s\" must be a string, bytes or a bytearray", "${api}", "${method.@name}");
\ No newline at end of file diff --git a/xbmc/interfaces/python/typemaps/python.buffer.outtm b/xbmc/interfaces/python/typemaps/python.buffer.outtm new file mode 100644 index 0000000..8e38d81 --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.buffer.outtm @@ -0,0 +1,11 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + boolean ispointer = swigTypeParser.SwigType_ispointer(type) + String accessor = ispointer ? '->' : '.' +%>${result} = PyByteArray_FromStringAndSize((char*)${api}${accessor}curPosition(),${api}${accessor}remaining());
\ No newline at end of file diff --git a/xbmc/interfaces/python/typemaps/python.dict.intm b/xbmc/interfaces/python/typemaps/python.dict.intm new file mode 100644 index 0000000..ea3c78c --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.dict.intm @@ -0,0 +1,23 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + List templateArgs = swigTypeParser.SwigType_templateparmlist(ltype) + valtype = templateArgs[0] +%> + { + PyObject *pykey, *pyvalue; + Py_ssize_t pos = 0; + while(PyDict_Next(${slarg}, &pos, &pykey, &pyvalue)) + { + std::string key; + PyXBMCGetUnicodeString(key,pykey,false,"${api}","${method.@name}"); + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(valtype))} value; + ${helper.getInConversion(valtype, 'value', 'pyvalue' ,method)} + ${api}.emplace(std::move(key), std::move(value)); + } + }
\ No newline at end of file diff --git a/xbmc/interfaces/python/typemaps/python.map.intm b/xbmc/interfaces/python/typemaps/python.map.intm new file mode 100644 index 0000000..095d23b --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.map.intm @@ -0,0 +1,24 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + List templateArgs = swigTypeParser.SwigType_templateparmlist(ltype) + keytype = templateArgs[0] + valtype = templateArgs[1] +%> + { + PyObject *pykey, *pyvalue; + Py_ssize_t pos = 0; + while(PyDict_Next(${slarg}, &pos, &pykey, &pyvalue)) + { + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(keytype))} key; + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(valtype))} value; + ${helper.getInConversion(keytype, 'key', 'pykey', method)} + ${helper.getInConversion(valtype, 'value', 'pyvalue' ,method)} + ${api}.emplace(std::move(key), std::move(value)); + } + }
\ No newline at end of file diff --git a/xbmc/interfaces/python/typemaps/python.smart_ptr.outtm b/xbmc/interfaces/python/typemaps/python.smart_ptr.outtm new file mode 100644 index 0000000..0d7fa31 --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.smart_ptr.outtm @@ -0,0 +1,14 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + itype = swigTypeParser.SwigType_templateparmlist(type)[0] + pointertype = swigTypeParser.SwigType_makepointer(itype) + int seq = sequence.increment() +%> + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(pointertype))} entry${seq} = ${api}.get(); + ${helper.getOutConversion(pointertype,'result',method,[ 'api' : 'entry' + seq, 'sequence' : sequence ])} diff --git a/xbmc/interfaces/python/typemaps/python.string.outtm b/xbmc/interfaces/python/typemaps/python.string.outtm new file mode 100644 index 0000000..f9eb068 --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.string.outtm @@ -0,0 +1,11 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +%>${result} = <% + if(method.@feature_python_strictUnicode) { %>PyUnicode_DecodeUTF8(${api}.c_str(),${api}.size(),"strict");<% } + else { %>PyUnicode_DecodeUTF8(${api}.c_str(),${api}.size(),"surrogateescape");<% } %> diff --git a/xbmc/interfaces/python/typemaps/python.vector.intm b/xbmc/interfaces/python/typemaps/python.vector.intm new file mode 100644 index 0000000..6bc71ec --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.vector.intm @@ -0,0 +1,36 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + List templateArgs = swigTypeParser.SwigType_templateparmlist(ltype) + vectype = templateArgs[0] + boolean ispointer = swigTypeParser.SwigType_ispointer(type) + String accessor = ispointer ? '->' : '.' + int seq = sequence.increment() +%> + if (${slarg}) + { + bool isTuple = PyObject_TypeCheck(${slarg},&PyTuple_Type); + if (!isTuple && !PyObject_TypeCheck(${slarg},&PyList_Type)) + throw WrongTypeException("The parameter \"${api}\" must be either a Tuple or a List."); + + <% if (ispointer) print("${api} = new std::vector<${swigTypeParser.SwigType_str(vectype)}>();") %> + PyObject *pyentry${seq} = NULL; + Py_ssize_t vecSize = (isTuple ? PyTuple_Size(${slarg}) : PyList_Size(${slarg})); + ${api}${accessor}reserve(vecSize); + for(Py_ssize_t i = 0; i < vecSize; i++) + { + pyentry${seq} = (isTuple ? PyTuple_GetItem(${slarg}, i) : PyList_GetItem(${slarg}, i)); + ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(vectype))} entry${seq}; + ${helper.getInConversion(vectype, 'entry' + seq, 'pyentry' + seq, method, + [ 'type' : vectype, + 'ltype' : swigTypeParser.SwigType_ltype(vectype), + 'sequence' : sequence + ])} + ${api}${accessor}push_back(<% if (swigTypeParser.SwigType_ispointer(vectype) || vectype in ["bool", "double", "int"]) { %>entry${seq}<% } else { %>std::move(entry${seq})<% } %>); + } + } diff --git a/xbmc/interfaces/python/typemaps/python.vector.outtm b/xbmc/interfaces/python/typemaps/python.vector.outtm new file mode 100644 index 0000000..c1c4c79 --- /dev/null +++ b/xbmc/interfaces/python/typemaps/python.vector.outtm @@ -0,0 +1,35 @@ +<% +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + List templateArgs = swigTypeParser.SwigType_templateparmlist(type) + vectype = templateArgs[0] + boolean ispointer = swigTypeParser.SwigType_ispointer(type) + String accessor = ispointer ? '->' : '.' + seq = sequence.increment() + + if (ispointer) + { +%> + if (${api} != NULL) + { +<% } %> + ${result} = PyList_New(0); + + for (std::vector<${swigTypeParser.SwigType_str(vectype)}>::iterator iter = ${api}${accessor}begin(); iter != ${api}${accessor}end(); ++iter) + { + PyObject* pyentry${seq}; + ${helper.getOutConversion(vectype,'result',method,[ 'result' : 'pyentry' + seq, 'api' : '(*iter)', 'sequence' : sequence ])} + PyList_Append(${result}, pyentry${seq}); + Py_DECREF(pyentry${seq}); + } +<% + if (ispointer) + { +%> + } +<% } %> |