From f9ff49797878522e1f6b1979b6835faafd49db6f Mon Sep 17 00:00:00 2001 From: attila Date: Fri, 3 Feb 2023 12:54:58 +0100 Subject: [PATCH] CMake: Add embedded Linux subprocess for WebView support In order to display a WebKit based webview a plugin will deploy a temporary standalone executable on the system and host the WebKit instance inside that. --- CMakeLists.txt | 2 +- docs/CMake API.md | 15 ++++ extras/Build/CMake/JUCEUtils.cmake | 89 ++++++++++++++++++- .../CMake/juce_LinuxSubprocessHelper.cpp | 36 ++++++++ .../messages/juce_ApplicationBase.cpp | 4 +- .../juce_linux_X11_WebBrowserComponent.cpp | 89 +++++++++++++------ 6 files changed, 204 insertions(+), 31 deletions(-) create mode 100644 extras/Build/CMake/juce_LinuxSubprocessHelper.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4289d06721..6814814c0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ install(FILES "${JUCE_BINARY_DIR}/JUCEConfigVersion.cmake" "${JUCE_CMAKE_UTILS_DIR}/checkBundleSigning.cmake" "${JUCE_CMAKE_UTILS_DIR}/copyDir.cmake" "${JUCE_CMAKE_UTILS_DIR}/juce_runtime_arch_detection.cpp" + "${JUCE_CMAKE_UTILS_DIR}/juce_LinuxSubprocessHelper.cpp" DESTINATION "${JUCE_INSTALL_DESTINATION}") if("${CMAKE_SOURCE_DIR}" STREQUAL "${JUCE_SOURCE_DIR}") @@ -170,4 +171,3 @@ if("${CMAKE_SOURCE_DIR}" STREQUAL "${JUCE_SOURCE_DIR}") install(EXPORT LV2_HELPER NAMESPACE juce:: DESTINATION "${JUCE_INSTALL_DESTINATION}") endif() endif() - diff --git a/docs/CMake API.md b/docs/CMake API.md index ad99f6a9a7..6586d77832 100644 --- a/docs/CMake API.md +++ b/docs/CMake API.md @@ -764,6 +764,21 @@ This function sets the `CMAKE__FLAGS_` to empty in the current direc allowing alternative optimisation/debug flags to be supplied without conflicting with the CMake-supplied defaults. +#### `juce_link_with_embedded_linux_subprocess` + + juce_link_with_embedded_linux_subprocess() + +This function links the provided target with an interface library that generates a barebones +standalone executable file and embeds it as a binary resource. This binary resource is only used +by the `juce_gui_extra` module and only when its `JUCE_WEB_BROWSER` capability is enabled. This +executable will then be deployed into a temporary file only when the code is running in a +non-standalone format, and will be used to host a WebKit view. This technique is used by audio +plugins on Linux. + +This function is automatically called if necessary for all targets created by one of the JUCE target +creation functions i.e. `juce_add_gui_app`, `juce_add_console_app` and `juce_add_gui_app`. You don't +need to call this function manually in these cases. + ### Targets #### `juce::juce_recommended_warning_flags` diff --git a/extras/Build/CMake/JUCEUtils.cmake b/extras/Build/CMake/JUCEUtils.cmake index 9e0475bc5b..5744f6c0df 100644 --- a/extras/Build/CMake/JUCEUtils.cmake +++ b/extras/Build/CMake/JUCEUtils.cmake @@ -147,6 +147,86 @@ endfunction() # ================================================================================================== +function(_juce_create_linux_subprocess_helper_target) + if(TARGET juce_linux_subprocess_helper) + return() + endif() + + set(source "${JUCE_CMAKE_UTILS_DIR}/juce_LinuxSubprocessHelper.cpp") + + add_executable(juce_linux_subprocess_helper ${source}) + add_executable(juce::juce_linux_subprocess_helper ALIAS juce_linux_subprocess_helper) + target_compile_features(juce_linux_subprocess_helper PRIVATE cxx_std_17) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) + target_link_libraries(juce_linux_subprocess_helper PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) +endfunction() + +function(_juce_create_embedded_linux_subprocess_target output_target_name target) + # This library needs to be created in every directory where a target wants to link against it. + # Pre CMake 3.20 the GENERATED property is only visible in the directory of the current target, + # and even post 3.20, CMake will not know how to generate the sources outside this directory. + set(target_directory_key JUCE_EMBEDDED_LINUX_SUBPROCESS_TARGET) + get_directory_property(embedded_linux_subprocess_target ${target_directory_key}) + + if(embedded_linux_subprocess_target) + set(${output_target_name} juce::${embedded_linux_subprocess_target} PARENT_SCOPE) + return() + endif() + + set(prefix "_juce_directory_prefix") + set(embedded_linux_subprocess_target ${prefix}_embedded_linux_subprocess) + + while(TARGET ${embedded_linux_subprocess_target}) + set(embedded_linux_subprocess_target ${prefix}_${embedded_linux_subprocess_target}) + endwhile() + + _juce_create_linux_subprocess_helper_target() + + get_target_property(generated_sources_directory ${target} JUCE_GENERATED_SOURCES_DIRECTORY) + + if(generated_sources_directory) + set(juce_linux_subprocess_helper_binary_dir "${generated_sources_directory}") + else() + set(juce_linux_subprocess_helper_binary_dir "${CMAKE_CURRENT_BINARY_DIR}/juce_LinuxSubprocessHelper") + endif() + + set(binary_header_file "${juce_linux_subprocess_helper_binary_dir}/juce_LinuxSubprocessHelperBinaryData.h") + set(binary_source_file "${juce_linux_subprocess_helper_binary_dir}/juce_LinuxSubprocessHelperBinaryData.cpp") + set(juceaide_input_file "${juce_linux_subprocess_helper_binary_dir}/input_file_list") + + file(GENERATE OUTPUT ${juceaide_input_file} CONTENT "$") + file(MAKE_DIRECTORY ${juce_linux_subprocess_helper_binary_dir}) + + add_custom_command( + OUTPUT + ${binary_header_file} + ${binary_source_file} + COMMAND juce::juceaide binarydata "LinuxSubprocessHelperBinaryData" "${binary_header_file}" + ${juce_linux_subprocess_helper_binary_dir} "${juceaide_input_file}" + COMMAND + ${CMAKE_COMMAND} -E rename "${juce_linux_subprocess_helper_binary_dir}/BinaryData1.cpp" "${binary_source_file}" + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + DEPENDS juce_linux_subprocess_helper + VERBATIM) + + add_library(${embedded_linux_subprocess_target} INTERFACE) + target_sources(${embedded_linux_subprocess_target} INTERFACE ${binary_source_file}) + target_include_directories(${embedded_linux_subprocess_target} INTERFACE ${juce_linux_subprocess_helper_binary_dir}) + target_compile_definitions(${embedded_linux_subprocess_target} INTERFACE JUCE_USE_EXTERNAL_TEMPORARY_SUBPROCESS=1) + add_library(juce::${embedded_linux_subprocess_target} ALIAS ${embedded_linux_subprocess_target}) + + set_directory_properties(PROPERTIES ${target_directory_key} ${embedded_linux_subprocess_target}) + set(${output_target_name} juce::${embedded_linux_subprocess_target} PARENT_SCOPE) +endfunction() + +function(juce_link_with_embedded_linux_subprocess target) + _juce_create_embedded_linux_subprocess_target(embedded_linux_subprocess_target ${target}) + target_link_libraries(${target} PRIVATE ${embedded_linux_subprocess_target}) +endfunction() + +# ================================================================================================== + # Ideally, we'd check the preprocessor defs on the target to see whether # JUCE_USE_CURL, JUCE_WEB_BROWSER, or JUCE_IN_APP_PURCHASES have been explicitly turned off, # and then link libraries as appropriate. @@ -167,6 +247,12 @@ function(_juce_link_optional_libraries target) if(needs_browser) target_link_libraries(${target} PRIVATE juce::pkgconfig_JUCE_BROWSER_LINUX_DEPS) + + get_target_property(is_plugin ${target} JUCE_IS_PLUGIN) + + if(is_plugin) + juce_link_with_embedded_linux_subprocess(${target}) + endif() endif() elseif(APPLE) get_target_property(needs_storekit ${target} JUCE_NEEDS_STORE_KIT) @@ -846,10 +932,9 @@ function(_juce_add_lv2_manifest_helper_target) add_executable(juce::juce_lv2_helper ALIAS juce_lv2_helper) target_compile_features(juce_lv2_helper PRIVATE cxx_std_17) set_target_properties(juce_lv2_helper PROPERTIES BUILD_WITH_INSTALL_RPATH ON) - target_link_libraries(juce_lv2_helper PRIVATE ${CMAKE_DL_LIBS}) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) - target_link_libraries(juce_lv2_helper PRIVATE Threads::Threads) + target_link_libraries(juce_lv2_helper PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) endfunction() # ================================================================================================== diff --git a/extras/Build/CMake/juce_LinuxSubprocessHelper.cpp b/extras/Build/CMake/juce_LinuxSubprocessHelper.cpp new file mode 100644 index 0000000000..c690442f06 --- /dev/null +++ b/extras/Build/CMake/juce_LinuxSubprocessHelper.cpp @@ -0,0 +1,36 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +int main (int argc, const char* const* argv) +{ + if (argc >= 3) + if (auto* handle = dlopen (argv[1], RTLD_LAZY)) + if (auto* function = reinterpret_cast (dlsym (handle, argv[2]))) + return function (argc - 3, argv + 3); + + return 1; +} diff --git a/modules/juce_events/messages/juce_ApplicationBase.cpp b/modules/juce_events/messages/juce_ApplicationBase.cpp index 04f534bef6..7b0eb87252 100644 --- a/modules/juce_events/messages/juce_ApplicationBase.cpp +++ b/modules/juce_events/messages/juce_ApplicationBase.cpp @@ -184,7 +184,7 @@ StringArray JUCE_CALLTYPE JUCEApplicationBase::getCommandLineParameterArray() #endif #if (JUCE_LINUX || JUCE_BSD) && JUCE_MODULE_AVAILABLE_juce_gui_extra && (! defined(JUCE_WEB_BROWSER) || JUCE_WEB_BROWSER) - extern int juce_gtkWebkitMain (int argc, const char* argv[]); + extern "C" int juce_gtkWebkitMain (int argc, const char* const* argv); #endif #if JUCE_WINDOWS @@ -231,7 +231,7 @@ int JUCEApplicationBase::main (int argc, const char* argv[]) initialiseNSApplication(); #endif - #if (JUCE_LINUX || JUCE_BSD) && JUCE_MODULE_AVAILABLE_juce_gui_extra && (! defined(JUCE_WEB_BROWSER) || JUCE_WEB_BROWSER) + #if (JUCE_LINUX || JUCE_BSD) && JUCE_MODULE_AVAILABLE_juce_gui_extra && (! defined (JUCE_WEB_BROWSER) || JUCE_WEB_BROWSER) if (argc >= 2 && String (argv[1]) == "--juce-gtkwebkitfork-child") return juce_gtkWebkitMain (argc, argv); #endif diff --git a/modules/juce_gui_extra/native/juce_linux_X11_WebBrowserComponent.cpp b/modules/juce_gui_extra/native/juce_linux_X11_WebBrowserComponent.cpp index 69851c483d..273be7b5cf 100644 --- a/modules/juce_gui_extra/native/juce_linux_X11_WebBrowserComponent.cpp +++ b/modules/juce_gui_extra/native/juce_linux_X11_WebBrowserComponent.cpp @@ -23,6 +23,10 @@ ============================================================================== */ +#if JUCE_USE_EXTERNAL_TEMPORARY_SUBPROCESS + #include "juce_LinuxSubprocessHelperBinaryData.h" +#endif + namespace juce { @@ -215,7 +219,7 @@ private: JUCE_IMPLEMENT_SINGLETON (WebKitSymbols) //============================================================================== -extern int juce_gtkWebkitMain (int argc, const char* argv[]); +extern "C" int juce_gtkWebkitMain (int argc, const char* const* argv); class CommandReceiver { @@ -776,34 +780,68 @@ private: ret = pipe (outPipe); jassert (ret == 0); + std::vector arguments; + + #if JUCE_USE_EXTERNAL_TEMPORARY_SUBPROCESS + if (! JUCEApplicationBase::isStandaloneApp()) + { + subprocessFile.emplace ("_juce_linux_subprocess"); + + const auto externalSubprocessAvailable = subprocessFile->getFile().replaceWithData (LinuxSubprocessHelperBinaryData::juce_linux_subprocess_helper, + LinuxSubprocessHelperBinaryData::juce_linux_subprocess_helperSize) + && subprocessFile->getFile().setExecutePermission (true); + + ignoreUnused (externalSubprocessAvailable); + jassert (externalSubprocessAvailable); + + /* The external subprocess will load the .so specified as its first argument and execute + the function specified by the second. The remaining arguments will be passed on to + the function. + */ + arguments.emplace_back (subprocessFile->getFile().getFullPathName()); + arguments.emplace_back (File::getSpecialLocation (File::currentExecutableFile).getFullPathName()); + arguments.emplace_back ("juce_gtkWebkitMain"); + } + #endif + + arguments.emplace_back (File::getSpecialLocation (File::currentExecutableFile).getFullPathName()); + arguments.emplace_back ("--juce-gtkwebkitfork-child"); + arguments.emplace_back (outPipe[0]); + arguments.emplace_back (inPipe [1]); + + if (userAgent.isNotEmpty()) + arguments.emplace_back (userAgent); + + std::vector argv (arguments.size() + 1, nullptr); + std::transform (arguments.begin(), arguments.end(), argv.begin(), [] (const auto& arg) + { + return arg.toRawUTF8(); + }); + auto pid = fork(); + if (pid == 0) { close (inPipe[0]); close (outPipe[1]); - StringArray arguments; - - arguments.add (File::getSpecialLocation (File::currentExecutableFile).getFullPathName()); - arguments.add ("--juce-gtkwebkitfork-child"); - arguments.add (String (outPipe[0])); - arguments.add (String (inPipe [1])); - - if (userAgent.isNotEmpty()) - arguments.add (userAgent); - - std::vector argv; - argv.reserve (static_cast (arguments.size() + 1)); - - for (const auto& arg : arguments) - argv.push_back (arg.toRawUTF8()); - - argv.push_back (nullptr); - if (JUCEApplicationBase::isStandaloneApp()) + { execv (arguments[0].toRawUTF8(), (char**) argv.data()); + } else - juce_gtkWebkitMain (arguments.size(), (const char**) argv.data()); + { + #if JUCE_USE_EXTERNAL_TEMPORARY_SUBPROCESS + execv (arguments[0].toRawUTF8(), (char**) argv.data()); + #else + // After a fork in a multithreaded program, the child can only safely call + // async-signal-safe functions until it calls execv, but if we reached this point + // then execv won't be called at all! The following call is unsafe, and is very + // likely to lead to unexpected behaviour. + jassertfalse; + juce_gtkWebkitMain ((int) arguments.size(), argv.data()); + #endif + } exit (0); } @@ -881,12 +919,10 @@ private: void handleCommand (const String& cmd, const var& params) override { - MessageManager::callAsync ([liveness = std::weak_ptr (livenessProbe), this, cmd, params]() + MessageManager::callAsync ([liveness = std::weak_ptr (livenessProbe), this, cmd, params] { - if (liveness.lock() == nullptr) - return; - - handleCommandOnMessageThread (cmd, params); + if (liveness.lock() != nullptr) + handleCommandOnMessageThread (cmd, params); }); } @@ -903,6 +939,7 @@ private: std::unique_ptr xembed; std::shared_ptr livenessProbe = std::make_shared (0); std::vector pfds; + std::optional subprocessFile; }; //============================================================================== @@ -1015,7 +1052,7 @@ bool WebBrowserComponent::areOptionsSupported (const Options& options) return (options.getBackend() == Options::Backend::defaultBackend); } -int juce_gtkWebkitMain (int argc, const char* argv[]) +extern "C" __attribute__ ((visibility ("default"))) int juce_gtkWebkitMain (int argc, const char* const* argv) { if (argc < 4) return -1;