From 6b9ae78b2749588eabe67154013b6dd1d28f6290 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Thu, 3 Dec 2020 14:20:42 +0100 Subject: [PATCH] Factor out all plumbing in Vst2PluginBridge So we can reuse it in Vst3PluginBridge later. --- src/plugin/bridges/common.h | 247 ++++++++++++++++++++++++++++++++++++ src/plugin/bridges/vst2.cpp | 153 +++------------------- src/plugin/bridges/vst2.h | 58 +-------- src/plugin/bridges/vst3.h | 8 +- src/plugin/utils.cpp | 2 + 5 files changed, 273 insertions(+), 195 deletions(-) create mode 100644 src/plugin/bridges/common.h diff --git a/src/plugin/bridges/common.h b/src/plugin/bridges/common.h new file mode 100644 index 00000000..c845a2c9 --- /dev/null +++ b/src/plugin/bridges/common.h @@ -0,0 +1,247 @@ +// yabridge: a Wine VST bridge +// Copyright (C) 2020 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +// Generated inside of the build directory +#include +#include + +#include "../../common/configuration.h" +#include "../../common/utils.h" +#include "../host-process.h" + +/** + * Handles all common operations for hosting plugins such as setting up the + * plugin host process, the logger, and logging debug information on startup. + * + * @tparam Sockets the `Sockets` implementation to use. We have to initialize it + * here because we need to pass it to our `HostProcess`. + */ +template TSockets> +class PluginBridge { + public: + /** + * Sets up everything needed to start the host process. Classes deriving + * from this should call `log_init_message()` themselves after their + * initialization list. + * + * @param plugin_type The type of the plugin we're handling. + * @param plugin_path The path to the plugin. For VST2 plugins this is the + * path to the `.dll` file, and for VST3 plugins this is the path to the + * module (either a `.vst3` DLL file or a bundle). + * @param create_socket_instance A function to create a socket instance. + * Using a lambda here feels wrong, but I can't think of a better + * solution right now. + */ + template + PluginBridge(PluginType plugin_type, + const boost::filesystem::path& plugin_path, + F create_socket_instance) + : plugin_type(plugin_type), + plugin_path(plugin_path), + io_context(), + sockets(create_socket_instance(io_context)), + config(load_config_for(get_this_file_location())), + generic_logger(Logger::create_from_environment( + create_logger_prefix(sockets.base_dir))), + plugin_host( + config.group + ? std::unique_ptr(std::make_unique( + io_context, + generic_logger, + HostRequest{ + .plugin_type = plugin_type, + .plugin_path = plugin_path.string(), + .endpoint_base_dir = sockets.base_dir.string()}, + sockets, + *config.group)) + : std::unique_ptr( + std::make_unique( + io_context, + generic_logger, + HostRequest{.plugin_type = plugin_type, + .plugin_path = plugin_path.string(), + .endpoint_base_dir = + sockets.base_dir.string()}))), + has_realtime_priority(set_realtime_priority()), + wine_io_handler([&]() { io_context.run(); }) {} + + virtual ~PluginBridge(){}; + + protected: + /** + * Format and log all relevant debug information during initialization. + */ + void log_init_message() { + std::stringstream init_msg; + + init_msg << "Initializing yabridge version " << yabridge_git_version + << std::endl; + init_msg << "host: '" << plugin_host->path().string() << "'" + << std::endl; + init_msg << "plugin: '" << plugin_path.string() << "'" + << std::endl; + init_msg << "plugin type: '" << plugin_type_to_string(plugin_type) + << "'" << std::endl; + init_msg << "realtime: '" << (has_realtime_priority ? "yes" : "no") + << "'" << std::endl; + init_msg << "sockets: '" << sockets.base_dir.string() << "'" + << std::endl; + init_msg << "wine prefix: '"; + + // If the Wine prefix is manually overridden, then this should be made + // clear. This follows the behaviour of `set_wineprefix()`. + boost::process::environment env = boost::this_process::environment(); + if (!env["WINEPREFIX"].empty()) { + init_msg << env["WINEPREFIX"].to_string() << " "; + } else { + init_msg << find_wineprefix().value_or("").string(); + } + init_msg << "'" << std::endl; + + init_msg << "wine version: '" << get_wine_version() << "'" << std::endl; + init_msg << std::endl; + + // Print the path to the currently loaded configuration file and all + // settings in use. Printing the matched glob pattern could also be + // useful but it'll be very noisy and it's likely going to be clear from + // the shown values anyways. + init_msg << "config from: '" + << config.matched_file.value_or("").string() << "'" + << std::endl; + + init_msg << "hosting mode: '"; + if (config.group) { + init_msg << "plugin group \"" << *config.group << "\""; + } else { + init_msg << "individually"; + } + if (plugin_host->architecture() == LibArchitecture::dll_32) { + init_msg << ", 32-bit"; + } else { + init_msg << ", 64-bit"; + } + init_msg << "'" << std::endl; + + init_msg << "other options: "; + std::vector other_options; + if (config.cache_time_info) { + other_options.push_back("hack: time info cache"); + } + if (config.editor_double_embed) { + other_options.push_back("editor: double embed"); + } + if (!other_options.empty()) { + init_msg << join_quoted_strings(other_options) << std::endl; + } else { + init_msg << "''" << std::endl; + } + + // To make debugging easier, we'll print both unrecognized options (that + // might be left over when an option gets removed) as well as options + // have the wrong argument types + if (!config.invalid_options.empty()) { + init_msg << "invalid arguments: " + << join_quoted_strings(config.invalid_options) + << " (check the readme for more information)" << std::endl; + } + if (!config.unknown_options.empty()) { + init_msg << "unrecognized options: " + << join_quoted_strings(config.unknown_options) + << std::endl; + } + init_msg << std::endl; + + // Include a list of enabled compile-tiem features, mostly to make debug + // logs more useful + init_msg << "Enabled features:" << std::endl; +#ifdef WITH_BITBRIDGE + init_msg << "- bitbridge support" << std::endl; +#endif +#ifdef WITH_WINEDBG + init_msg << "- winedbg" << std::endl; +#endif +#ifdef WITH_VST3 + init_msg << "- VST3 support" << std::endl; +#endif +#if !(defined(WITH_BITBRIDGE) || defined(WITH_WINEDBG) || defined(WITH_VST3)) + init_msg << " " << std::endl; +#endif + init_msg << std::endl; + + for (std::string line = ""; std::getline(init_msg, line);) { + generic_logger.log(line); + } + } + + /** + * The type of the plugin we're dealing with. Passed to the host process and + * printed in the initialisation message. + */ + const PluginType plugin_type; + + /** + * The path to the plugin (`.dll` or module) being loaded in the Wine plugin + * host. + */ + const boost::filesystem::path plugin_path; + + boost::asio::io_context io_context; + + /** + * The sockets used for communication with the Wine process. + * + * @see PluginBridge::log_init_message + */ + TSockets sockets; + + /** + * The configuration for this instance of yabridge. Set based on the values + * from a `yabridge.toml`, if it exists. + * + * @see ../utils.h:load_config_for + */ + Configuration config; + + /** + * The logging facility used for this instance of yabridge. See + * `Logger::create_from_env()` for how this is configured. + * + * @see Logger::create_from_env + */ + Logger generic_logger; + + /** + * The Wine process hosting our plugins. In the case of group hosts a + * `PluginBridge` instance doesn't actually own a process, but rather either + * spawns a new detached process or it connects to an existing one. + */ + std::unique_ptr plugin_host; + + /** + * Whether this process runs with realtime priority. We'll set this _after_ + * spawning the Wine process because from my testing running wineserver with + * realtime priority can actually increase latency. + */ + bool has_realtime_priority; + + /** + * Runs the Boost.Asio `io_context` thread for logging the Wine process + * STDOUT and STDERR messages. + */ + std::jthread wine_io_handler; +}; diff --git a/src/plugin/bridges/vst2.cpp b/src/plugin/bridges/vst2.cpp index c716d8ed..c52213bd 100644 --- a/src/plugin/bridges/vst2.cpp +++ b/src/plugin/bridges/vst2.cpp @@ -16,15 +16,9 @@ #include "vst2.h" -// Generated inside of the build directory -#include -#include - #include "../../common/communication/vst2.h" -#include "../../common/utils.h" #include "../utils.h" -namespace bp = boost::process; // I'd rather use std::filesystem instead, but Boost.Process depends on // boost::filesystem namespace fs = boost::filesystem; @@ -46,42 +40,28 @@ Vst2PluginBridge& get_bridge_instance(const AEffect& plugin) { } Vst2PluginBridge::Vst2PluginBridge(audioMasterCallback host_callback) - : config(load_config_for(get_this_file_location())), - vst_plugin_path(find_vst_plugin()), + : PluginBridge(PluginType::vst2, + find_vst_plugin(), + [](boost::asio::io_context& io_context) { + return Vst2Sockets( + io_context, + generate_endpoint_base(find_vst_plugin() + .filename() + .replace_extension("") + .string()), + true); + }), // All the fields should be zero initialized because // `Vst2PluginInstance::vstAudioMasterCallback` from Bitwig's plugin // bridge will crash otherwise plugin(), - io_context(), - sockets(io_context, - generate_endpoint_base( - vst_plugin_path.filename().replace_extension("").string()), - true), host_callback_function(host_callback), - // This weird cast is not needed, but without it clang/ccls won't shut up + // TODO: This is UB, use composition with `generic_logger` instead logger(static_cast(Logger::create_from_environment( - create_logger_prefix(sockets.base_dir)))), - vst_host( - config.group - ? std::unique_ptr(std::make_unique( - io_context, - logger, - HostRequest{.plugin_type = plugin_type, - .plugin_path = vst_plugin_path.string(), - .endpoint_base_dir = sockets.base_dir.string()}, - sockets, - *config.group)) - : std::unique_ptr(std::make_unique( - io_context, - logger, - HostRequest{ - .plugin_type = plugin_type, - .plugin_path = vst_plugin_path.string(), - .endpoint_base_dir = sockets.base_dir.string()}))), - has_realtime_priority(set_realtime_priority()), - wine_io_handler([&]() { io_context.run(); }) { + create_logger_prefix(sockets.base_dir)))) { log_init_message(); + // TODO: Also move his to `PluginHost` #ifndef WITH_WINEDBG // If the Wine process fails to start, then nothing will connect to the // sockets and we'll be hanging here indefinitely. To prevent this, we'll @@ -93,7 +73,7 @@ Vst2PluginBridge::Vst2PluginBridge(audioMasterCallback host_callback) using namespace std::literals::chrono_literals; while (!st.stop_requested()) { - if (!vst_host->running()) { + if (!plugin_host->running()) { logger.log( "The Wine host process has exited unexpectedly. Check the " "output above for more information."); @@ -439,7 +419,7 @@ intptr_t Vst2PluginBridge::dispatch(AEffect* /*plugin*/, logger.log("The plugin crashed during shutdown, ignoring"); } - vst_host->terminate(); + plugin_host->terminate(); // The `stop()` method will cause the IO context to just drop all of // its work immediately and not throw any exceptions that would have @@ -619,107 +599,6 @@ void Vst2PluginBridge::set_parameter(AEffect* /*plugin*/, assert(!response.value); } -void Vst2PluginBridge::log_init_message() { - std::stringstream init_msg; - - init_msg << "Initializing yabridge version " << yabridge_git_version - << std::endl; - init_msg << "host: '" << vst_host->path().string() << "'" - << std::endl; - init_msg << "plugin: '" << vst_plugin_path.string() << "'" - << std::endl; - init_msg << "plugin type: '" << plugin_type_to_string(plugin_type) << "'" - << std::endl; - init_msg << "realtime: '" << (has_realtime_priority ? "yes" : "no") - << "'" << std::endl; - init_msg << "sockets: '" << sockets.base_dir.string() << "'" - << std::endl; - init_msg << "wine prefix: '"; - - // If the Wine prefix is manually overridden, then this should be made - // clear. This follows the behaviour of `set_wineprefix()`. - bp::environment env = boost::this_process::environment(); - if (!env["WINEPREFIX"].empty()) { - init_msg << env["WINEPREFIX"].to_string() << " "; - } else { - init_msg << find_wineprefix().value_or("").string(); - } - init_msg << "'" << std::endl; - - init_msg << "wine version: '" << get_wine_version() << "'" << std::endl; - init_msg << std::endl; - - // Print the path to the currently loaded configuration file and all - // settings in use. Printing the matched glob pattern could also be useful - // but it'll be very noisy and it's likely going to be clear from the shown - // values anyways. - init_msg << "config from: '" - << config.matched_file.value_or("").string() << "'" - << std::endl; - - init_msg << "hosting mode: '"; - if (config.group) { - init_msg << "plugin group \"" << *config.group << "\""; - } else { - init_msg << "individually"; - } - if (vst_host->architecture() == LibArchitecture::dll_32) { - init_msg << ", 32-bit"; - } else { - init_msg << ", 64-bit"; - } - init_msg << "'" << std::endl; - - init_msg << "other options: "; - std::vector other_options; - if (config.cache_time_info) { - other_options.push_back("hack: time info cache"); - } - if (config.editor_double_embed) { - other_options.push_back("editor: double embed"); - } - if (!other_options.empty()) { - init_msg << join_quoted_strings(other_options) << std::endl; - } else { - init_msg << "''" << std::endl; - } - - // To make debugging easier, we'll print both unrecognized options (that - // might be left over when an option gets removed) as well as options have - // the wrong argument types - if (!config.invalid_options.empty()) { - init_msg << "invalid arguments: " - << join_quoted_strings(config.invalid_options) - << " (check the readme for more information)" << std::endl; - } - if (!config.unknown_options.empty()) { - init_msg << "unrecognized options: " - << join_quoted_strings(config.unknown_options) << std::endl; - } - init_msg << std::endl; - - // Include a list of enabled compile-tiem features, mostly to make debug - // logs more useful - init_msg << "Enabled features:" << std::endl; -#ifdef WITH_BITBRIDGE - init_msg << "- bitbridge support" << std::endl; -#endif -#ifdef WITH_WINEDBG - init_msg << "- winedbg" << std::endl; -#endif -#ifdef WITH_VST3 - init_msg << "- VST3 support" << std::endl; -#endif -#if !(defined(WITH_BITBRIDGE) || defined(WITH_WINEDBG) || defined(WITH_VST3)) - init_msg << " " << std::endl; -#endif - init_msg << std::endl; - - for (std::string line = ""; std::getline(init_msg, line);) { - logger.log(line); - } -} - // The below functions are proxy functions for the methods defined in // `Bridge.cpp` diff --git a/src/plugin/bridges/vst2.h b/src/plugin/bridges/vst2.h index 6a2fcd1a..19d6e3de 100644 --- a/src/plugin/bridges/vst2.h +++ b/src/plugin/bridges/vst2.h @@ -23,9 +23,8 @@ #include #include "../../common/communication/vst2.h" -#include "../../common/configuration.h" #include "../../common/logging/vst2.h" -#include "../host-process.h" +#include "common.h" /** * This handles the communication between the Linux native VST2 plugin and the @@ -36,7 +35,7 @@ * for greppability reasons. The `Plugin` infix is added on the native plugin * side. */ -class Vst2PluginBridge { +class Vst2PluginBridge : PluginBridge> { public: /** * Initializes the Wine VST bridge. This sets up the sockets for event @@ -50,12 +49,6 @@ class Vst2PluginBridge { */ Vst2PluginBridge(audioMasterCallback host_callback); - /** - * The type of the plugin we're dealing with. Passed to the host process and - * printed in the initialisation message. - */ - static constexpr PluginType plugin_type = PluginType::vst2; - // The four below functions are the handlers from the VST2 API. They are // called through proxy functions in `plugin.cpp`. @@ -123,19 +116,6 @@ class Vst2PluginBridge { template void do_process(T** inputs, T** outputs, int sample_frames); - /** - * The configuration for this instance of yabridge. Set based on the values - * from a `yabridge.toml`, if it exists. - * - * @see ../utils.h:load_config_for - */ - Configuration config; - - /** - * The path to the .dll being loaded in the Wine VST host. - */ - const boost::filesystem::path vst_plugin_path; - /** * This AEffect struct will be populated using the data passed by the Wine * VST host during initialization and then passed as a pointer to the Linux @@ -144,14 +124,6 @@ class Vst2PluginBridge { AEffect plugin; private: - /** - * Format and log all relevant debug information during initialization. - */ - void log_init_message(); - - boost::asio::io_context io_context; - Vst2Sockets sockets; - /** * The thread that handles host callbacks. */ @@ -171,20 +143,11 @@ class Vst2PluginBridge { audioMasterCallback host_callback_function; /** - * The logging facility used for this instance of yabridge. See - * `Logger::create_from_env()` for how this is configured. - * - * @see Logger::create_from_env + * The logging facility used for this instance of yabridge. Wraps around + * `PluginBridge::generic_logger`. */ Vst2Logger logger; - /** - * The Wine process hosting the Windows VST plugin. - * - * @see launch_vst_host - */ - std::unique_ptr vst_host; - /** * A thread used during the initialisation process to terminate listening on * the sockets if the Wine process cannot start for whatever reason. This @@ -194,19 +157,6 @@ class Vst2PluginBridge { */ std::jthread host_guard_handler; - /** - * Whether this process runs with realtime priority. We'll set this _after_ - * spawning the Wine process because from my testing running wineserver with - * realtime priority can actually increase latency. - */ - bool has_realtime_priority; - - /** - * Runs the Boost.Asio `io_context` thread for logging the Wine process - * STDOUT and STDERR messages. - */ - std::jthread wine_io_handler; - /** * A scratch buffer for sending and receiving data during `process`, * `processReplacing` and `processDoubleReplacing` calls. diff --git a/src/plugin/bridges/vst3.h b/src/plugin/bridges/vst3.h index 50cd70ec..106201d2 100644 --- a/src/plugin/bridges/vst3.h +++ b/src/plugin/bridges/vst3.h @@ -42,6 +42,8 @@ * The naming scheme of all of these 'bridge' classes is `{,Plugin}Bridge` * for greppability reasons. The `Plugin` infix is added on the native plugin * side. + * + * TODO: Also inherit this from PluginBridge */ class Vst3PluginBridge { public: @@ -85,10 +87,8 @@ class Vst3PluginBridge { Vst3Sockets sockets; /** - * The logging facility used for this instance of yabridge. See - * `Logger::create_from_env()` for how this is configured. - * - * @see Logger::create_from_env + * The logging facility used for this instance of yabridge. Wraps around + * `PluginBridge::generic_logger`. */ Vst3Logger logger; diff --git a/src/plugin/utils.cpp b/src/plugin/utils.cpp index a9e7d894..0693b39c 100644 --- a/src/plugin/utils.cpp +++ b/src/plugin/utils.cpp @@ -81,6 +81,8 @@ fs::path find_vst_host(LibArchitecture plugin_arch, bool use_plugin_groups) { } fs::path find_vst_plugin() { + // TODO: This has to be able to differentiate between VST2 plugins and VST3 + // modules const fs::path this_plugin_path = get_this_file_location(); fs::path plugin_path(this_plugin_path);