Visibility settings in JUCEUtils.cmake

Hi there,

I’m seeing an issue related to visibility settings in JUCEUtils.cmake. In particular in functions _juce_link_plugin_wrapper and _juce_configure_plugin_targets. If visibility is set to:

VISIBILITY_INLINES_HIDDEN TRUE

C_VISIBILITY_PRESET hidden

CXX_VISIBILITY_PRESET hidden

it can cause bad_any_cast exceptions when using types across library boundaries. I’ve only seen this happening when compiling in Release + Apple ARM.

If visibility were set to:

VISIBILITY_INLINES_HIDDEN FALSE

C_VISIBILITY_PRESET default

CXX_VISIBILITY_PRESET default

then the exception does not occur.

For future versions of juce I would propose that we can change those visibility settings from outside without needing to modify the juce source code.

thanks,

eduard

What are you actually trying to do? For plugin targets, setting the symbol visibility to hidden is essential, and changing this will cause problems in the future. Imagine if the host uses a different JUCE version, or loads another plugin that contains a different JUCE version. If the symbols are not hidden, JUCE function calls from outside your plugin might end up calling the implementations inside your plugin, which is very likely to cause difficult-to-diagnose crashes.

Changing the visibility is risky, so we are unlikely to make these settings configurable. I’d recommend looking for a different solution to your problem. If you can provide some more details, then we may be able to offer some suggestions.

yeah, that makes perfect sense. I’ve tried changing visibility for some symbols (this only happens with non POD types such as std::vector). The plugin seems to work fine, but unit tests throw bad_any_cast. If I can come up with a simple example, I will post it.

Thanks

It’ll be happening with anything under std::any .. so - apropos those unit tests - are you able to type check your way around it, like:

if (value.type() == typeid(std::string)) {
    std::string result = std::any_cast<std::string>(value);
} else {
    std::cerr << "Type mismatch: expected std::string, got " << value.type().name() << std::endl;
}

.. and I also note that non-visibile is a sensible default from the perspective of cross-platform library/module packaging, vis a vis end-user bundles/sandboxing…

How are your unit tests built/linked? If you’re currently loading the plugin as a shared library for testing purposes, maybe you could statically link the plugin’s “shared code” target instead. Another idea would be to structure your plugin code as a JUCE module, and then to link your unit test executable against this module.

@ibisum typeids match, it’s a problem that only shows up in macOS arm architecture when compiled in Release or RelWithDebInfo. The types that seem to be affected are complex structs that are defined in a third party dependency which is consumed via fetch_content. Changing visibility with __attribute__ ((visibility("default"))) in front of those structs does fix it. I’m not sure how/why those visibility settings set by juce at a target level should affect this third party library.

@reuk the unit tests are built as a normal cmake target which links against the plugin’s share code statically.

Here’s a minimal example that tries to mimmic what happens. There are three actors: types, storage and app. Types is supposed to be a third party library where the “complex” type is defined, storage simulates the plugin’s shared code by setting the visibility settings to hidden, and finally app is the application that links against the library and exhibits the std::bad_any_cast problem.
As far as I know, it can only be reproduced in Release mode under Apple ARM arch.
Uncomment the line in Types/types.h to fix the problem, however that is supposed to be a third party library that should not be touched.

juce_bad_any_cast.zip (6.1 KB)

I see two potential problems.

The main problem is that you’re linking storage like so:

target_link_libraries(storage PUBLIC juce::juce_core ...)

Then, later you link the app target like so:

target_link_libraries(app PUBLIC storage juce::juce_audio_processors)

JUCE modules are CMake “interface” targets, which means that their source files get separately built into each target that links against them privately or publicly. So, the lines above will build juce_core once for storage, and again for app. The linker will end up seeing two copies of all definitions in juce_core, which breaks the One Definition Rule and results in undefined behaviour, such as crashes.

I can think of a couple of solutions to this first problem:

  • Recommended: restructure your “storage” code as a JUCE module, using the docs in docs/JUCE Module Format.md. You can make CMake aware of the module by calling juce_add_module. Then, link your app target PRIVATEly against your storage module and any other JUCE modules. This addresses the problem by ensuring that each module will be built, once, directly into the final executable.
  • Advanced: Use the approach described in this post to build the JUCE modules used by your app (juce_audio_processors and its dependencies) into a single static library. Then, storage and app can both link PUBLICly against this static library. If you follow this approach, make sure that the new static library is the only target that links directly against any JUCE modules. All other targets must link only against this new static library target. This addresses the problem by building all JUCE modules once into a static library, on which other libraries may depend.

The second problem I see is that you’re using add_executable instead of juce_add_console_app or juce_add_gui_app to define your executable target. This is generally not recommended, as the juce functions do some useful work including setting up necessary preprocessor definitions on the executable target.

@reuk apologies cause the cmake wasn’t clean enough, app (i.e the unittest) should not link against juce::juce_audio_processorsm and should only link against storage (i.e. the plugin shared code). So only storage has a dependency with juce and should be the only target building the juce sources.
In that scenario the problem still occurs.
Note that in the case of the real plugin I do use juce_add_plugin, here in this example I’m just trying to use the minimum to show the problem I’m facing.

@reuk creating a juce_module seems to do the trick in the example I posted. Will try to do that with plugins too and report back.

unfortunately that didn’t seem to work when generating a plugin. I made sure no other library or module was linking against any juce::juce_ module a part from the single static juce library I created.

@reuk using juce_add_console_app to create unit_tests and link against the plugin shared code doesn’t work cause JUCE_STANDALONE_APPLICATION will be defined twice once as JucePlugin_Build_Standalone and another one as 1. So I can’t use juce_add_console_app.

@reuk I think it’s all good, the problem is that unit tests link against the shared code generated by juce which has changed the visibility, so when in a unit test we try to cast to a hidden type, it throws bad_any_cast cause they aren’t the same at this point.
I think JUCEUtils.cmake should at least honor CMAKE_VISIBILITY_INLINES_HIDDEN and/or CMAKE_<LANG>_VISIBILITY_PRESET if they were set, this would allow to at least be able to quickly change it temporarily, without having to dig into and change juce source code.