AudioDeviceManager Memory Leak (Related to MidiDeviceListConnection)

Summary

While trying various methods due to the inability to define AudioDeviceManager as a global variable, I ultimately discovered that AudioDeviceManager causes a memory leak. There might be something I’m missing.

Short Test Code

// include ...

//auto adm = std::make_unique<juce::AudioDeviceManager>(); //fail
std::unique_ptr<juce::AudioDeviceManager> adm; //success

int main()
{
	//adm = std::make_unique<juce::AudioDeviceManager>(); //JUCE_ASSERT_MESSAGE_THREAD!!!

	// <<MessageManager Preparation>>

	juce::MessageManager::callAsync([] {
		adm = std::make_unique<juce::AudioDeviceManager>(); //success
	});
	return 0;
} //but ~AudioDeviceManager fail also memory leak warning
// *** Leaked objects detected: 1 instance(s) of class CallbackHandler
// JUCE Assertion failure in memory/juce_LeakedObjectDetector.h:92
// ...other

Additional Test Code

// include ...

int main()
{
	// <<MessageManager Preparation>>

	juce::MessageManager::callAsync([] {
		juce::MidiDeviceListConnection midiDeviceListConnection = juce::MidiDeviceListConnection::make([] {
		});
	});
	return 0;
} //memory leak warning (see below)

Debug Output (LLDB)

JUCE v7.0.9
*** Leaked objects detected: 1 instance(s) of class MidiServiceType
JUCE Assertion failure in memory/juce_LeakedObjectDetector.h:92
JUCE Assertion failure in memory/juce_Singleton.h:50
Detected memory leaks!
Dumping objects ->
{408} normal block at 0x000001CBA4A297E0, 120 bytes long.
 Data: < V j            > 90 56 DB 6A F6 7F 00 00 00 00 00 00 00 00 00 00 
{402} normal block at 0x000001CBA4A28FA0, 120 bytes long.
 Data: < X j            > 80 58 DB 6A F6 7F 00 00 E0 97 A2 A4 CB 01 00 00 
Object dump complete.

Conclusion

I found out that the root cause of all these issues is the midiDeviceListConnection member function in the AudioDeviceManager class: Direct link to the git source
Commenting out this variable resolves all the above issues.

It seems necessary to address both the enforcement of creating and destroying the AudioDeviceManager on the message thread and the issue of memory leaks.
If these problems are arising due to incorrect usage of the functions, any advice would be greatly appreciated.
(It used to work fine in previous juce versions.)

Environment

juce: 7.0.9, clang, windows console

+++ Additionally, I have constructed a console-based message thread with the following class. However, declaring it as a global variable also causes issues. Could I get some advice on this?

class MainMessageThread : public juce::Thread
{
public:
	MainMessageThread() : Thread("MessageThread")
	{
		startThread();
	}
	~MainMessageThread() override
	{
		auto mm = juce::MessageManager::getInstanceWithoutCreating();
		if (!mm) return;
		mm->stopDispatchLoop();
		stopThread(1000);
	}
	void run() override
	{
		auto mm = std::unique_ptr<juce::MessageManager>(juce::MessageManager::getInstance());
		notify();
		mm->runDispatchLoop();
		notify();
	}
};

IIRC, you and to use juce::DeletedAtShutdown to ensure that things are cleaned up before the JUCE leak detector runs.

Also, something not related to any issues, your usage of notify in your run () method doesn’t do anything. Thread::notify () would be called from another thread context to wake up a thread which has called Thread::wait ().

I have read the related documentation (DeletedAtShutdown) and it seems that juce::DeletedAtShutdown is used in singleton classes by inheriting from this class. However, MidiDeviceListConnection is a class implemented in JUCE and seems to be a different case. What I mean is that there appears to be a bug in the MidiDeviceListConnection class, which does not properly clean up its internal objects even after its destructor is called. Thank you for your response.

+++Regarding the additional question about Thread::notify(), the wait() function was omitted when transferring the code and removing logging functions. I apologize for not accurately describing the issue in the additional question. The problem is that declaring it as a global variable like below causes an exception. However, it works as expected when declared within a function like main().

MainMessageThread MMT; //global variable

The exception occurs at this point (Threads_windows.cpp line 50). Here is the call stack:

<unknown> 0x00007ff9aefa3aca
<unknown> 0x00007ff9aef918e4
<unknown> 0x00007ff9aef916d2
juce::CriticalSection::enter() juce_Threads_windows.cpp:50
juce::GenericScopedLock::GenericScopedLock<…>(const juce::CriticalSection &) juce_ScopedLock.h:67
juce::SingletonHolder::get() juce_Singleton.h:59
juce::InternalMessageQueue::getInstance() juce_Messaging_windows.cpp:54
juce::MessageManager::doPlatformSpecificInitialisation() juce_Messaging_windows.cpp:290
juce::MessageManager::getInstance() juce_MessageManager.cpp:52
uniq::MainMessageThread::run() core.cpp:75
juce::Thread::threadEntryPoint() juce_Thread.cpp:98
juce::juce_threadEntryPoint(void *) juce_Thread.cpp:120
juce::threadEntryProc(void *) juce_Threads_windows.cpp:61
<unknown> 0x00007ff90e6b2d20
<unknown> 0x00007ff9add5257d
<unknown> 0x00007ff9aefcaa58

Fair enough about the MidiDeviceListConnection issue. Sorry about the confusion.

regarding notify (), it makes no sense to call it within the same thread. It is used from OTHER threads to wake up a sleeping thread.

And, my apologies for not looking more closely at your actual code, I think the dispatch loop has to run ON the MM thread, which is the apps main thread. What is your intention for not just running it in main?

Actually, I can see a reason to call resume() in the running thread context, as a way to bypass a wait () that can also be signalled by a separate thread. So, that may be how you are using it. As you said, you removed some code for your example.

In Windows, running runDispatchLoop() blocks the thread, which is why I couldn’t call it from the main thread. When I tested it on Android, the thread blocking didn’t occur, suggesting that the behavior might vary across platforms. I chose not to use the JUCEApplication class as I’m developing for a dynamic library and wanted to avoid including unnecessary GUI components. Although directly creating a message thread might not be the best approach, I couldn’t think of a better alternative, and it seems to work fine in practice.

Regarding notify(), I used it in the following code to check if the main thread starts and stops correctly:

class MainMessageThread : public juce::Thread
{
public:
    MainMessageThread() : Thread("MessageThread")
    {
        startThread();
        log::println(wait(1000) ? "MainMessageThread start" : "MainMessageThread fail");
    }
    // ...
};

Thank you for your response.

I don’t understand your use case, and thus cannot make any suggestions as to what you are trying to achieve. But, as far as I know, the MessageManager loop expects to be the main application thread. Is it possible for you to run the code you want to run from the main thread as a seperate juce::Thread? ie. initialize juce, start your new thread, run dispatch loop. I have done this for a console app.

int main (int argc, char* argv[])
{
    juce::ScopedJuceInitialiser_GUI initialiser;
    class myThread : juce::Thread
    {
    public:
        myThread () : Thread { "myThread" }
        {
            startThread ();
        }
        void run () override
        {
            while (!threadShouldExit())
            {
				// do stuff, where signalThreadShouldExit () is called when app should exit
            }
			juce::MessageManager::getInstance ()->stopDispatchLoop ();
        }
    };
    MyThread myThread;
    juce::MessageManager::getInstance ()->runDispatchLoop ();
    return 0;
}

First of all, thank you for taking the time to respond. English is not my first language, and I might not have explained my situation thoroughly. Let me clarify my situation. While the runDispatchLoop() may not block the thread on different platforms, at least on the Windows platform I am using, it does block. In this case, creating a separate thread for tasks (like file loading, audio playback, etc.) is necessary, otherwise no further actions can be performed. Please see the code below for reference:

int main(int argc, char* argv[])
{
    juce::ScopedJuceInitialiser_GUI initialiser;
    class myThread : juce::Thread
    {
        //...
    };
    myThread myThread;
    std::cout << "runDispatchLoop" << std::endl;
    juce::MessageManager::getInstance()->runDispatchLoop(); // Main thread blocks here.

    // Do something here, e.g., load a file, play a sound, etc.
    // But this line is never reached.
    // Even signalThreadShouldExit() cannot be called.
    std::cout << "Was it called?" << std::endl;

    return 0;
}

Therefore, I thought of creating a separate thread like the MainMessageThread approach, making this thread dedicated to message handling. In this way, the main thread is no longer blocked and can perform the desired tasks. Since my ultimate goal is to create a dynamic library, blocking the calling thread is not suitable, hence the creation of a separate message thread.

If I follow your advice to call runDispatchLoop() from the main thread and run tasks from a separate thread (let’s call it the worker thread), the main thread is still different from the worker thread. Therefore, functions that require calling from the message thread (like AudioDeviceManager) still need to be called through functions like MessageManager::callAsync(). It seems that my original issue remains unresolved.

To sum up, my ultimate questions are:

  1. AudioDeviceManager’s requirement of the message thread for constructor/destructor and memory leaks upon destruction.
  2. Why does the MainMessageThread fail when defined globally - specifically, why does an exception occur in CriticalSection::enter() during global initialization?