Fix callback/timer GC: prevent premature destruction of Python callbacks
closes #251 Two related bugs where Python garbage collection destroyed callbacks that were still needed by live C++ objects: 1. **Drawable callbacks (all 8 types)**: tp_dealloc unconditionally called click_unregister() etc., destroying callbacks even when the C++ object was still alive in a parent's children vector. Fixed by guarding with shared_ptr::use_count() <= 1 — only unregister when the Python wrapper is the last owner. 2. **Timer GC prevention**: Active timers now hold a Py_INCREF'd reference to their Python wrapper (Timer::py_wrapper), preventing GC while the timer is registered in the engine. Released on stop(), one-shot fire, or destruction. mcrfpy.Timer("name", cb, 100) now works without storing the return value. Also includes audio synth demo UI fixes: button click handling (don't set on_click on Caption children), single-column slider layout, improved Animalese contrast. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
97dbec9106
commit
9718153709
15 changed files with 740 additions and 231 deletions
|
|
@ -415,7 +415,8 @@ inline PyTypeObject PyViewport3DType = {
|
|||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
if (obj->data) {
|
||||
// Only unregister callbacks if we're the last owner (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
|||
it->second->stop();
|
||||
}
|
||||
Resources::game->timers[self->name] = self->data;
|
||||
// Prevent Python GC while timer is active (#251)
|
||||
self->data->retainPyWrapper((PyObject*)self);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
@ -147,6 +149,8 @@ PyObject* PyTimer::start(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
|||
}
|
||||
|
||||
self->data->start(current_time);
|
||||
// Prevent Python GC while timer is active (#251)
|
||||
self->data->retainPyWrapper((PyObject*)self);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +222,8 @@ PyObject* PyTimer::restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
|||
}
|
||||
|
||||
self->data->restart(current_time);
|
||||
// Prevent Python GC while timer is active (#251)
|
||||
self->data->retainPyWrapper((PyObject*)self);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +322,8 @@ int PyTimer::set_active(PyTimerObject* self, PyObject* value, void* closure) {
|
|||
Resources::game->timers[self->name] = self->data;
|
||||
}
|
||||
self->data->start(current_time);
|
||||
// Prevent Python GC while timer is active (#251)
|
||||
self->data->retainPyWrapper((PyObject*)self);
|
||||
} else if (self->data->isPaused()) {
|
||||
// Resume from pause
|
||||
self->data->resume(current_time);
|
||||
|
|
|
|||
|
|
@ -15,11 +15,24 @@ Timer::Timer()
|
|||
{}
|
||||
|
||||
Timer::~Timer() {
|
||||
releasePyWrapper();
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
void Timer::retainPyWrapper(PyObject* wrapper) {
|
||||
if (py_wrapper == wrapper) return; // Already held
|
||||
Py_XDECREF(py_wrapper);
|
||||
py_wrapper = wrapper;
|
||||
Py_XINCREF(py_wrapper);
|
||||
}
|
||||
|
||||
void Timer::releasePyWrapper() {
|
||||
Py_XDECREF(py_wrapper);
|
||||
py_wrapper = nullptr;
|
||||
}
|
||||
|
||||
bool Timer::hasElapsed(int now) const
|
||||
{
|
||||
if (paused || stopped) return false;
|
||||
|
|
@ -71,6 +84,7 @@ bool Timer::test(int now)
|
|||
// Handle one-shot timers: stop but preserve callback for potential restart
|
||||
if (once) {
|
||||
stopped = true; // Will be removed from map by testTimers(), but callback preserved
|
||||
releasePyWrapper(); // Allow Python GC now (#251)
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -123,6 +137,7 @@ void Timer::stop()
|
|||
paused = false;
|
||||
pause_start_time = 0;
|
||||
total_paused_time = 0;
|
||||
releasePyWrapper(); // Allow Python GC now that timer is inactive (#251)
|
||||
}
|
||||
|
||||
bool Timer::isActive() const
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ public:
|
|||
uint64_t serial_number = 0; // For Python object cache
|
||||
std::string name; // Store name for creating Python wrappers (#180)
|
||||
|
||||
// Strong reference to Python wrapper prevents GC while timer is active (#251)
|
||||
PyObject* py_wrapper = nullptr;
|
||||
void retainPyWrapper(PyObject* wrapper);
|
||||
void releasePyWrapper();
|
||||
|
||||
Timer(); // for map to build
|
||||
Timer(PyObject* target, int interval, int now, bool once = false, bool start = true, const std::string& name = "");
|
||||
~Timer();
|
||||
|
|
|
|||
|
|
@ -122,7 +122,8 @@ namespace mcrfpydef {
|
|||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
if (obj->data) {
|
||||
// Only unregister callbacks if we're the last owner (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ namespace mcrfpydef {
|
|||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// Clear Python references to break cycles
|
||||
if (obj->data) {
|
||||
// Only unregister callbacks if we're the last owner (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ namespace mcrfpydef {
|
|||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
if (obj->data) {
|
||||
// Only unregister callbacks if we're the last owner (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
|
|
|
|||
|
|
@ -90,8 +90,10 @@ namespace mcrfpydef {
|
|||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// Clear Python references to break cycles
|
||||
if (obj->data) {
|
||||
// Only unregister callbacks if we're the last owner of the
|
||||
// C++ object. If other shared_ptr owners exist (e.g. parent's
|
||||
// children vector), the callbacks must survive. (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
|
|
|
|||
|
|
@ -269,8 +269,8 @@ namespace mcrfpydef {
|
|||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// Clear Python references to break cycles
|
||||
if (obj->data) {
|
||||
// Only unregister callbacks if we're the last owner (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ namespace mcrfpydef {
|
|||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
if (obj->data) {
|
||||
// Only unregister callbacks if we're the last owner (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ namespace mcrfpydef {
|
|||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// Clear Python references to break cycles
|
||||
if (obj->data) {
|
||||
// Only unregister callbacks if we're the last owner (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue