A friend was running my plugin in Logic X, on a swanky new M1 Macbook, and it crashed at shutdown. Normally I’d expect this to be my fault (it usually is!) but this appeared to be a crash firmly in JUCE code, in the TimerThread. This has always been rock solid for me in the past. I thought I’d post the crash log, in case anyone else has seen the same thing.
Thanks for reporting this issue. Looking at the crash log, my best guess is that the timer thread is trying to lock MessageQueue::messages after the queue has already been destroyed. I’m not sure how that could be happening though…
Are you calling stopTimer() in the destructors of all classes derived from Timer?
Have you seen the same thing in any other JUCE plugins, such as the AudioPluginDemo?
Is the plugin running as a native arm64 plugin, or emulated x86_64?
I’ll have a go at reproducing this myself, but I’d really appreciate some repro steps if you’re able to provide them.
I’ve had a go at reproducing this now without much luck. I tried modifying the AudioPluginDemo so that the AudioProcessor is a Timer with a high update rate. I’m then able to load up a few instances of the plugin in Logic and quit the app without causing a crash.
I’m really perplexed by this bug. The TimerThread should be halted in DeletedAtShutdown::deleteAll, which is called in shutdownJuce_GUI. shutdownJuce_GUI should halt the timer thread before destroying the MessageManager. I think that means the issue has a couple of potential causes:
A new timer is started during the call to DeletedAtShutdown::deleteAll, after the TimerThread has been destroyed for the first time. It looks like this would start up a new TimerThread which might outlive the MessageManager. This seems quite unlikely though!
The MessageManager instance is being destroyed in a separate location, before the call to shutdownJuce_GUI. This also seems unlikely…
It might be worth sticking some breakpoints or logging in the constructor/destructor of the TimerThread and MessageQueue and checking whether these objects are being constructed/destroyed more than once during the lifetime of the plugin.
Yes I second calling stopTimer(), although it doesn’t matter. The base class juce::Timer calls stopTimer() in the destructor.
If you didn’t call stopTimer() yourself you would get an assert firing.
The argument to have stopTimer() in the derived class is, so there cannot be a call during destruction (the baseclass still alive but the derived class implementing the virtual timerCallback() already destroyed.
But then this can only happen if the destruction happens on a thread which is not the message thread (otherwise the synchronous timerCallback() cannot happen at the same time). In this case you have already a race condition and you are in UB territory.