McRogueFace/src/PyLock.cpp

100 lines
3.4 KiB
C++
Raw Normal View History

// #219 - Thread synchronization context manager for mcrfpy.lock()
#include "PyLock.h"
#include "GameEngine.h"
#include "Resources.h"
// Forward declarations
static PyObject* PyLockContext_enter(PyLockContextObject* self, PyObject* args);
static PyObject* PyLockContext_exit(PyLockContextObject* self, PyObject* args);
static void PyLockContext_dealloc(PyLockContextObject* self);
static PyObject* PyLockContext_new(PyTypeObject* type, PyObject* args, PyObject* kwds);
// Context manager methods
static PyMethodDef PyLockContext_methods[] = {
{"__enter__", (PyCFunction)PyLockContext_enter, METH_NOARGS,
"Acquire the frame lock, blocking until safe window opens"},
{"__exit__", (PyCFunction)PyLockContext_exit, METH_VARARGS,
"Release the frame lock"},
{NULL}
};
// Type definition
PyTypeObject PyLockContextType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy._LockContext",
.tp_basicsize = sizeof(PyLockContextObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyLockContext_dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "Thread synchronization context manager for safe UI updates from background threads",
.tp_methods = PyLockContext_methods,
.tp_new = PyLockContext_new,
};
static PyObject* PyLockContext_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyLockContextObject* self = (PyLockContextObject*)type->tp_alloc(type, 0);
if (self) {
self->acquired = false;
}
return (PyObject*)self;
}
static void PyLockContext_dealloc(PyLockContextObject* self) {
// Safety: if we're destroyed while holding the lock, release it
if (self->acquired && Resources::game) {
Resources::game->getFrameLock().release();
}
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject* PyLockContext_enter(PyLockContextObject* self, PyObject* args) {
if (!Resources::game) {
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
return NULL;
}
// #219 - If we're on the main thread, no-op (already synchronized)
// This allows the same code to work from callbacks, script init, AND background threads
if (Resources::game->isMainThread()) {
self->acquired = false; // Don't try to release what we didn't acquire
Py_INCREF(self);
return (PyObject*)self;
}
// Acquire the frame lock - this will block (with GIL released) until safe window
Resources::game->getFrameLock().acquire();
self->acquired = true;
// Return self for the `with` statement
Py_INCREF(self);
return (PyObject*)self;
}
static PyObject* PyLockContext_exit(PyLockContextObject* self, PyObject* args) {
// args contains (exc_type, exc_val, exc_tb) - we don't suppress exceptions
if (self->acquired && Resources::game) {
Resources::game->getFrameLock().release();
self->acquired = false;
}
// Return False to not suppress any exceptions
Py_RETURN_FALSE;
}
// The lock() function that users call - returns a new context manager
PyObject* PyLock::lock(PyObject* self, PyObject* args) {
if (!Resources::game) {
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
return NULL;
}
// Create and return a new context manager object
return PyObject_CallObject((PyObject*)&PyLockContextType, NULL);
}
int PyLock::init() {
if (PyType_Ready(&PyLockContextType) < 0) {
return -1;
}
return 0;
}