Assert failure with vst2 wrapper using SDK 3.7.2

Steps to reproduce:

  1. Compile vst2 plugin using VST3 SDK 3.7.2 and the included wrapper (and use the VST2 SDK that was last released)
  2. Load 1 instance of the plugin in Reason 11… works
  3. Load another instance of the plugin in Reason 11 => fails
// from platformfactory.cpp line 30
void initPlatform (PlatformInstanceHandle instance)
{
	vstgui_assert (!gPlatformFactory); // <======== this is where it fails
	setPlatformFactory (std::unique_ptr<IPlatformFactory> (new VSTGUIPlatformFactory (instance)));
}

This code gets triggered by:

// from vstguieditor.cpp line 53
#if VSTGUI_NEEDS_INIT_LIB
#include "vstgui/lib/vstguiinit.h"
static Steinberg::ModuleInitializer InitVSTGUI ([] () {
	using namespace Steinberg;
	VSTGUI::init (getPlatformModuleHandle ());
#if SMTG_MODULE_IS_BUNDLE
	Vst::setupVSTGUIBundleSupport (getPlatformModuleHandle ());
#endif // SMTG_MODULE_IS_BUNDLE
});

It seems that loading plugins in Reason (unclear if other DAWs behave the same way) does not get done in isolation. The same plugin built with 3.7.1 does not have this problem.

It looks like instead of hard setting variables, a reference counter should be used instead ensuring that this kind of problem doesn’t happen…

To follow up on this issue, I have done the following experiment:

I modified a very simply VST2 plugin (purely VST2, not using VST3 wrapper) with the original code on github. I made the following change:

static int __staticIssue = 0;


VSTPlugin *VSTPluginMain(VSTHostCallback vstHostCallback)
{
  printf("called VSTPluginMain... \n");

  // simply create our plugin C++ class
  VSTPluginWrapper *plugin =
    new VSTPluginWrapper(vstHostCallback,
                         CCONST('u', 's', 'a', 'n'),
                         PLUGIN_VERSION, // version
                         0,    // no params
                         0,    // no programs
                         2,    // 2 inputs
                         2);   // 2 outputs

  printf("[vst.usan.M3dB] Created plugin %p / __staticIssue=%d \n", plugin, __staticIssue);

  __staticIssue++;

  // return the plugin per the contract of the API
  return plugin->getVSTPlugin();
}

I then compiled and loaded the plugin in 2 different DAWs (Reason and Maschine 2). Both of them behave in the exact same way:

// add 1 instance
called VSTPluginMain...
[vst.usan.M3dB] Created plugin 0x7fcdf51a7a60 / __staticIssue=0
// add 1 instance
called VSTPluginMain...
[vst.usan.M3dB] Created plugin 0x7fcdf5cacd70 / __staticIssue=1
// add 1 instance
called VSTPluginMain...
[vst.usan.M3dB] Created plugin 0x7fcdf51200b0 / __staticIssue=2
// delete all instances
// then add 1 instance
called VSTPluginMain...
[vst.usan.M3dB] Created plugin 0x7fcdf58aa1d0 / __staticIssue=3

So what this shows is that VST2 loading (at least in 2 different DAWs) is not isolating instances and as a result static variables end up being shared across instances.

Which creates a serious issue if you use the VST3 SDK 3.7.2 VST2 wrapper since it uses a static variable (gPlatformFactory) protected with an assert, thus crashing the DAW if 2 instances get loaded.

I am not entirely sure what 3.7.1 was doing instead (because there was no such issue) but 3.7.2 has seriously broken this code path. You can argue that DAWs are not doing the right thing. But 2 of the 2 that I tried on (so 100% of my tests) are failing… I know that Steinberg does not support VST2 anymore, but just wanted to point out that 3.7.2 has put the nail in the coffin with this “small” change…

After more hunting, I have traced down the issue to public.sdk/source/vst/vst2wrapper/vst2wrapper.cpp line 1943

SMTG_EXPORT_SYMBOL AEffect* VSTPluginMain (audioMasterCallback audioMaster)
{
	// Get VST Version of the host
	if (!audioMaster (nullptr, audioMasterVersion, 0, 0, nullptr, 0))
		return nullptr; // old version

	if (InitModule () == false) // <====== this is called always
		return nullptr;

	// Create the AudioEffect
	AudioEffect* effect = createEffectInstance (audioMaster);
	if (!effect)
		return nullptr;

	// Return the VST AEffect structure
	return effect->getAeffect ();
}

which unconditionally call InitModule() every time a plugin is instantiated. I don’t believe there is any restriction from the DAW side to do that.

I think this is broken. InitModule() should be called when the plugin/library is loaded only not every time a plugin is instantiated as documented :

// macmain.cpp L54
bool InitModule (); ///< must be provided by plug-in: called when the library is loaded
bool DeinitModule (); ///< must be provided by plug-in: called when the library is unloaded

I patched vst2wrapper.cpp in the following fashion:

extern bool InitModule ();
extern bool DeinitModule();

struct ModuleLifecycle {
  ModuleLifecycle() : fInitModuleResult(InitModule()) {}

  inline bool isInitSuccessful() const { return fInitModuleResult; }

  ~ModuleLifecycle()
  {
    if(fInitModuleResult)
      DeinitModule();
  }

private:
  const bool fInitModuleResult;
};

//-----------------------------------------------------------------------------
extern "C" {

//-----------------------------------------------------------------------------
/** Prototype of the export function main */
//-----------------------------------------------------------------------------
SMTG_EXPORT_SYMBOL AEffect* VSTPluginMain (audioMasterCallback audioMaster)
{
  static ModuleLifecycle moduleLifecycle{};

	// Get VST Version of the host
	if (!audioMaster (nullptr, audioMasterVersion, 0, 0, nullptr, 0))
		return nullptr; // old version

  if(!moduleLifecycle.isInitSuccessful())
    return nullptr;

	// Create the AudioEffect
	AudioEffect* effect = createEffectInstance (audioMaster);
	if (!effect)
		return nullptr;

	// Return the VST AEffect structure
	return effect->getAeffect ();
}

// IMPORTANT:
// note you need to remove the `DeinitModule()` call from `~BaseWrapper`

which ensures that

a) InitModule() is only called once (in a thread safe manner)
b) DeInitModule() is also called whenever, either the DAW terminates or the library is unloaded

This code will work whether the DAW loads the code only once and initialize multiple instances of the plugin (which seem to be the case for Reason and Maschine 2) as well as if the DAW loads the code for each instance of the plugin.

Finally, the reason why it works with VST3 as demonstrated by editorhost is this kind of code:

// from macmain.cpp
/** must be called from host right after loading bundle
Note: this could be called more than one time! */
bool bundleEntry (CFBundleRef ref)
{
	if (ref)
	{
		bundleRefCounter++;
		CFRetain (ref);

		// hold all bundle refs until plug-in is fully uninitialized
		gBundleRefs.push_back (ref);

		if (!moduleHandle)
		{
			ghInst = ref;
			moduleHandle = ref;

			// obtain the bundle path
			CFURLRef tempURL = CFBundleCopyBundleURL (ref);
			CFURLGetFileSystemRepresentation (tempURL, true, (UInt8*)gPath, VST_MAX_PATH);
			CFRelease (tempURL);
		}

		if (bundleRefCounter == 1) // <==== this ensures called only once!
			return InitModule ();
	}
	return true;
}
1 Like

Maybe these kind of problems are suited best in the github issue tracker, especially if you already have a fix (merge request)? Issues · steinbergmedia/vst3sdk · GitHub

Thanks for the report. We will upload a fix in github as soon as possible. This issue concerns VST2 and AAX Wrapper when using VSTGUI.

@Yvan Thank you for confirming. I opened a ticket on github for it with suggestion on how to implement.

There also seems to be another issue with vst3 on Windows10 (VSTGUI / json format). See here.

1 Like