Unload a VST3?

I’m hosting a plugin using:

AudioPluginFormatManager::createPluginInstance()

That works and the plugin loads correctly (this is VST3/Mac).

However - even after destroying the returned object, I still see most of the plugin’s memory and threads still loaded and running in the Mac activity monitor.

I also see this behavior in AudioPluginHost.

Any ideas how can I make sure that an unused VST3 is completely removed and its threads stop working?

Edit: This seems to be related to DLLHandleCache never releasing the used dlls.

See more info in my comment here:

Are those threads you spawned in your plugin?
If they are owned by the plugin instance they should call stopThread upon destruction.
If they are owned by a SharedResourcePointer, they should be destroyed when the last instance goes out of scope.

If that is not happening, either the host keeps one instance spare for whatever reason (can you verify by counting the number of threads?) or something is leaking.

Reclaiming the memory will be harder. It is platform specific, and afaik only a few hosts do that properly (IIRC Bitwig and I could imagine Reaper maybe?)

On windows there is

But ofc. it would be the responsibility of the host or juce in the AudioPluginHost case.

These are threads held by the 3rd party plugins - not mine. Almost all plugins allocate some singleton memory + singleton threads that are running until the plugin gets fully removed.

I am destroying the instance completely - but the instance is not removed (and that happened in APH as well).

I am the host in this case - but it’s the same issue in AudioPluginHost.
From what I can see the dll isn’t getting up getting destroyed. Even though this could be leaked memory I don’t think it is - this is something I see in many plugins ‘out in open’, including mine, that are very likely using singletons with RAII that need to be destroyed when the plugin is unloaded.

Ah ok. Well it is a mix of a bug in the third party, but an unload feature in juce would indeed be great!

I am not sure how it would be possible, you would need to get a list of the dlls loaded and then be able to call FreeLibrary or the respective method on the other platforms.

The out of process hosting that apple is doing would make those problems go away, but it will add another can of worms for sure.

Yes, I would expect having some reference counting of the same dll object, and when the ref goes to 0 unload the dll completely, which to my knowledge should destroy all the static/singletons.

I don’t think it’s a bug in the 3rd party, almost every commercial plugin I’ve tested is using some static memory and threads.

Looking at the docs I linked above, Windows does this by default. If you call FreeLibrary it will return an error code if the dll is still in use (using a reference count indeed)

Yes, but to stay in JUCE terminology, I would expect them to use a SharedResourcePointer that is destroyed and cleans up when the last instance is gone and not a Singleton that keeps resources running in such a scenario.

Let’s at least call it poor design.

Such behaviour leads to the “Did you try to close and open again” routines, which are ridiculous and always have been, even though it seems the accepted way.

I think these are different use cases. Sometimes you want to tie your resources to life time of Processor or Editor classes, and sometimes you want it statically within the same dll, such as copy protection methods, etc.

Some things like juce::TimerThread and the message manager are singletons by JUCE itself, too.

I don’t see using Singletons as a bug in this case. And it’s so widely used anyway and not related to JUCE. I tested about 30 commercial plugins today and 100% of them have static memory and threads, so it’s not anything out of the norm.

I think a SharedResourcePointer would live in the same dll memory, but I leave it now because I derail from your actual topic which deserves a solution.

I would love to see a way to unload dll/plugins in JUCE.

There are still many reasons to have things tied to the global dll memory. For example with copy protection, those classes are also showing up during scan, so before an actual instance of the plugin is even loaded.

You can also search the JUCE code base for JUCE_IMPLEMENT_SINGLETON to see how many singletons are getting created in the JUCE code even if your code doesn’t do that explicitly.

And actually, looking for references to the JUCE singleton also led me to the culprint of this bug.
It’s called DLLHandleCache in juce_VST3PluginFormat.cpp

Basically once a specific dll gets added there, it never gets removed - since the DLLHandleCache itself is a singleton!

This method:

struct DLLHandleCache final : public DeletedAtShutdown
{
    DLLHandleCache() = default;
    ~DLLHandleCache() override { clearSingletonInstance(); }

    JUCE_DECLARE_SINGLETON (DLLHandleCache, false)

    DLLHandle& findOrCreateHandle (const String& modulePath)
    {
        //Other stuff to fetch the file and then:
        openHandles.push_back (std::make_unique<DLLHandle> (file));
    //...

This handle never gets removed until the host process/dll gets destroyed. So the OS still thinks we need this dll until the very end.

What we need here is some form of refcounting.

4 Likes

Yeah this is likely to just suck up memory indefinitely, especially in a DAW where people load different sessions with different plugins over many hours.

It would be good if this was garbage collected so handles were closed if inactive after 30s or so.

2 Likes

I know 30s sounds like a good default for a DAW, but I really think the user should set up that Timer and not JUCE.

For example we have a small plugin host where the user might change presets that hold different chains of plugins. Since users might browser presets quickly it might blow up the users memory if that queue waits for 30s and the user in that time changed 10 different plugin chains which are still lingering.

I’d much rather an immediate removal when the refcount is 0, and then I can just set up a clean and sweep timer with destructors called at the time I expect.

Sure. I actually don’t think having to load a DLL takes much time these days does it?

I’d be happy with a pure ref-count system. We do some handle holding in Waveform to keep plugins alive for a bit anyway so it probably won’t make any difference to us.

I prefer the idea of pure determinism rather than async timers.

2 Likes

I created a fork with a commit that in my initial experiments, fixes the issue:

The idea behind the change is: DLLHandleCache is not a global owner anymore (with a vector of unique_ptr). Instead, it holds a vector of weak_ptr that creates shared_ptr on demand for all users that require the handle to create a factory.

One of those users is the VST3ModuleHandle class that holds that handle as a member, so there’s no way for the handle to get out of scope while it’s still in use.

That means that additional instances would reuse the handle, but once the number of users goes to 0, it gets cleaned up.

When I test with AudioPluginHost I can see that indeed the last instance of a plugin cleans up all it’s memory and threads.

Edit: it seems like an explicit call to CFBundleUnloadExecutable was needed. I added it to the fork as well.

Happy to send an organized PR, if there’s an interest from the JUCE team.

3 Likes

Bumping as it’s an important bug affecting commercial plugin hosts out there.

It looks like JUCE used to use pure ref-counting to manage module lifetimes, and the current caching mechanism was added to work around issues with some buggy plugins that weren’t correctly shutting down the Obj-C runtime on when they were unloaded:

I agree the holding onto module handles indefinitely is not a good idea, but we need to test whether removing the global cache breaks some plugins. If it does, then we’d need to weigh up whether it’s more valuable to continue supporting those plugins, or to improve the memory usage of hosts.

The singleton itself is fine and actually correct - you don’t want to load the same plugin twice which I think is the cause of the original bug. The problem is logical - the singleton isn’t releasing the plugin even after it finishes ref counting. It does on VST2 and to my knowledge those plugins work fine there?

Update:
With AudioPluginHost and my own host built from my fork here:

I’m able to load/remove the plugins mentioned in that thread (Padshop and Retrologue) multiple times with no issues.

Tested on Windows 11 and Mac in VST3.

Thanks for testing this out with the problematic plugins.

We’ve now added refcounting for VST3 modules, so that they are automatically unloaded when no longer referenced:

2 Likes