Crash when deleting a component while a slider is being dragged

This is easy to reproduce. I have a component that contains a slider. I drag the slider and while I’m dragging, I delete the component in the MessageManager thread, which calls the slider destructor. It will crash in Slider::Pimpl::sendDragEnd() when “this” becomes nullptr.

What I want to happen: force a stop to the dragging in the Slider, then delete the Slider immediately as normal.

If I can’t do that, then alternately: wait somehow, or set a flag, and then delete the component when the user stops dragging. This sounds complex and probably creates new bugs.

I’m stumped - any pointers would be appreciated.

I would just change its visibility. If you absolutely need to destroy it then, in a timer, i would check if: the slider is not nullptr & is not visible & is not dragging. Then, i’ll destroy it.

do you call removeChildComponent() before delete?

I had the thought that show/hide might be the preference. It is not intuitive in this case, as there are ~50 different components that could be displayed in this area, and only one is displayed at any given time. So, think of something like an NI Guitar Rig rack that has many different completely different virtual UI’s that are dynamically inserted/removed by the user. Allocating all 50 feels bad, but I guess I’m realizing that the rules are different with components and UI where a user is interacting, and some of the common programming guidelines like saving memory don’t apply.

I did exactly as you indicate - I used a timer, and Component::isMouseDownAnywhere() to brute force a solution, but still thinking about show/hide.

It is actually a small hierarchy of components that I am deleting. Now that I look, I am not doing removeChild on the top component - that is a great idea. Maybe that solves it (?). Will try this in a few hours.

Pointers would be appreciated? Pointers are the problem!
In C++ just you have to make sure not to delete stuff that are still in use

It seems that deleting the component removes the child from the parent automatically, so apparently it wouldn’t fix it. But if for some reason remove previously is necessary in your case, it would mean that it is not totally safe to delete the component in all cases without first removing it, as stated in the info.

Note that removing a child will not delete it! But it’s ok to delete a component without first removing it - doing so will automatically remove it and send out the appropriate notifications before the deletion completes.
JUCE: Component Class Reference

Another alternative would be to only remove the component, and later when it is safe, delete it. maybe checking for components that have no parent

Are you unregistering from being a juce::Slider::Listener before deleting the component?

I also thought it was related to listeners, but it isn’t. There are no registered listeners - just a basic slider in a parent component that is destroyed. Apparently sendDragEnd() is always called when you drag a slider, whether there are listeners or not.

Can you share the exact code that exposes the problm?
A possible scenario is, if you syncronously delete the slider while being in a callback, this has to fail.
But if you wrap the deleting call in a MessageManager::callAsync() it should be fine.

Hmmm, maybe I have misjudged that I am in the MessageManager thread. Here is the crash call stack:

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 AM 0x155b4e3ad juce::Slider::Pimpl::sendDragEnd() + 13
1 AM 0x155b576eb juce::Slider::Pimpl::~Pimpl() + 283
2 AM 0x155b5727e juce::Slider::Pimpl::~Pimpl() + 14
3 AM 0x155b4a9ae juce::Slider::~Slider() + 62
4 AM 0x155899a33 LTL_SB::~LTL_SB() + 2243
5 AM 0x155897eae LTL_SB::~LTL_SB() + 14
6 AM 0x15579be28 TitleComp::~TitleComp() + 168
7 AM 0x155777247 PanelComp::~PanelComp() + 151
8 AM 0x1558a9325 SinglePanelComp::~SinglePanelComp() + 117
9 AM 0x155750e10 MainEditor::refreshPanels() + 384
10 AM 0x155a27d86 juce::MessageQueue::deliverNextMessage() + 246
11 AM 0x155a27c3e juce::MessageQueue::runLoopSourceCallback(void*) + 14
12 CoreFoundation 0x7ff819633b78 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION + 17
13 CoreFoundation 0x7ff819633b27 __CFRunLoopDoSource0 + 157
14 CoreFoundation 0x7ff819633901 __CFRunLoopDoSources0 + 212
15 CoreFoundation 0x7ff81963257b __CFRunLoopRun + 929
16 CoreFoundation 0x7ff819631b60 CFRunLoopRunSpecific + 560
17 HIToolbox 0x7ff822f7f766 RunCurrentEventLoopInMode + 292
18 HIToolbox 0x7ff822f7f396 ReceiveNextEventCommon + 199
19 HIToolbox 0x7ff822f7f2b3 _BlockUntilNextEventMatchingListInModeWithFilter + 70
20 AppKit 0x7ff81c782293 _DPSNextEvent + 909
21 AppKit 0x7ff81c781114 -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1219
22 AppKit 0x7ff81c773757 -[NSApplication run] + 586
23 Cubase 11 0x10d5a09c8 0x10b42c000 + 35080648
24 Cubase 11 0x10d3bbb26 0x10b42c000 + 33094438
25 Cubase 11 0x10cf3862a 0x10b42c000 + 28362282
26 Cubase 11 0x10cf3c11b 0x10b42c000 + 28377371
27 dyld 0x7ff819225310 start + 2432

I know that says main thread, but the MessageQueue made me think it was MessageManager, and also there is no other thread in the crash dump that mentions Message.

Easy enough to add a callAsync if I have misunderstood that.

Adding callAsync doesn’t help.

Ah shame. FTR, my hypothesis wasn’t about a background thread. You are correct, the MessageQueue::deliverNextMessage is the MessageManager.

But if you trigger a delete in a callback and there are subsequent calls in that callback refering to this, they would have to fail.
By calling the callAsync, it would allow to finish the callback before the delete happens.

Looking into the mouseDown and the ~ScopedDragNotification(), there is a check if the slider wasn’t deleted. So it is weird that it still fails.
I have no further idea what’s going on.

Good luck figuring it out…

I went through this with custom widgets and calls from other threads, and callAsync didn’t help either. I made my own “async” that just uses a timer and “moves” the call on the message thread. I use it as a member of the class that has to destroy the custom widgets. I’m not sure if it’ll help :

	class EventAsync : private juce::Timer
	{
	public:

		EventAsync() {}

		~EventAsync()
		{
			stopTimer();
		}

		void set_event(std::function<void()> callback_init)
		{
			callback = callback_init;

			startTimerHz(25);
		}

	private:

		void timerCallback()
		{
			callback();

			stopTimer();
		}

		std::function<void()> callback = []() {};
	};

I ended up with

class Foo : public Timer
{

void actionListenerCallback(const String &message) override
{
  // functionThatDeletesSlider() previously called directly here, but now...
  startTimer(10);
}

void timerCallback() override
{
        if (!Component::isMouseButtonDownAnywhere())
        {
            functionThatDeletesSlider();
            stopTimer();
        }
}

This situation has a bit of a smell aspect … using timers to delete out of scope components, hmm …

One question: is this situation occurring in a Plugin type project, or JUCEApplication? If Plugin → AUv3, VST2: equivalent behaviour?

FWIW, I just tested, this doesn’t crash (in a GUI app):

MainComponent::MainComponent()
{
    slider = std::make_unique<juce::Slider> ( juce::Slider::LinearHorizontal, juce::Slider::NoTextBox );

    slider->onValueChange = [this]()
    {
        juce::MessageManager::callAsync ([this]()
        {
            if (slider && slider->getValue() > 5.0)
                slider.reset();
        });
    };
    
    addAndMakeVisible (slider.get());
    setSize (600, 400);
}

Look mom, no timer :wink:

1 Like