Using JUCE as a library... Advice needed

Hi Jucers…

I am trying to use JUCE as a library to add plugin hosting (VST/AU/LADSPA) support to our application. I can build JUCE as a static library, link to it, and it “sort of” works. I can call JUCE code from our app, list available plugins, load plugins, and load plugin UI.

However, I quickly run into all sorts of hard-to-debug problems (crashes, hangs), that appear to be related to JUCE’s message manager and / or how it is initialized, and / or how it interacts with our existing code.

Some questions:

  1. Is it possible to run VST hosting without the message manager being initialised…? I’m guessing not, but this would be useful (even if it involves hacks) if only for debugging purposes
  2. On what thread should I be calling initialiseJuce_GUI() ? Should I be calling it on our main thread, which our GUI also runs on, or should I be running the JUCE message manager in its own thread? I’ve been trying with the main thread, but depending on where in our app’s initialisation process it’s called results in different problems occurring.
  3. Is it safe to call initialiseJuce_GUI() more than once… for example in a background thread at startup that scans for plugins, and then again in the “main” thread?

Many thanks for any advise on this.

UPDATE (17/5/21): It appears that the problems I’ve been getting are a result of JUCE code being called from threads other than the one that initialiseJuce_GUI() was called on, so I need to fix that. Still, it would be useful to have answers to the questions above.

I think this is very unlikely. VSTPluginInstance derives from AsyncUpdater, so at least some functionality will be degraded/broken if there is no active message loop. Running plugins without a message loop also isn’t a use-case that we support or test, so it’s possible that a future change will cause breakage, even if it appears to work today.

Doing this on the main thread is probably safest. On Windows, it looks like this call may create a new HiddenMessageWindow instance. As I understand it, the Windows message queue must then be pumped on the same thread that initially created this window. For this reason, it’s probably best to call initialiseJuce_GUI from the same thread that will own the message loop.

The docs for ScopedJuceInitialiser_GUI (which calls initialiseJuce_GUI in its constructor) say:

Be careful with your threading though - to be safe, you should always make sure that these objects are created and deleted on the message thread.

I believe that it is, although you should be careful to only call shutdownJuce_GUI once, when the application is quitting. If you accidentally call it before you’re completely done with JUCE, it will clear singleton instances which may still be in use. You may also see false-positive leak detector warnings, because the leak detector could be destroyed before some other JUCE objects which are still in use.

Thanks @reuk. Calling initialiseJuce_GUI() once on the main thread seems to have done the trick.

1 Like

Hi @reuk … apologies for resurrecting this.

Whilst the above approach is working on macOS and Windows, I’m having a problem on Linux.

To recap:

  • My application is a plugin host
  • I’m using JUCE as a static library to provide plugin hosting functionality and DSP
  • I am calling all JUCE functions on the main thread (that owns GTKRootWindow) including initialiseJuce_GUI()

I am able to create a VST3 plugin instance fine using the JUCE GainPlugin demo.

However, when I call AudioProcessor::createEditorIfNeeded() then add it to a DocumentWindow with setContentOwned(), I get a “transparent” window created, with no window decoration or content.

This is shown below. You will see the content behind the Window, which contains VSCode, and then the Gain plugin’s window, which contains a fragment of the VSCode UI, which was sitting behind it!

Any clues as to why this is happening and how I might resolve it?

Screenshot 2021-05-27 at 08.46.02

OK, I know a bit more what’s happening here.

If I take VST hosting out of the equation, and just create an empty DocumentWindow I can see that the window gets added to the Desktop, and if setUsingNativeTitlebar() is set to true then the native title bar is added with a close and minimize button. resize() is also called on the window.

However… paint() is not called on the window, which explains why no content is drawn!

As far as I can tell, repaints are supposed to be triggered by LinuxRepaintManager, but this does not seem to be happening.

Any clues as to why?

It sounds like you might have built the static library with different config flags to the application, specifically the JUCE_USE_XSHM flag in juce_gui_basics that controls whether the Xshm extension is used for managing repaints on Linux. Can you check the value of the flag for both and see if they match up?

@ed95 It’s enabled in the Projucer for the static lib, and I’ve now defined it for the application, but still no joy. Is there a breakpoint I can set somewhere to confirm it’s running…?

Note: I can see that LinuxRepaintManager gets created when addToDesktop() is called for the window but performAnyPendingRepaintsNow() is never called, so the Timer for the repaint manager never starts.

Are any timers being called? Perhaps create a simple class that derives from Timer, start it and see if it gets called.

OK, just created a simple Timer class as you suggest, and it turns out that timerCallback() never gets called! So, no, it seems no timers are getting called. Any ideas?

@ed95
Also, if I get the ComponentPeer for the window and call performAnyPendingRepaintsNow() then the window content gets drawn, so this seems definitely a Timer issue.

It sounds like the message loop isn’t running. Try sticking a breakpoint in the dispatchPendingEvents() method of the internal Linux run loop and see if it’s hit. Unfortunately without seeing any of the code I’m not able to provide much more insight than that…

Thanks @ed95. I can confirm the message loop isn’t running. I feel I’m bit further forward now I at least know what the problem is, so thanks for your help, much appreciated.

As for the code, I’m not currently doing anything besides calling initialiseJuce_GUI() on our application’s main thread, and trying to add a DocumentWindow to the desktop, just to get a proof-of-concept going.

Our application is using GTK if that makes any difference.

@ed95 I’ve now reduced down to a simple test program that just uses JUCE (6.0.8) and nothing else. No static library, no external code. Just JUCE. Building on macOS.

I create a “Console” app from the Projucer, and have the following code for Main.cpp:

#include <JuceHeader.h>

using namespace juce;

struct MyTimer : public Timer
{
   void timerCallback() override
   {
      DBG("HERE");
   }
};


int main(void)
{
   ScopedJuceInitialiser_GUI init;
   
   MyTimer t;
   t.startTimerHz(1);
      
   
   if (t.isTimerRunning())
   {
      DBG("TIMER IS RUNNING");
   }
   
   DBG("SLEEPING");
   Thread::sleep(10000);
   DBG("DONE");

   return 0;
}

This simply prints:

JUCE v6.0.8
TIMER IS RUNNING
SLEEPING
DONE

If I call MessageManager::getInstance()->runDispatchLoop() after creating the ScopedJuceInitialiser_GUI I also get the same behaviour.

What is the expected output from the above program?

My observations so far as to whether JUCE’s message loop runs:

  • macOS GUI app: YES
  • Windows GUI app: YES
  • macOS CLI app: NO
  • Linux GUI app (GTK): NO

With all of the above, I am initializing JUCE on the main thread either with ScopedJuceInitialiser_GUI or initialiseJuce_GUI() and running the above test Timer.

If you sleep the main thread as you are doing in your test program then the timerCallback() won’t get called. Pumping the message thread with something like the following gets timer callbacks for me on both Linux and macOS:

int main (int argc, char* argv[])
{
    struct TimerTest  : public juce::Timer
    {
        void timerCallback() override { DBG ("timerCallback()"); }
    };

    juce::ScopedJuceInitialiser_GUI gui;
    
    TimerTest t;
    t.startTimer (50);
    
    jassert (t.isTimerRunning());
    
    juce::MessageManager::getInstance()->runDispatchLoopUntil (200);
    
    return 0;
}

What do you mean by GUI vs CLI app in this context? The type of Projucer project, or something else?

OK, I can confirm your test program works for me.

What do you mean by GUI vs CLI app in this context? The type of Projucer project, or something else?

I mean an external “native” program (not JUCE) that links to a JUCE static library.

So:

  • On Mac we are using Apple’s native frameworks
  • On Windows are using Microsoft’s native frameworks
  • On Linux we are using GTK

On Mac and Windows, everything “just works”. I call juce::initialiseJuce_GUI() on our main thread and JUCE behaves as expected. I can create windows, Component::paint() gets called, Timer::timerCallback() gets called etc. This is without calling MessageManager::runDispatchLoop().

With GTK it does not work.

  • If I call juce::initialiseJuce_GUI() on our main thread (same code as Mac and Windows) then I do not get calls to Component::paint() or Timer::timerCallback()
  • If I call MessageManager::runDispatchLoop() as in the above CLI example, it just blocks the main thread and of course, the UI stops responding

This begs some questions:

  • Why does the same code work on Windows and Mac but not on Linux?
  • Am I doing something wrong? Should I be running JUCE on a background thread and using MessageManager::runDispatchLoop() on all platforms?
  • Should I be repeatedly calling juce::MessageManager::runDispatchLoopUntil() in our application’s main event loop?

If it would be helpful, I can post a simple GTK linking to JUCE on GitHub to illustrate the problem.

Unlike macOS or Windows, on Linux there’s no concept of an application’s main “run loop” for your external program to hook into. I’m not too familiar with GTK but if it expects to run the main loop then you’ll need to register the JUCE input event file descriptors and handlers. You can retreive these with the getFdReadCallbacks() method. Take a look at the VST3 wrapper code to see how we handle registering JUCE’s input handlers with an external run loop -