Compile juce modules to static libraries

Hello, i know that this question was already asked some time ago, but with the change to cmake i wanted to re-open this discussion.
With all the functionality and capabilities that cmake provides, would it be possible to compile the juce-modules to static libraries, setup the dependencies accordingly and then just link it, say, against the shared code of an audio plugin?
I know that it was said that there are some interdependencies between modules, but i dont see why this couldn’t be handled via cmake too.
For me this would feel like the next logical step, how does the rest here feel about this?

1 Like

This is possible, but not necessarily recommended. In particular, if you choose to build JUCE as a staticlib, you should ensure that all your targets only link your custom “juce static lib” target, and don’t try to pull in extra modules (that is, the staticlib target should be the only target linking JUCE modules). JUCE modules export a JUCE_MODULE_AVAILABLE_modulename=1 preprocessor definition - in order to work properly, it’s important that all modules see the same set of AVAILABLE flags.

To actually build the staticlib, something like this should work:

add_library(my_plugin_modules STATIC)

target_link_libraries(my_plugin_modules
    PRIVATE
        juce::juce_audio_utils
        juce::juce_dsp
        # If you're using your own JUCE-style modules,
        # you should link those here too
    PUBLIC
        juce::juce_recommended_config_flags
        juce::juce_recommended_lto_flags
        juce::juce_recommended_warning_flags)

# We're linking the modules privately, but we need to export
# their compile flags
target_compile_definitions(my_plugin_modules
    PUBLIC
        JUCE_WEB_BROWSER=0
        JUCE_USE_CURL=0
        JucePlugin_Build_Standalone=1
        JUCE_STANDALONE_APPLICATION=JucePlugin_Build_Standalone
    INTERFACE
        $<TARGET_PROPERTY:my_plugin_modules,COMPILE_DEFINITIONS>)

# We also need to export the include directories for the modules
target_include_directories(my_plugin_modules
    INTERFACE
        $<TARGET_PROPERTY:my_plugin_modules,INCLUDE_DIRECTORIES>)

Then, in all code which depends on JUCE or a JUCE module, link my_plugin_modules instead of linking directly to any JUCE module.

1 Like

Sure, but then i’m not able to browse through the juce code any more… Wouldn’t it be a cleaner approach to add a simple static library target for all juce-modules, and use something like source_group() to order the sources in the IDE? Then the current workaround with the dummy target for listing the sources would be obsolete, and the whole juce structure would be much easier to understand?

This is surprisingly difficult. Bear in mind that each user will want a different subset of the JUCE modules, and will have their own set of preprocessor config flags. Given that each project needs to configure JUCE in a slightly unique way, it’s not practical to define a “standard” JUCE staticlib.

In cases where a staticlib is required, the technique above will allow each project to set up a JUCE staticlib in a way that makes sense for that particular project.

I think the main thing discussed here is that it is really difficult to understand the whole logic behind the JUCE CMake implementation. Simply linking my project to some precompiled library target seems like an obvious approach at first especially if you are just getting started with CMake and learned the basics from some tutorials. I found myself wondering why JUCE handles this in such a ridiculously complex way – and I’m still far from being able to understand the CMake code in the same way I’m roughly able to understand the C++ codebase :wink: This is no offense at all, I think this was a well thought design choice.

If I get it right, this is coupled to the way users were used to manage JUCE-based projects long before CMake, e.g. the project itself defines a bunch of preprocessor config flags and compiles the JUCE code along with the project specific code, using the same flags for every source file. What @vallant describes would basically mean that the single per-module static libraries would have to be compiled BEFORE the project target, leading to a different target linkage hierarchy, so a project target that manages the preprocessor flags for the module targets linked to it like it does right now would not work with CMake.

So theoretically, the major design change that would be needed to make something like that possible would be some kind of a compile flag target that has to be generated based on the project settings, which sets all those flags. All module library targets then would need to link against that compile flag target in order to be compiled with the same flags.

Do I get that right?

1 Like

Yes, exactly. One of the design goals for the CMake support was to allow users to transition existing Projucer projects to CMake without needing to modify the project structure at all.

It’s more complicated than that, even. Some modules need to reconfigure themselves at build time depending on the other modules that are available. Additionally, most modules have config flags (preprocessor definitions) which enable/disable certain features of each module. It wouldn’t really be possible to provide a single staticlib per module, as this would necessitate hard-coding a single set of config flags. We also couldn’t supply a separate staticlib for each combination of flags - this would produce an immense number of targets!

I realize of course that this, if thought through to the end, would mean a major change, and would completely change the way JUCE is consumed. I guess it would be a transition away from it being a all-encompassing framework, to it being a utility library.
I’m just wondering if this setup could make JUCE more attractive to potential users, since they could just consume a library like so many others too, without having to learn the JUCE way of doing things.

I completely understand that the handling of preprocessor flags complicates things a lot. But if we set aside all backwards-compatibility concerns for a moment, wouldn’t it be possible to move away from the preprocessor-based configuration, to a more flexible approach (possibly accepting performance losses), or is there simply too much functionality in the codebase that needs these kinds of information at compile time? I’m just trying to imagine what JUCE might look like when version 10.0.0 is released.

1 Like

Generally with more information at compile time you can gain performance and achieve smaller binaries by stripping off all unneeded stuff or even don’t even pull it in.

So there is a benefit in the way it is now compared to a monolithic library, that features all use cases and might even have to pull switches at runtime, that never change for that project.

Just my 2 pennies.

If we’re ignoring backwards compatibility, I think this sort of change would be possible in some (most?) cases, but probably not all, and the resulting structure may not be that much simpler, and may even add complexity in some cases.

Consider a flag like JUCE_USE_OGGVORBIS. Depending on whether or not this flag is set, we’ll choose whether to build library components for reading ogg-vorbis files. In addition, we’ll adjust the wav audio format so that it can handle wavs with an ogg-vorbis subformat. Finally, we adjust the definition of AudioFormatManager::addBasicFormats to add the ogg-vorbis format. Currently all of that behaviour is controlled by a single flag; if we moved away from the preprocessor, we’d probably have to require users to manually link an 'juce_ogg_vorbis` staticlib containing the definition of the vorbis audio format, and then require users to manually register this format in an AudioFormatManager before use.

This is just an example I picked at random, but I suspect that most of the config flags would have similar trade-offs. Removing the flags would force users to do more manual set-up work before using these kinds of optional features.

I like the sound of a config-less JUCE, but in practice I’m not sure that the results would be worth the cost of the change (breaking existing projects, adding complexity, and of course actually doing the work to remove the flags).

1 Like

I dream of it to be made of C++20 modules

1 Like

So, all the compiling overhead that is needed for ogg isn’t really an issue in my eyes, since this could be built as a static library too. Other than that- registering the ogg format instead of using the preprocessor-flag is exactly what i had in mind. And while the users might have to do more to setup the project (at runtime, instead of compile time too!), at least for me personally it would make it easier to understand how the modules interact with each other, and where some functionality comes from.
But i see that the preprocessor-defines are so baked-into juce that it would be hard to make such a change.

Within the C++ code, this can still be done with a preprocessor flag. Why not have something like this in the CMake:

option (JUCE_USE_OGGVORBIS "Enable ogg-vorbis support" OFF)

if (JUCE_USE_OGGVORBIS)
    target_compile_definitions (juce_audio_formats INTERFACE JUCE_USE_OGGVORBIS=1)
else()
    target_compile_definitions (juce_audio_formats INTERFACE JUCE_USE_OGGVORBIS=0)
endif()

Then users have that option exposed via the cmake configuration.

I think that allowing JUCE modules to be built as static libraries would be a very valuable feature.

1 Like

I would like to speed up the compilation of my plug-ins (it takes hours… :sweat:) and using a static library for JUCE seems to be a good approach for that. However, some of my plug-ins use the ARA extension and it seems that the approach proposed here is not compatible because the juce_audio_processors module uses preprocessor definitions defined for the plug-in such as JucePlugin_Name. Do you think there are other solutions? Or that the implementation of the ARA::PlugIn::FactoryConfig could be defined in the juce_audio_plugin_client module instead?

@PierreGuillot If you are making clean builds (such as in a CI system) and don’t already use CCache or SCCache they could be worth looking into. In my experience a static library will only decrease the compilation time not linking time. I found using S/CCache does that just as well as using a static lib for the juce modules and you are still compiling JUCE in a sanctioned way.

https://ccache.dev/

Thanks for the tip. But it’s not exactly the same, it seems to me that CCache optimizes the compilation time between two changes (two commits) but not between two plug-in compilations. I guess CCache creates a hash with the file content and the compilation instructions, but the instructions change between two plug-ins (especially the preprocessor instructions, e.g. -DJucePlugin_Name) so it can’t optimize that.