createEditorIfNeeded returning non-existent editor

Thanks for taking a look. I cobbled together a way to host a plug-in inside a plug-in, but:

  1. its interface disappears after closing and reopening.
  2. It is positioned at the top of the editor window (in JUCE Audio Plug-in Host) when I setTopLeftPosition(0, 60)
  3. paintOverChildren paints under it. On reopening, the results of paintOverChildren are visible, but not the hosted plug-in.
    By the way, the string “realFileName” here is just an arbitrary VST3 plug-in that I drop into the same folder for it to load. That is how I want this to be used, by being in the same folder, but it will not be “Surge.vst3”.

Code here:
https://github.com/chuckkh/WrapperTest/tree/master/Source

//PluginProcessor.h

#pragma once

#include <JuceHeader.h>

class WrapperTestAudioProcessor : public juce::AudioProcessor
{
public:
WrapperTestAudioProcessor();
~WrapperTestAudioProcessor() override;
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
void releaseResources() override;

#ifndef JucePlugin_PreferredChannelConfigurations
bool isBusesLayoutSupported(const BusesLayout& layouts) const override;
#endif
void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override;
juce::AudioProcessorEditor* getWrappedInstanceEditor();

const juce::String getName() const override;
bool acceptsMidi() const override;
bool producesMidi() const override;
bool isMidiEffect() const override;
double getTailLengthSeconds() const override;
int getNumPrograms() override;
int getCurrentProgram() override;
void setCurrentProgram(int index) override;
const juce::String getProgramName(int index) override;
void changeProgramName(int index, const juce::String& newName) override;
void getStateInformation(juce::MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override;

private:
AudioPluginFormatManager fm;
PluginDescription descr;
OwnedArray pluginDescriptions;
KnownPluginList plist;
File myFile, myPath;
const String myFileName = myFile.getFileName();
const String realFileName = “Surge.vst3”;
String realFullPathName;
AudioProcessor* plugin;
std::unique_ptr wrappedInstance;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WrapperTestAudioProcessor)
};

// PluginProcessor.cpp

#include “PluginProcessor.h”
#include “PluginEditor.h”

WrapperTestAudioProcessor::WrapperTestAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
: AudioProcessor(BusesProperties()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
.withInput(“Input”, juce::AudioChannelSet::stereo(), true)
#endif
.withOutput(“Output”, juce::AudioChannelSet::stereo(), true)
#endif
)
#endif

{
fm.addDefaultFormats();
descr.pluginFormatName = “VST3”;
myFile = File::getSpecialLocation(File::currentApplicationFile);
myPath = myFile.getParentDirectory();
const String myPathName = myPath.getFullPathName();

static String myPathNameSep = File::addTrailingSeparator(myPathName);
realFullPathName = myPathNameSep + realFileName;
descr.fileOrIdentifier = realFullPathName;

for (uint32 i = 0; i < fm.getNumFormats(); ++i)
    plist.scanAndAddFile(realFullPathName,
        true,
        pluginDescriptions,
        *fm.getFormat(i));

jassert(pluginDescriptions.size() > 0);

bool loadingSuccess = false;
String ignore;
if (wrappedInstance = fm.createPluginInstance(*pluginDescriptions[0], 44100.0, 512, ignore))
    loadingSuccess = true;

}
WrapperTestAudioProcessor::~WrapperTestAudioProcessor()
{
delete wrappedInstance->getActiveEditor();
}
const juce::String WrapperTestAudioProcessor::getName() const
{
return JucePlugin_Name;
}
bool WrapperTestAudioProcessor::acceptsMidi() const
{
return wrappedInstance->acceptsMidi();
}
bool WrapperTestAudioProcessor::producesMidi() const
{
return wrappedInstance->producesMidi();
}
bool WrapperTestAudioProcessor::isMidiEffect() const
{
return wrappedInstance->isMidiEffect();
}
double WrapperTestAudioProcessor::getTailLengthSeconds() const
{
return wrappedInstance->getTailLengthSeconds();
}
int WrapperTestAudioProcessor::getNumPrograms()
{
return 1;
}
int WrapperTestAudioProcessor::getCurrentProgram()
{
return 0;
}
void WrapperTestAudioProcessor::setCurrentProgram(int index)
{
}
const juce::String WrapperTestAudioProcessor::getProgramName(int index)
{
return {};
}
void WrapperTestAudioProcessor::changeProgramName(int index, const juce::String& newName)
{
}
void WrapperTestAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
wrappedInstance->prepareToPlay(sampleRate, samplesPerBlock);
}
void WrapperTestAudioProcessor::releaseResources()
{
}
#ifndef JucePlugin_PreferredChannelConfigurations
bool WrapperTestAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const
{
#if JucePlugin_IsMidiEffect
juce::ignoreUnused(layouts);
return true;
#else
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
#if ! JucePlugin_IsSynth
if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
return false;
#endif
return true;
#endif
}
#endif
void WrapperTestAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
wrappedInstance->processBlock(buffer, midiMessages);
}
bool WrapperTestAudioProcessor::hasEditor() const
{
return true;
}
juce::AudioProcessorEditor* WrapperTestAudioProcessor::createEditor()
{
return new WrapperTestAudioProcessorEditor(this);
}
juce::AudioProcessorEditor
WrapperTestAudioProcessor::getWrappedInstanceEditor()
{
return wrappedInstance->createEditorIfNeeded();
}
void WrapperTestAudioProcessor::getStateInformation(juce::MemoryBlock& destData)
{
wrappedInstance->getStateInformation(destData);
}
void WrapperTestAudioProcessor::setStateInformation(const void* data, int sizeInBytes)
{
wrappedInstance->setStateInformation(data, sizeInBytes);
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new WrapperTestAudioProcessor();
}

// PluginEditor.h

#pragma once

#include <JuceHeader.h>
#include “PluginProcessor.h”

class WrapperTestAudioProcessorEditor : public juce::AudioProcessorEditor
{
public:
WrapperTestAudioProcessorEditor(WrapperTestAudioProcessor&);
~WrapperTestAudioProcessorEditor() override;
void paint(juce::Graphics&) override;
void paintOverChildren(juce::Graphics&) override;
void resized() override;
private:
WrapperTestAudioProcessor& audioProcessor;
AudioProcessorEditor* wrappedEditor;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WrapperTestAudioProcessorEditor)
};

// PluginEditor.cpp

#include “PluginProcessor.h”
#include “PluginEditor.h”

WrapperTestAudioProcessorEditor::WrapperTestAudioProcessorEditor(WrapperTestAudioProcessor& p)
: AudioProcessorEditor(&p), audioProcessor(p)
{
wrappedEditor = audioProcessor.getWrappedInstanceEditor();
addAndMakeVisible(wrappedEditor, -1);
bool visible = wrappedEditor->isVisible();
setSize(wrappedEditor->getWidth(), wrappedEditor->getHeight() + 60);
}
WrapperTestAudioProcessorEditor::~WrapperTestAudioProcessorEditor()
{
// delete wrappedEditor;
}
void WrapperTestAudioProcessorEditor::paint(juce::Graphics& g)
{
}
void WrapperTestAudioProcessorEditor::paintOverChildren(juce::Graphics& g)
{

g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
g.setColour(juce::Colours::white);
g.setFont(15.0f);
g.drawFittedText(“Hello World!”, getLocalBounds(), juce::Justification::centred, 1);
}
void WrapperTestAudioProcessorEditor::resized()
{
wrappedEditor->setTopLeftPosition(0, 60);
}

I haven’t read all of the code, but that paintOverChildren is not visible is expected.
That method can only paint over juce Components. The PluginEditor wraps a native window, because the wrapped plugin can be a juce plugin or something else. That’s why it needs to be a native window.
There are a few juce components that wrap a native window where paintOverChildren as well as placing juce Components on top will not work. The ones I know of as of today:

  • VideoComponent
  • WebViewComponent

Hope that helps, even though it’s bad news, I know

Hi, daniel. Thanks for the clarification.
That’s not such a big deal. I can live with having my controls next to the loaded plug-in window.

The big hang-up is that the loaded plug-in’s window is no longer visible after closing and reopening.
I have tried a delete command either in my editor’s destructor or as a separate method in my processor class, but this doesn’t seem to help. I thought maybe deleting that pointer would force it to be reloaded the next time my editor runs its constructor.
I can only imagine that the problem is that, somehow, either the hosted plug-in’s editor is closed and reinitialized and I have the old pointer, or vice versa. The JUCE Audio Plug-In host doesn’t have this problem.

I see.

It seems to me you are not taking ownership over the wrapped editor. In that case you are the host and you are leaking the editor.
Can you check if createEditorIfNeeded() returns an editor?

I would change the

juce::AudioProcessorEditor* WrapperTestAudioProcessor::getWrappedInstanceEditor()
{
    return wrappedInstance->createEditorIfNeeded();
}

to

std::unique_ptr<juce::AudioProcessorEditor> WrapperTestAudioProcessor::createWrappedInstanceEditor()
{
    return { wrappedInstance->createEditor() };
}

And store it in an unique_ptr inside your PluginEditor, i.e.

// instead
AudioProcessorEditor* wrappedEditor;
// use this
std::unique_ptr<AudioProcessorEditor> wrappedEditor;

Let me know if that works.

It seems very close, but how do I do anything with a unique_ptr to a component that I can only use by passing it as an argument? I see there are ways to do that that I probably want to know, but they seem like overkill.

“copy-list initialization cannot use a constructor marked ‘explicit’”
on

return { wrappedInstance->createEditor() };

The std::unique_ptr has the -> operator overloaded, so you can call anything like you would on a raw pointer:

wrappedEditor->setBounds (0, 60, 400, 400);

The difference is that you cannot copy a unique_ptr and that the thing it points to is deleted once the unique_ptr goes of scope. I hope I understood the question right.

I see, that is new to me. But I guess you need to spell the type out:

return std::unique_ptr<AudioProcessorEditor> (wrappedInstance->createEditor());

Hope that’s better

For addAndMakeVisible, is there such a syntax available?
AFAIK it takes the pointer as argument, called by the parent.
In other words, I can’t addAndMakeVisible, or even add, a unique_ptr, as it would make a copy.

There are two ways, either

// return the raw pointer => T*
addAndMakeVisible (wrappedEditor.get());
// or dereference => T&
addAndMakeVisible (*wrappedEditor);

The first version can also return nullptr, the second would be an error if the wrappedEditor is nullptr.

1 Like

Still… Sounds like a problem with exclusive ownership?

Do you take ownership in the processor as well? That would indeed be a double delete.

The processor creates an editor each time one is required.
In my host I put it into a DocumentWindow using setContentOwned().

This works for me, I don’t know what you are doing differently…

The processor takes ownership of the hosted PluginInstance. What you’ve shown me has the editor now taking ownership of the hosted plug-in’s editor. Might that be a conflict since the hosted plug-in’s editor may have a pointer to the processor?
But I don’t see how I could run an array of plug-ins inside a plug-in without the processor taking ownership of the PluginInstance(s).
Experience tells me I’m probably misunderstanding what you’re saying…

The processor takes ownership of the wrapped processors. And the processorEditor takes ownership of the wrapped processor’s editors.

You are in the situation the host and need to make sure that the processor is never destroyed before the editor. If your wrapped plugin remains alive for the whole life time this is guaranteed, because the real host also makes sure that your editor is destroyed before the editor.

If my editor takes ownership of the wrapped editor; and my processor takes ownership of the wrapped processor; but if the wrapped editor also contains a reference to the wrapped processor, won’t that cause an error?

The error I’m getting now happens when loading the editor; as always, AudioPluginHost opens and closes an editor object a few times before drawing it. And on closing it the first time, it tells me the plug-in - the wrapped plug-in - is trying to read an inaccessible location. It says:

warnOnFailure (view->removed());

That is, by the way, using the technique you mentioned:

addAndMakeVisible(wrappedEditor.get(), -1)

And I suspect that I’m causing it to be deleted twice. Once as the child and once as unique_ptr?

I might end up trying this, inheriting from DocumentWindow, though it feels odd.
I notice your constructor takes a raw pointer, not a unique_ptr. Does this completely negate the need for unique_ptr?

I’m not 100% sure I follow this, but I can confirm that I get the same read access error if I inherit from ResizableWindow and use setContentOwned on the wrappedEditor instead of the addAndMakeVisible I use when it’s not a unique_ptr.

So if I don’t take ownership of the wrapped editor, the second time I open my editor, the hosted editor is nowhere to be found. If I do take ownership, something apparently gets closed before the last reference to it is deleted.

I am still perplexed:
createEditorIfNeeded() does return a pointer to an editor. Here, I add that editor as a child and make it visible, but it is not present. In debugging, I saw that the address returned the second time was the same as the first time. So it is returning the wrong address.
Without taking ownership of the editor, that function should still either return a new editor or an existing one; not a non-existent one, as it does.
Since taking ownership of that editor doesn’t work, why can’t I just close it manually, as host? I actually don’t see any way to do that.

Let’s have a walk through the code:

createEditorIfNeeded() returns a member called activeEditor. If that is nullptr, it creates one calling createEditor():

Now the question: is activeEditor owning?

No it’s not. It is a SafePointer, which means it is automatically set to nullptr if the component is destroyed.

If you leak the editor, the SafePointer still points to the old editor, so no new one is created.

That means the double delete doesn’t seem to stem from the JUCE code. So it is most likely that you take ownership twice.

The code I linked uses the raw pointer, because the DowumentWindow still uses the old method where a flag defines if it is owned or not.

I was hoping to destroy it, but I don’t see any way to do that.
As far as I can tell, my original code didn’t take ownership of the wrapped editor at all. It sounds like it was doing what you say, so no new one was created. But??? If my editor was destroyed (by the user), how did it keep a reference to something else?
Then, as you said, I added a unique_ptr to hold the wrapped editor, and what seems to be a double delete crops up.
I am completely in the mist here. I went from no ownership to double delete by changing one thing?

Thanks for that clarification, by the way. Very interesting stuff!

It’s indeed a riddle, I cannot tell without the rest of the code and running it through a debugger.

Good luck, sorry I couldn’t solve it.