Skip to content

Commit 736a0b7

Browse files
v-einhoffstadt
authored andcommitted
fix (callbacks): A large rework of the callback mechanism that fixes unbalanced INCREF/DECREF calls and prevents leaks and some other issues #2036
1 parent 8369d7c commit 736a0b7

35 files changed

+753
-1325
lines changed

src/dearpygui_commands.h

Lines changed: 112 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -2006,19 +2006,13 @@ set_frame_callback(PyObject* self, PyObject* args, PyObject* kwargs)
20062006
&frame, &callback, &user_data))
20072007
return GetPyNone();
20082008

2009+
std::lock_guard<std::recursive_mutex> lk(GContext->mutex);
2010+
20092011
if (frame > GContext->callbackRegistry->highestFrame)
20102012
GContext->callbackRegistry->highestFrame = frame;
20112013

2012-
// TODO: check previous entry and deprecate if existing
2013-
Py_XINCREF(callback);
2014-
2015-
if(user_data)
2016-
Py_XINCREF(user_data);
2017-
mvSubmitCallback([=]()
2018-
{
2019-
GContext->callbackRegistry->frameCallbacks[frame] = callback;
2020-
GContext->callbackRegistry->frameCallbacksUserData[frame] = user_data;
2021-
});
2014+
GContext->callbackRegistry->frameCallbacks.insert_or_assign(frame, mvPyObject(callback, true));
2015+
GContext->callbackRegistry->frameCallbacksUserData.insert_or_assign(frame, mvPyObject(user_data, true));
20222016

20232017
return GetPyNone();
20242018
}
@@ -2033,14 +2027,9 @@ set_exit_callback(PyObject* self, PyObject* args, PyObject* kwargs)
20332027
&user_data))
20342028
return GetPyNone();
20352029

2036-
Py_XINCREF(callback);
2037-
if(user_data)
2038-
Py_XINCREF(user_data);
2039-
mvSubmitCallback([=]()
2040-
{
2041-
GContext->callbackRegistry->onCloseCallback = SanitizeCallback(callback);
2042-
GContext->callbackRegistry->onCloseCallbackUserData = user_data;
2043-
});
2030+
*GContext->callbackRegistry->onCloseCallback = mvPyObject(callback == Py_None? nullptr : callback, true);
2031+
*GContext->callbackRegistry->onCloseCallbackUserData = mvPyObject(user_data, true);
2032+
20442033
return GetPyNone();
20452034
}
20462035

@@ -2054,17 +2043,8 @@ set_viewport_resize_callback(PyObject* self, PyObject* args, PyObject* kwargs)
20542043
&callback, &user_data))
20552044
return GetPyNone();
20562045

2057-
if (callback)
2058-
Py_XINCREF(callback);
2059-
2060-
if (user_data)
2061-
Py_XINCREF(user_data);
2062-
2063-
mvSubmitCallback([=]()
2064-
{
2065-
GContext->callbackRegistry->resizeCallback = SanitizeCallback(callback);
2066-
GContext->callbackRegistry->resizeCallbackUserData = user_data;
2067-
});
2046+
*GContext->callbackRegistry->resizeCallback = mvPyObject(callback == Py_None? nullptr : callback, true);
2047+
*GContext->callbackRegistry->resizeCallbackUserData = mvPyObject(user_data, true);
20682048

20692049
return GetPyNone();
20702050
}
@@ -2527,14 +2507,26 @@ output_frame_buffer(PyObject* self, PyObject* args, PyObject* kwargs)
25272507

25282508
if (filepathLength == 0 && callback) // not specified, return array instead
25292509
{
2530-
//Py_XINCREF(callback);
25312510
PyObject* newbuffer = nullptr;
25322511
PymvBuffer* newbufferview = PyObject_New(PymvBuffer, &PymvBufferType);
25332512
newbuffer = PyObject_Init((PyObject*)newbufferview, &PymvBufferType);
2534-
mvSubmitTask([newbuffer, callback, newbufferview]() {
2513+
2514+
// Making an owned ref while we're still holding GIL (can't do this within mvSubmitTask).
2515+
auto stored_callback = std::make_shared<mvPyObject>(callback, true);
2516+
// We need to schedule this into the rendering thread because OutputFrameBufferArray
2517+
// accesses the rendering API, which might well have thread-local things in the context.
2518+
mvSubmitTask([stored_callback, newbuffer, newbufferview]() {
25352519
OutputFrameBufferArray(newbufferview);
2536-
mvAddCallback(callback, 0, newbuffer, nullptr, false);
2537-
});
2520+
mvAddOwnerlessCallback(
2521+
stored_callback, std::make_shared<mvPyObject>(nullptr),
2522+
0, "",
2523+
// Note: the callback queue will DECREF the value returned by this
2524+
// lambda, effectively deleting `newbuffer` so that we don't need
2525+
// to perform any special cleanup. We just pass the value as it is,
2526+
// keeping a refcount of 1 all the time until the callback is done.
2527+
[=]() { return newbuffer; }
2528+
);
2529+
});
25382530

25392531
return GetPyNone();
25402532
}
@@ -2571,7 +2563,7 @@ output_frame_buffer(PyObject* self, PyObject* args, PyObject* kwargs)
25712563
static PyObject*
25722564
is_dearpygui_running(PyObject* self, PyObject* args, PyObject* kwargs)
25732565
{
2574-
return ToPyBool(GContext->started);
2566+
return ToPyBool(GContext->running);
25752567
}
25762568

25772569
static PyObject*
@@ -2591,6 +2583,7 @@ setup_dearpygui(PyObject* self, PyObject* args, PyObject* kwargs)
25912583
while (!GContext->itemRegistry->containers.empty())
25922584
GContext->itemRegistry->containers.pop();
25932585
GContext->started = true;
2586+
GContext->running = true;
25942587
GContext->future = std::async(std::launch::async, []() {return mvRunCallbacks(); });
25952588
Py_END_ALLOW_THREADS;
25962589
return GetPyNone();
@@ -2646,67 +2639,73 @@ create_context(PyObject* self, PyObject* args, PyObject* kwargs)
26462639
static PyObject*
26472640
destroy_context(PyObject* self, PyObject* args, PyObject* kwargs)
26482641
{
2649-
// std::lock_guard<std::recursive_mutex> lk(GContext->mutex);
2650-
2651-
Py_BEGIN_ALLOW_THREADS;
2652-
26532642
if (GContext == nullptr)
26542643
{
26552644
assert(false);
26562645
}
26572646

26582647
else
26592648
{
2660-
// hacky fix, started was set to false
2661-
// to exit the event loop, but needs to be
2662-
// true in order to run DPG commands for the
2663-
// exit callback.
2664-
GContext->started = true;
2665-
mvSubmitCallback([=]() {
2666-
mvRunCallback(GContext->callbackRegistry->onCloseCallback, 0, nullptr, GContext->callbackRegistry->onCloseCallbackUserData);
2667-
GContext->started = false; // return to false after
2668-
});
2669-
2670-
if (GContext->viewport != nullptr)
2671-
mvCleanupViewport(*GContext->viewport);
2672-
2673-
ImNodes::DestroyContext();
2674-
ImPlot::DestroyContext();
2675-
ImGui::DestroyContext();
2649+
// Make sure everyone knows we're shutting down, even if stop_dearpygui
2650+
// was not called.
2651+
StopRendering();
26762652

2677-
mvToolManager::Reset();
2678-
ClearItemRegistry(*GContext->itemRegistry);
2653+
Py_BEGIN_ALLOW_THREADS;
26792654

2655+
// Queue the close callback, if any. The environment is still healthy enough
2656+
// for it to run, except that no more frames will be rendered with the current GContext.
2657+
mvAddOwnerlessCallback(GContext->callbackRegistry->onCloseCallback, GContext->callbackRegistry->onCloseCallbackUserData);
26802658

2681-
2682-
//#define X(el) el::s_class_theme_component = nullptr; el::s_class_theme_disabled_component = nullptr;
2683-
#define X(el) DearPyGui::GetClassThemeComponent(mvAppItemType::el) = nullptr; DearPyGui::GetDisabledClassThemeComponent(mvAppItemType::el) = nullptr;
2684-
MV_ITEM_TYPES
2685-
#undef X
2686-
2687-
mvSubmitCallback([=]() {
2659+
// Shutting down the callback loop - this will run right after the close callback
2660+
mvSubmitCallback([]() {
26882661
GContext->callbackRegistry->running = false;
2689-
});
2662+
}, true);
2663+
// Waiting for it to complete all tasks and shut down
26902664
if (GContext->future.valid())
26912665
GContext->future.get();
2692-
if (GContext->viewport)
2693-
delete GContext->viewport;
26942666

2695-
delete GContext->itemRegistry;
2696-
delete GContext->callbackRegistry;
2697-
delete GContext;
2698-
GContext = nullptr;
2667+
// The rest of cleanup must be done with GIL locked because it might lead
2668+
// to occasional DECREFs on Python objects (e.g. a callback or user_data).
2669+
Py_END_ALLOW_THREADS;
2670+
2671+
mvContext* context_to_delete = nullptr;
2672+
{
2673+
// Even though the handlers thread is down, there's still a chance that
2674+
// the user calls DPG from another Python thread. We'd better lock the
2675+
// mutex while we're tinkering with all the global structures.
2676+
std::lock_guard<std::recursive_mutex> lk(GContext->mutex);
2677+
2678+
mvToolManager::Reset();
2679+
ClearItemRegistry(*GContext->itemRegistry);
2680+
2681+
ImNodes::DestroyContext();
2682+
ImPlot::DestroyContext();
2683+
ImGui::DestroyContext();
2684+
2685+
#define X(el) DearPyGui::GetClassThemeComponent(mvAppItemType::el) = nullptr; DearPyGui::GetDisabledClassThemeComponent(mvAppItemType::el) = nullptr;
2686+
MV_ITEM_TYPES
2687+
X(All)
2688+
#undef X
2689+
2690+
if (GContext->viewport)
2691+
delete GContext->viewport;
2692+
2693+
delete GContext->itemRegistry;
2694+
delete GContext->callbackRegistry;
2695+
context_to_delete = GContext;
2696+
GContext = nullptr;
2697+
}
2698+
delete context_to_delete;
26992699
}
2700-
Py_END_ALLOW_THREADS;
27012700

27022701
return GetPyNone();
27032702
}
27042703

27052704
static PyObject*
27062705
stop_dearpygui(PyObject* self, PyObject* args, PyObject* kwargs)
27072706
{
2708-
std::lock_guard<std::recursive_mutex> lk(GContext->mutex);
2709-
GContext->started = false;
2707+
StopRendering();
2708+
std::lock_guard<std::recursive_mutex> lk(GContext->mutex);
27102709
auto viewport = GContext->viewport;
27112710
if (viewport)
27122711
viewport->running = false;
@@ -3771,37 +3770,17 @@ get_item_configuration(PyObject* self, PyObject* args, PyObject* kwargs)
37713770
PyDict_SetItemString(pdict, "height", py_height);
37723771
PyDict_SetItemString(pdict, "indent", py_indent);
37733772

3774-
if (appitem->config.callback)
3775-
{
3776-
Py_XINCREF(appitem->config.callback);
3777-
PyDict_SetItemString(pdict, "callback", appitem->config.callback);
3778-
}
3779-
else
3780-
PyDict_SetItemString(pdict, "callback", GetPyNone());
3773+
PyObject* callback = appitem->config.callback;
3774+
PyDict_SetItemString(pdict, "callback", callback? callback : Py_None);
37813775

3782-
if (appitem->config.dropCallback)
3783-
{
3784-
Py_XINCREF(appitem->config.dropCallback);
3785-
PyDict_SetItemString(pdict, "drop_callback", appitem->config.dropCallback);
3786-
}
3787-
else
3788-
PyDict_SetItemString(pdict, "drop_callback", GetPyNone());
3776+
PyObject* dropCallback = appitem->config.dropCallback;
3777+
PyDict_SetItemString(pdict, "drop_callback", dropCallback? dropCallback : Py_None);
37893778

3790-
if (appitem->config.dragCallback)
3791-
{
3792-
Py_XINCREF(appitem->config.dragCallback);
3793-
PyDict_SetItemString(pdict, "drag_callback", appitem->config.dragCallback);
3794-
}
3795-
else
3796-
PyDict_SetItemString(pdict, "drag_callback", GetPyNone());
3779+
PyObject* dragCallback = appitem->config.dragCallback;
3780+
PyDict_SetItemString(pdict, "drag_callback", dragCallback? dragCallback : Py_None);
37973781

3798-
if (appitem->config.user_data)
3799-
{
3800-
Py_XINCREF(appitem->config.user_data);
3801-
PyDict_SetItemString(pdict, "user_data", appitem->config.user_data);
3802-
}
3803-
else
3804-
PyDict_SetItemString(pdict, "user_data", GetPyNone());
3782+
PyObject* user_data = *(appitem->config.user_data);
3783+
PyDict_SetItemString(pdict, "user_data", user_data? user_data : Py_None);
38053784

38063785
appitem->getSpecificConfiguration(pdict);
38073786
}
@@ -4229,21 +4208,8 @@ capture_next_item(PyObject* self, PyObject* args, PyObject* kwargs)
42294208

42304209
std::lock_guard<std::recursive_mutex> lk(GContext->mutex);
42314210

4232-
if (GContext->itemRegistry->captureCallback)
4233-
Py_XDECREF(GContext->itemRegistry->captureCallback);
4234-
4235-
if (GContext->itemRegistry->captureCallbackUserData)
4236-
Py_XDECREF(GContext->itemRegistry->captureCallbackUserData);
4237-
4238-
Py_XINCREF(callable);
4239-
if(user_data)
4240-
Py_XINCREF(user_data);
4241-
if (callable == Py_None)
4242-
GContext->itemRegistry->captureCallback = nullptr;
4243-
else
4244-
GContext->itemRegistry->captureCallback = callable;
4245-
4246-
GContext->itemRegistry->captureCallbackUserData = user_data;
4211+
GContext->itemRegistry->captureCallback = mvPyObject(callable == Py_None? nullptr : callable, true);
4212+
GContext->itemRegistry->captureCallbackUserData = mvPyObject(user_data, true);
42474213

42484214
return GetPyNone();
42494215
}
@@ -4254,29 +4220,44 @@ get_callback_queue(PyObject* self, PyObject* args, PyObject* kwargs)
42544220
if (GContext->callbackRegistry->jobs.empty())
42554221
return GetPyNone();
42564222

4223+
std::lock_guard<std::recursive_mutex> lk(GContext->mutex);
4224+
42574225
PyObject* pArgs = PyTuple_New(GContext->callbackRegistry->jobs.size());
42584226
for (int i = 0; i < GContext->callbackRegistry->jobs.size(); i++)
42594227
{
42604228
PyObject* job = PyTuple_New(4);
4261-
if (GContext->callbackRegistry->jobs[i].callback)
4262-
PyTuple_SetItem(job, 0, GContext->callbackRegistry->jobs[i].callback);
4263-
else
4264-
PyTuple_SetItem(job, 0, GetPyNone());
4229+
const mvCallbackJob& cur_entry = GContext->callbackRegistry->jobs[i];
42654230

4266-
if(GContext->callbackRegistry->jobs[i].sender == 0)
4267-
PyTuple_SetItem(job, 1, ToPyString(GContext->callbackRegistry->jobs[i].sender_str));
4231+
PyObject* callback;
4232+
if (cur_entry.ownerless_callback)
4233+
{
4234+
callback = *cur_entry.ownerless_callback;
4235+
Py_XINCREF(callback);
4236+
}
42684237
else
4269-
PyTuple_SetItem(job, 1, ToPyUUID(GContext->callbackRegistry->jobs[i].sender));
4238+
{
4239+
auto liveOwner = cur_entry.owner.lock();
4240+
// If the owner of this entry is gone, we'll just set the callback to None.
4241+
// This lets us create the output list right away, without the need to collect
4242+
// valid callbacks first. Also, this mimicks the behavior of widgets without
4243+
// a `callback` set on them, which in the "manual" mode put null callbacks into
4244+
// the queue. It's mostly a debug/diagnostic mode anyway - captures everything.
4245+
callback = liveOwner? cur_entry.callback : nullptr;
4246+
// Must only be done while we own liveOwner.
4247+
Py_XINCREF(callback);
4248+
}
4249+
PyTuple_SetItem(job, 0, callback? callback : GetPyNone());
42704250

4271-
if (GContext->callbackRegistry->jobs[i].app_data)
4272-
PyTuple_SetItem(job, 2, GContext->callbackRegistry->jobs[i].app_data); // steals data, so don't deref
4273-
else
4274-
PyTuple_SetItem(job, 2, GetPyNone());
4251+
PyTuple_SetItem(job, 1, ToPyUUID(cur_entry.sender, cur_entry.alias));
42754252

4276-
if (GContext->callbackRegistry->jobs[i].user_data)
4277-
PyTuple_SetItem(job, 3, GContext->callbackRegistry->jobs[i].user_data); // steals data, so don't deref
4278-
else
4279-
PyTuple_SetItem(job, 3, GetPyNone());
4253+
// app_data_func() returns a new PyObject reference (passing ownership to us),
4254+
// therefore we don't need to INCREF it.
4255+
PyObject* app_data = cur_entry.app_data_func();
4256+
PyTuple_SetItem(job, 2, app_data? app_data : GetPyNone());
4257+
4258+
PyObject* user_data = *cur_entry.user_data;
4259+
Py_XINCREF(user_data);
4260+
PyTuple_SetItem(job, 3, user_data? user_data : GetPyNone());
42804261

42814262
PyTuple_SetItem(pArgs, i, job);
42824263
}

0 commit comments

Comments
 (0)