16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-17 23:39:18 +02:00
Files
maszyna/scripting/PyInt.cpp
Mateusz Włodarczyk fd4c112436 scripting: make CWD-local packages importable in embedded Python
Embedded CPython builds sys.path from PYTHONHOME only. Unlike the python
CLI it does not prepend the working directory, and unlike Windows'
getpathp.c it does not add the executable's directory either, so on POSIX
"from scripts import ..." (the asset-side scripts package used by texture
and screen renderers) fails to import.

Insert "." at the front of sys.path after interpreter init, matching how
the engine resolves every other asset (relative to CWD). Idempotent and
harmless on Windows.
2026-07-06 20:07:08 +02:00

721 lines
20 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "scripting/PyInt.h"
#include "stdafx.h"
#include "application/application.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/dictionary.h"
#ifdef __GNUC__
#pragma GCC diagnostic ignored "-Wwrite-strings"
#endif
#include <simulation/simulation.h>
void render_task::run()
{
// convert provided input to a python dictionary
auto *input = PyDict_New();
if (input == nullptr)
{
cancel();
return;
}
for (auto const &datapair : m_input->floats)
{
auto *value{PyGetFloat(datapair.second)};
if (value == nullptr)
{
PyErr_Clear();
continue;
}
PyDict_SetItemString(input, datapair.first.c_str(), value);
Py_DECREF(value);
}
for (auto const &datapair : m_input->integers)
{
auto *value{PyLong_FromLong(datapair.second)};
if (value == nullptr)
{
PyErr_Clear();
continue;
}
PyDict_SetItemString(input, datapair.first.c_str(), value);
Py_DECREF(value);
}
for (auto const &datapair : m_input->bools)
{
// Py_True / Py_False sa niesmiertelne, ale PyDict_SetItemString i tak
// pobiera wlasna referencje - nie zwalniamy
auto *value{PyGetBool(datapair.second)};
PyDict_SetItemString(input, datapair.first.c_str(), value);
}
for (auto const &datapair : m_input->strings)
{
// Nazwy scenerii/wagonow (asName, SceneryFile, asCarName, cCode...) moga
// byc albo w UTF-8, albo w starym kodowaniu Windows-1250. PyUnicode_FromString
// wymaga POPRAWNEGO UTF-8 - dla nie-UTF-8 zwracalo NULL, a potem
// PyDict_SetItemString(dict, key, NULL) robilo Py_INCREF na NULL -> crash.
//
// Strategia: najpierw probujemy UTF-8 (strict). Jesli sie nie uda - probujemy
// cp1250. Jesli oba zawioda - pomijamy klucz, ale nie wywracamy symulatora.
char const *const str{datapair.second.c_str()};
Py_ssize_t const len{static_cast<Py_ssize_t>(datapair.second.size())};
auto *value{PyUnicode_DecodeUTF8(str, len, "strict")};
if (value == nullptr)
{
PyErr_Clear();
value = PyUnicode_Decode(str, len, "cp1250", "strict");
}
if (value == nullptr)
{
PyErr_Clear();
continue;
}
PyDict_SetItemString(input, datapair.first.c_str(), value);
Py_DECREF(value);
}
for (auto const &datapair : m_input->vec2_lists)
{
PyObject *list = PyList_New(datapair.second.size());
for (size_t i = 0; i < datapair.second.size(); i++)
{
auto const &vec = datapair.second[i];
WriteLog("passing " + glm::to_string(vec));
PyObject *tuple = PyTuple_New(2);
PyTuple_SetItem(tuple, 0, PyGetFloat(vec.x)); // steals ref
PyTuple_SetItem(tuple, 1, PyGetFloat(vec.y)); // steals ref
PyList_SetItem(list, i, tuple); // steals ref
}
PyDict_SetItemString(input, datapair.first.c_str(), list);
Py_DECREF(list);
}
m_input = nullptr;
// call the renderer
auto *output{PyObject_CallMethod(m_renderer, const_cast<char *>("render"), const_cast<char *>("O"), input)};
Py_DECREF(input);
if (output != nullptr)
{
auto *outputWidth = PyObject_CallMethod(m_renderer, const_cast<char *>("get_width"), nullptr);
auto *outputHeight = PyObject_CallMethod(m_renderer, const_cast<char *>("get_height"), nullptr);
if (outputWidth != nullptr && outputHeight != nullptr && m_target != nullptr)
{
const int screenWidth = static_cast<int>(PyLong_AsLong(outputWidth));
const int screenHeight = static_cast<int>(PyLong_AsLong(outputHeight));
const bool useRgb = false && !Global.gfx_usegles;
const int glFormat = useRgb ? GL_SRGB8 : GL_SRGB8_ALPHA8;
const int glComponents = useRgb ? GL_RGB : GL_RGBA;
const size_t bytesPerPixel = useRgb ? 3u : 4u;
const size_t expectedBytes = static_cast<size_t>(screenWidth) * static_cast<size_t>(screenHeight) * bytesPerPixel;
Py_ssize_t pythonBufferBytes = 0;
char *pythonBufferPtr = nullptr;
const bool bufferExtracted = PyBytes_AsStringAndSize(output, &pythonBufferPtr, &pythonBufferBytes) == 0 && pythonBufferPtr != nullptr;
if (!bufferExtracted)
{
ErrorLog("Python screen renderer: output is not a valid byte buffer");
}
else if (pythonBufferBytes < static_cast<Py_ssize_t>(expectedBytes))
{
ErrorLog(std::format("Python screen renderer: output buffer too small ({} bytes, expected {})", pythonBufferBytes, expectedBytes));
}
else
{
std::lock_guard guard(m_target->mutex);
if (m_target->image.size() != expectedBytes)
m_target->image.resize(expectedBytes);
std::memcpy(m_target->image.data(), pythonBufferPtr, expectedBytes);
m_target->width = screenWidth;
m_target->height = screenHeight;
m_target->components = glComponents;
m_target->format = glFormat;
m_target->timestamp = std::chrono::high_resolution_clock::now();
}
}
if (outputHeight != nullptr)
Py_DECREF(outputHeight);
if (outputWidth != nullptr)
Py_DECREF(outputWidth);
Py_DECREF(output);
}
// get commands from renderer
auto *commandsPO = PyObject_CallMethod(m_renderer, const_cast<char *>("getCommands"), nullptr);
if (commandsPO != nullptr)
{
std::vector<std::string> commands = python_external_utils::PyObjectToStringArray(commandsPO);
Py_DECREF(commandsPO);
// we perform any actions ONLY when there are any commands in buffer
if (!commands.empty())
{
for (const auto &cmd : commands)
{
std::string baseCmd;
int p1 = 0, p2 = 0;
size_t pos1 = cmd.find(';');
if (pos1 == std::string::npos)
{
baseCmd = cmd;
}
else
{
baseCmd = cmd.substr(0, pos1);
size_t pos2 = cmd.find(';', pos1 + 1);
if (pos2 == std::string::npos)
{
p1 = std::stoi(cmd.substr(pos1 + 1));
}
else
{
p1 = std::stoi(cmd.substr(pos1 + 1, pos2 - pos1 - 1));
p2 = std::stoi(cmd.substr(pos2 + 1));
}
}
auto it = simulation::commandMap.find(baseCmd);
if (it != simulation::commandMap.end())
{
command_data cd;
cd.command = it->second;
cd.action = GLFW_PRESS;
cd.param1 = p1;
cd.param2 = p2;
WriteLog("Python: Executing command [" + baseCmd + "] with params: P1=" + std::to_string(p1) + " P2=" + std::to_string(p2) +
" Target ID=" + std::to_string(simulation::Train->id()));
simulation::Commands.push(cd, static_cast<size_t>(command_target::vehicle) | simulation::Train->id());
}
else
{
ErrorLog("Python: Command [" + baseCmd + "] not found!");
}
}
}
}
}
void render_task::upload()
{
if (Global.python_uploadmain && m_target && m_target->shared_tex)
{
m_target->shared_tex->update_from_memory(m_target->width, m_target->height, reinterpret_cast<const uint8_t *>(m_target->image.data()));
}
}
void render_task::cancel() {}
// initializes the module. returns true on success
auto python_taskqueue::init() -> bool
{
crashreport_add_info("python.threadedupload", Global.python_threadedupload ? "yes" : "no");
crashreport_add_info("python.uploadmain", Global.python_uploadmain ? "yes" : "no");
#ifdef _WIN32
const wchar_t *pythonhome = sizeof(void *) == 8 ? L"python64" : L"python";
#elif __linux__
const wchar_t *pythonhome = (sizeof(void *) == 8) ? L"linuxpython64" : L"linuxpython";
#elif __APPLE__
const wchar_t *pythonhome = (sizeof(void *) == 8) ? L"macpython64" : L"macpython";
#endif
{
// Py_SetPythonHome / Py_InitializeEx are deprecated since Python 3.11.
// Use the PyConfig init API (PEP 587) instead.
PyConfig config;
PyConfig_InitPythonConfig(&config);
config.install_signal_handlers = 0; // matches former Py_InitializeEx(0)
PyStatus status = PyConfig_SetString(&config, &config.home, pythonhome);
if (PyStatus_Exception(status))
{
PyConfig_Clear(&config);
ErrorLog("Python Interpreter: failed to set PYTHONHOME");
return false;
}
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status))
{
std::string msg = "Python Interpreter: Py_InitializeFromConfig failed";
if (status.err_msg != nullptr)
msg += std::string(": ") + status.err_msg;
if (status.func != nullptr)
msg += std::string(" (in ") + status.func + ")";
PyConfig_Clear(&config);
ErrorLog(msg);
return false;
}
PyConfig_Clear(&config);
}
// make CWD-local packages (e.g. the asset-side "scripts" package used by
// texture/screen renderers) importable. Embedded CPython builds sys.path
// from PYTHONHOME only; unlike the python CLI it does not prepend the
// working directory, and unlike Windows' getpathp.c it does not add the
// executable's directory either, so on POSIX "from scripts import ..."
// would otherwise fail. "." matches how the engine resolves all other
// assets (relative to CWD). Idempotent/harmless on Windows.
if (PyObject *syspath = PySys_GetObject("path")) // borrowed ref
{
PyObject *cwd = PyUnicode_FromString(".");
if (cwd != nullptr)
{
PyList_Insert(syspath, 0, cwd);
Py_DECREF(cwd);
}
}
PyObject *stringiomodule{nullptr};
PyObject *stringioclassname{nullptr};
PyObject *stringioobject{nullptr};
// do the setup work while we hold the lock
m_main = PyImport_ImportModule("__main__");
if (m_main == nullptr)
{
ErrorLog("Python Interpreter: __main__ module is missing");
goto release_and_exit;
}
stringiomodule = PyImport_ImportModule("io");
stringioclassname = stringiomodule != nullptr ? PyObject_GetAttrString(stringiomodule, "StringIO") : nullptr;
stringioobject = stringioclassname != nullptr ? PyObject_CallObject(stringioclassname, nullptr) : nullptr;
m_stderr = {(stringioobject == nullptr ? nullptr : PySys_SetObject(const_cast<char *>("stderr"), stringioobject) != 0 ? nullptr : stringioobject)};
if (false == run_file("abstractscreenrenderer"))
{
goto release_and_exit;
}
// release the lock, save the state for future use
m_mainthread = PyEval_SaveThread();
WriteLog("Python Interpreter: setup complete");
// init workers
for (auto &worker : m_workers)
{
GLFWwindow *openglcontextwindow = nullptr;
if (Global.python_threadedupload)
openglcontextwindow = Application.window(-1);
worker = std::jthread(&python_taskqueue::run, this, openglcontextwindow, std::ref(m_tasks), std::ref(m_uploadtasks), std::ref(m_condition), std::ref(m_exit));
if (false == worker.joinable())
{
return false;
}
}
m_initialized = true;
return true;
release_and_exit:
PyEval_SaveThread();
return false;
}
// shuts down the module
void python_taskqueue::exit()
{
if (!m_initialized)
return;
// let the workers know we're done with them
m_exit = true;
m_condition.notify_all();
// let them free up their shit before we proceed
m_workers = {};
// drop any pending render/upload tasks. the workers are joined at this
// point so the locks are strictly defensive, but they cost nothing and
// document intent. clearing the deque also actually releases the tasks,
// which the previous code did not do (cancel() is a no-op stub).
{
std::lock_guard<std::mutex> lock(m_tasks.mutex);
for (auto &task : m_tasks.data)
{
task->cancel();
}
m_tasks.data.clear();
}
{
std::lock_guard<std::mutex> lock(m_uploadtasks.mutex);
m_uploadtasks.data.clear();
}
// reclaim cached python objects while the interpreter is still alive,
// so no Py_DECREF lands on a finalized interpreter during later teardown
acquire_lock();
for (auto &entry : m_renderers)
{
Py_XDECREF(entry.second);
}
m_renderers.clear();
Py_XDECREF(m_stderr);
m_stderr = nullptr;
Py_XDECREF(m_main);
m_main = nullptr;
// take a bow
Py_Finalize();
m_initialized = false;
}
// adds specified task along with provided collection of data to the work queue. returns true on success
auto python_taskqueue::insert(task_request const &Task) -> bool
{
if (!m_initialized || false == Global.python_enabled || Task.renderer.empty() || Task.input == nullptr || Task.target == 0)
{
return false;
}
auto *renderer{fetch_renderer(Task.renderer)};
if (renderer == nullptr)
{
return false;
}
auto newtask = std::make_shared<render_task>(renderer, Task.input, Task.target);
bool newtaskinserted{false};
// acquire a lock on the task queue and add the new task
{
std::lock_guard<std::mutex> lock(m_tasks.mutex);
// check the task list for a pending request with the same target
for (auto &task : m_tasks.data)
{
if (task->target() == Task.target)
{
// replace pending task in the slot with the more recent one
task->cancel();
task = newtask;
newtaskinserted = true;
break;
}
}
if (false == newtaskinserted)
{
m_tasks.data.emplace_back(newtask);
}
}
// potentially wake a worker to handle the new task
m_condition.notify_one();
// all done
return true;
}
// executes python script stored in specified file. returns true on success
auto python_taskqueue::run_file(std::string const &File, std::string const &Path) -> bool
{
auto const lookup{FileExists({Path + File, "python/local/" + File}, {".py"})};
if (lookup.first.empty())
{
return false;
}
std::ifstream inputfile{lookup.first + lookup.second};
std::string input;
input.assign(std::istreambuf_iterator<char>(inputfile), std::istreambuf_iterator<char>());
if (PyRun_SimpleString(input.c_str()) != 0)
{
error();
return false;
}
return true;
}
// acquires the python gil and sets the main thread as current
void python_taskqueue::acquire_lock()
{
PyEval_RestoreThread(m_mainthread);
}
// releases the python gil and swaps the main thread out
void python_taskqueue::release_lock()
{
PyEval_SaveThread();
}
auto python_taskqueue::fetch_renderer(std::string const Renderer) -> PyObject *
{
auto const lookup{m_renderers.find(Renderer)};
if (lookup != std::end(m_renderers))
{
return lookup->second;
}
// try to load specified renderer class
std::string const path{substr_path(Renderer)};
auto const file{Renderer.substr(path.size())};
PyObject *renderer{nullptr};
PyObject *rendererarguments{nullptr};
PyObject *renderername{nullptr};
acquire_lock();
{
if (m_main == nullptr)
{
ErrorLog("Python Renderer: __main__ module is missing");
goto cache_and_return;
}
if (false == run_file(file, path))
{
goto cache_and_return;
}
renderername = PyObject_GetAttrString(m_main, file.c_str());
if (renderername == nullptr)
{
ErrorLog("Python Renderer: class \"" + file + "\" not defined");
goto cache_and_return;
}
rendererarguments = Py_BuildValue("(s)", path.c_str());
if (rendererarguments == nullptr)
{
ErrorLog("Python Renderer: failed to create initialization arguments");
goto cache_and_return;
}
renderer = PyObject_CallObject(renderername, rendererarguments);
PyObject_CallMethod(renderer, const_cast<char *>("manul_set_format"), const_cast<char *>("(s)"), "RGBA");
if (PyErr_Occurred() != nullptr)
{
error();
renderer = nullptr;
}
cache_and_return:
// clean up after yourself
if (rendererarguments != nullptr)
{
Py_DECREF(rendererarguments);
}
}
release_lock();
// cache the failures as well so we don't try again on subsequent requests
m_renderers.emplace(Renderer, renderer);
return renderer;
}
void python_taskqueue::run(GLFWwindow *Context, rendertask_sequence &Tasks, uploadtask_sequence &Upload_Tasks, threading::condition_variable &Condition, std::atomic<bool> &Exit)
{
if (Context)
glfwMakeContextCurrent(Context);
// create a state object for this thread
PyEval_AcquireThread(m_mainthread);
auto *threadstate{PyThreadState_New(m_mainthread->interp)};
PyEval_ReleaseThread(m_mainthread);
std::shared_ptr<render_task> task{nullptr};
while (false == Exit.load())
{
// regardless of the reason we woke up prime the spurious wakeup flag for the next time
Condition.spurious(true);
// keep working as long as there's any scheduled tasks
do
{
task = nullptr;
// acquire a lock on the task queue and potentially grab a task from it
{
std::lock_guard<std::mutex> lock(Tasks.mutex);
if (false == Tasks.data.empty())
{
// fifo
task = Tasks.data.front();
Tasks.data.pop_front();
}
}
if (task != nullptr)
{
// swap in my thread state
PyEval_RestoreThread(threadstate);
{
// execute python code
task->run();
if (Context)
task->upload();
else
{
std::lock_guard<std::mutex> lock(Upload_Tasks.mutex);
Upload_Tasks.data.push_back(task);
}
if (PyErr_Occurred() != nullptr)
error();
}
// clear the thread state
PyEval_SaveThread();
}
// TBD, TODO: add some idle time between tasks in case we're on a single thread cpu?
} while (task != nullptr);
// if there's nothing left to do wait until there is
// short timeout: notify_all() in exit() can race with the worker's
// transition into the wait, and the run loop also drives prompt
// shutdown checks
Condition.wait_for(std::chrono::milliseconds(250));
}
// clean up thread state data
// Re-acquire the GIL with this worker's thread state, then clear and delete it.
// PyThreadState_DeleteCurrent() frees the current thread state AND releases the GIL,
// so we must NOT call PyEval_SaveThread() afterwards: at that point the current
// thread state is already NULL and PyEval_SaveThread() fatals on a NULL tstate.
PyEval_RestoreThread(threadstate);
PyThreadState_Clear(threadstate);
PyThreadState_DeleteCurrent();
// detach the GL context before the worker terminates; some drivers
// (NVIDIA on X11, certain Mesa/Wayland configs) hang in process teardown
// if a context is still current on a dying thread
if (Context)
glfwMakeContextCurrent(nullptr);
}
void python_taskqueue::update()
{
std::lock_guard<std::mutex> lock(m_uploadtasks.mutex);
for (auto &task : m_uploadtasks.data)
task->upload();
m_uploadtasks.data.clear();
}
void python_taskqueue::error()
{
if (m_stderr != nullptr)
{
// std err pythona jest buforowane
PyErr_Print();
auto *errortext{PyObject_CallMethod(m_stderr, const_cast<char *>("getvalue"), nullptr)};
if (errortext != nullptr)
{
const char *errstr = PyUnicode_AsUTF8(errortext);
if (errstr != nullptr)
ErrorLog(errstr);
Py_DECREF(errortext);
}
PyObject_CallMethod(m_stderr, const_cast<char *>("truncate"), const_cast<char *>("L"), (long long)0);
PyObject_CallMethod(m_stderr, const_cast<char *>("seek"), const_cast<char *>("L"), (long long)0);
}
else
{
// nie dziala buffor pythona
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
if (type == nullptr)
{
ErrorLog("Python Interpreter: don't know how to handle null exception");
}
PyErr_NormalizeException(&type, &value, &traceback);
if (type == nullptr)
{
ErrorLog("Python Interpreter: don't know how to handle null exception");
}
auto *typetext{PyObject_Str(type)};
if (typetext != nullptr)
{
const char *s = PyUnicode_AsUTF8(typetext);
if (s)
ErrorLog(s);
Py_DECREF(typetext);
}
if (value != nullptr)
{
auto *valuetext{PyObject_Str(value)};
if (valuetext != nullptr)
{
const char *s = PyUnicode_AsUTF8(valuetext);
if (s)
ErrorLog(s);
Py_DECREF(valuetext);
}
}
auto *tracebacktext{PyObject_Str(traceback)};
if (tracebacktext != nullptr)
{
const char *s = PyUnicode_AsUTF8(tracebacktext);
if (s)
ErrorLog(s);
Py_DECREF(tracebacktext);
}
else
{
WriteLog("Python Interpreter: failed to retrieve the stack traceback");
}
}
}
std::vector<std::string> python_external_utils::PyObjectToStringArray(PyObject *pyList)
{
std::vector<std::string> result;
std::vector<std::string> emptyIfError = {};
if (!PySequence_Check(pyList))
{
ErrorLog("Python: Failed to convert PyObject -> vector<string>");
return emptyIfError;
}
Py_ssize_t size = PySequence_Size(pyList);
for (Py_ssize_t i = 0; i < size; ++i)
{
PyObject *item = PySequence_GetItem(pyList, i); // Increments reference count
if (item == nullptr)
{
ErrorLog("Python: Failed to get item from sequence.");
return emptyIfError;
}
const char *str = PyUnicode_AsUTF8(item);
if (str == nullptr)
{
Py_DECREF(item);
ErrorLog("Python: Failed to convert item to string.");
return emptyIfError;
}
result.push_back(std::string(str));
Py_DECREF(item); // Decrease reference count for the item
}
return result;
}
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif