From 2fbd14908a8a58c123fcc983cab0ce9167f0a366 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 29 Nov 2020 13:54:33 +0100 Subject: [PATCH] Split communication/vst2.h into common and VST2 So we can reuse the generic bits for our VST3 implementation. --- meson.build | 1 + src/common/communication/common.cpp | 38 ++++ src/common/communication/common.h | 319 ++++++++++++++++++++++++++++ src/common/communication/vst2.cpp | 37 ---- src/common/communication/vst2.h | 306 +------------------------- src/plugin/host-process.cpp | 2 - src/plugin/host-process.h | 3 + src/wine-host/bridges/group.cpp | 4 +- 8 files changed, 366 insertions(+), 344 deletions(-) create mode 100644 src/common/communication/common.cpp create mode 100644 src/common/communication/common.h diff --git a/meson.build b/meson.build index b7d408b9..544e1dad 100644 --- a/meson.build +++ b/meson.build @@ -103,6 +103,7 @@ include_dir = include_directories('src/include') shared_library( 'yabridge-vst2', [ + 'src/common/communication/common.cpp', 'src/common/communication/vst2.cpp', 'src/common/serialization/vst2.cpp', 'src/common/configuration.cpp', diff --git a/src/common/communication/common.cpp b/src/common/communication/common.cpp new file mode 100644 index 00000000..a4fba588 --- /dev/null +++ b/src/common/communication/common.cpp @@ -0,0 +1,38 @@ +#include "common.h" + +#include + +#include "../utils.h" + +namespace fs = boost::filesystem; + +/** + * Used for generating random identifiers. + */ +constexpr char alphanumeric_characters[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +boost::filesystem::path generate_endpoint_base(const std::string& plugin_name) { + fs::path temp_directory = get_temporary_directory(); + + std::random_device random_device; + std::mt19937 rng(random_device()); + fs::path candidate_endpoint; + do { + std::string random_id; + std::sample( + alphanumeric_characters, + alphanumeric_characters + strlen(alphanumeric_characters) - 1, + std::back_inserter(random_id), 8, rng); + + // We'll get rid of the file descriptors immediately after accepting the + // sockets, so putting them inside of a subdirectory would only leave + // behind an empty directory + std::ostringstream socket_name; + socket_name << "yabridge-" << plugin_name << "-" << random_id; + + candidate_endpoint = temp_directory / socket_name.str(); + } while (fs::exists(candidate_endpoint)); + + return candidate_endpoint; +} diff --git a/src/common/communication/common.h b/src/common/communication/common.h new file mode 100644 index 00000000..1bce69a4 --- /dev/null +++ b/src/common/communication/common.h @@ -0,0 +1,319 @@ +// 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 + +#include +#include + +#ifdef __WINE__ +#include "../wine-host/boost-fix.h" +#endif +#include +#include +#include +#include +#include + +template +using OutputAdapter = bitsery::OutputBufferAdapter; + +template +using InputAdapter = bitsery::InputBufferAdapter; + +/** + * Serialize an object using bitsery and write it to a socket. This will write + * both the size of the serialized object and the object itself over the socket. + * + * @param socket The Boost.Asio socket to write to. + * @param object The object to write to the stream. + * @param buffer The buffer to write to. This is useful for sending audio and + * chunk data since that can vary in size by a lot. + * + * @warning This operation is not atomic, and calling this function with the + * same socket from multiple threads at once will cause issues with the + * packets arriving out of order. + * + * @relates read_object + */ +template +inline void write_object(Socket& socket, + const T& object, + std::vector& buffer) { + const size_t size = + bitsery::quickSerialization>>( + buffer, object); + + // Tell the other side how large the object is so it can prepare a buffer + // large enough before sending the data + // NOTE: We're writing these sizes as a 64 bit integers, **not** as pointer + // sized integers. This is to provide compatibility with the 32-bit + // bit bridge. This won't make any function difference aside from the + // 32-bit host application having to convert between 64 and 32 bit + // integers. + boost::asio::write(socket, + boost::asio::buffer(std::array{size})); + const size_t bytes_written = + boost::asio::write(socket, boost::asio::buffer(buffer, size)); + assert(bytes_written == size); +} + +/** + * `write_object()` with a small default buffer for convenience. + * + * @overload + */ +template +inline void write_object(Socket& socket, const T& object) { + std::vector buffer(64); + write_object(socket, object, buffer); +} + +/** + * Deserialize an object by reading it from a socket. This should be used + * together with `write_object`. This will block until the object is available. + * + * @param socket The Boost.Asio socket to read from. + * @param buffer The buffer to read into. This is useful for sending audio and + * chunk data since that can vary in size by a lot. + * + * @return The deserialized object. + * + * @throw std::runtime_error If the conversion to an object was not successful. + * @throw boost::system::system_error If the socket is closed or gets closed + * while reading. + * + * @relates write_object + */ +template +inline T read_object(Socket& socket, std::vector& buffer) { + // See the note above on the use of `uint64_t` instead of `size_t` + std::array message_length; + boost::asio::read(socket, boost::asio::buffer(message_length)); + + // Make sure the buffer is large enough + const size_t size = message_length[0]; + buffer.resize(size); + + // `boost::asio::read/write` will handle all the packet splitting and + // merging for us, since local domain sockets have packet limits somewhere + // in the hundreds of kilobytes + const auto actual_size = + boost::asio::read(socket, boost::asio::buffer(buffer)); + assert(size == actual_size); + + T object; + auto [_, success] = + bitsery::quickDeserialization>>( + {buffer.begin(), size}, object); + + if (BOOST_UNLIKELY(!success)) { + throw std::runtime_error("Deserialization failure in call: " + + std::string(__PRETTY_FUNCTION__)); + } + + return object; +} + +/** + * `read_object()` with a small default buffer for convenience. + * + * @overload + */ +template +inline T read_object(Socket& socket) { + std::vector buffer(64); + return read_object(socket, buffer); +} + +/** + * A single, long-living socket + */ +class SocketHandler { + public: + /** + * Sets up the sockets and start listening on the socket on the listening + * side. The sockets won't be active until `connect()` gets called. + * + * @param io_context The IO context the socket should be bound to. + * @param endpoint The endpoint this socket should connect to or listen on. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see Sockets::connect + */ + SocketHandler(boost::asio::io_context& io_context, + boost::asio::local::stream_protocol::endpoint endpoint, + bool listen) + : endpoint(endpoint), socket(io_context) { + if (listen) { + boost::filesystem::create_directories( + boost::filesystem::path(endpoint.path()).parent_path()); + acceptor.emplace(io_context, endpoint); + } + } + + /** + * Depending on the value of the `listen` argument passed to the + * constructor, either accept connections made to the sockets on the Linux + * side or connect to the sockets on the Wine side. + */ + void connect() { + if (acceptor) { + acceptor->accept(socket); + } else { + socket.connect(endpoint); + } + } + + /** + * Close the socket. Both sides that are actively listening will be thrown a + * `boost::system_error` when this happens. + */ + void close() { + // The shutdown can fail when the socket is already closed + boost::system::error_code err; + socket.shutdown( + boost::asio::local::stream_protocol::socket::shutdown_both, err); + socket.close(); + } + + /** + * Serialize an object and send it over the socket. + * + * @param object The object to send. + * @param buffer The buffer to use for the serialization. This is used to + * prevent excess allocations when sending audio. + * + * @throw boost::system::system_error If the socket is closed or gets closed + * during sending. + * + * @warning This operation is not atomic, and calling this function with the + * same socket from multiple threads at once will cause issues with the + * packets arriving out of order. + * + * @see write_object + * @see SocketHandler::receive_single + * @see SocketHandler::receive_multi + */ + template + inline void send(const T& object, std::vector& buffer) { + write_object(socket, object, buffer); + } + + /** + * `SocketHandler::send()` with a small default buffer for convenience. + * + * @overload + */ + template + inline void send(const T& object) { + write_object(socket, object); + } + + /** + * Read a serialized object from the socket sent using `send()`. This will + * block until the object is available. + * + * @param buffer The buffer to read into. This is used to prevent excess + * allocations when sending audio. + * + * @return The deserialized object. + * + * @throw std::runtime_error If the conversion to an object was not + * successful. + * @throw boost::system::system_error If the socket is closed or gets closed + * while reading. + * + * @relates SocketHandler::send + * + * @see read_object + * @see SocketHandler::receive_multi + */ + template + inline T receive_single(std::vector& buffer) { + return read_object(socket, buffer); + } + + /** + * `SocketHandler::receive_single()` with a small default buffer for + * convenience. + * + * @overload + */ + template + inline T receive_single() { + return read_object(socket); + } + + /** + * Start a blocking loop to receive objects on this socket. This function + * will return once the socket gets closed. + * + * @param callback A function that gets passed the received object. Since + * we'd probably want to do some more stuff after sending a reply, calling + * `send()` is the responsibility of this function. + * + * @tparam F A function type in the form of `void(T, std::vector&)` + * that does something with the object, and then calls `send()`. The + * reading/writing buffer is passed along so it can be reused for sending + * large amounts of data. + * + * @relates SocketHandler::send + * + * @see read_object + * @see SocketHandler::receive_single + */ + template + void receive_multi(F callback) { + std::vector buffer{}; + while (true) { + try { + auto object = receive_single(buffer); + + callback(std::move(object), buffer); + } catch (const boost::system::system_error&) { + // This happens when the sockets got closed because the plugin + // is being shut down + break; + } + } + } + + private: + boost::asio::local::stream_protocol::endpoint endpoint; + boost::asio::local::stream_protocol::socket socket; + + /** + * Will be used in `connect()` on the listening side to establish the + * connection. + */ + std::optional acceptor; +}; + +/** + * Generate a unique base directory that can be used as a prefix for all Unix + * domain socket endpoints used in `Vst2PluginBridge`/`Vst2Bridge`. This will + * usually return `/run/user//yabridge--/`. + * + * Sockets for group hosts are handled separately. See + * `../plugin/utils.h:generate_group_endpoint` for more information on those. + * + * @param plugin_name The name of the plugin we're generating endpoints for. + * Used as a visual indication of what plugin is using this endpoint. + */ +boost::filesystem::path generate_endpoint_base(const std::string& plugin_name); diff --git a/src/common/communication/vst2.cpp b/src/common/communication/vst2.cpp index 845507cc..da13669d 100644 --- a/src/common/communication/vst2.cpp +++ b/src/common/communication/vst2.cpp @@ -16,18 +16,6 @@ #include "vst2.h" -#include - -#include "../utils.h" - -namespace fs = boost::filesystem; - -/** - * Used for generating random identifiers. - */ -constexpr char alphanumeric_characters[] = - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - EventPayload DefaultDataConverter::read(const int /*opcode*/, const int /*index*/, const intptr_t /*value*/, @@ -80,28 +68,3 @@ intptr_t DefaultDataConverter::return_value(const int /*opcode*/, const intptr_t original) const { return original; } - -boost::filesystem::path generate_endpoint_base(const std::string& plugin_name) { - fs::path temp_directory = get_temporary_directory(); - - std::random_device random_device; - std::mt19937 rng(random_device()); - fs::path candidate_endpoint; - do { - std::string random_id; - std::sample( - alphanumeric_characters, - alphanumeric_characters + strlen(alphanumeric_characters) - 1, - std::back_inserter(random_id), 8, rng); - - // We'll get rid of the file descriptors immediately after accepting the - // sockets, so putting them inside of a subdirectory would only leave - // behind an empty directory - std::ostringstream socket_name; - socket_name << "yabridge-" << plugin_name << "-" << random_id; - - candidate_endpoint = temp_directory / socket_name.str(); - } while (fs::exists(candidate_endpoint)); - - return candidate_endpoint; -} diff --git a/src/common/communication/vst2.h b/src/common/communication/vst2.h index 1bc0e658..ba30b841 100644 --- a/src/common/communication/vst2.h +++ b/src/common/communication/vst2.h @@ -18,296 +18,8 @@ #include -#include -#include - -#ifdef __WINE__ -#include "../wine-host/boost-fix.h" -#endif -#include -#include -#include -#include -#include - #include "../logging.h" - -template -using OutputAdapter = bitsery::OutputBufferAdapter; - -template -using InputAdapter = bitsery::InputBufferAdapter; - -/** - * Serialize an object using bitsery and write it to a socket. This will write - * both the size of the serialized object and the object itself over the socket. - * - * @param socket The Boost.Asio socket to write to. - * @param object The object to write to the stream. - * @param buffer The buffer to write to. This is useful for sending audio and - * chunk data since that can vary in size by a lot. - * - * @warning This operation is not atomic, and calling this function with the - * same socket from multiple threads at once will cause issues with the - * packets arriving out of order. - * - * @relates read_object - */ -template -inline void write_object(Socket& socket, - const T& object, - std::vector& buffer) { - const size_t size = - bitsery::quickSerialization>>( - buffer, object); - - // Tell the other side how large the object is so it can prepare a buffer - // large enough before sending the data - // NOTE: We're writing these sizes as a 64 bit integers, **not** as pointer - // sized integers. This is to provide compatibility with the 32-bit - // bit bridge. This won't make any function difference aside from the - // 32-bit host application having to convert between 64 and 32 bit - // integers. - boost::asio::write(socket, - boost::asio::buffer(std::array{size})); - const size_t bytes_written = - boost::asio::write(socket, boost::asio::buffer(buffer, size)); - assert(bytes_written == size); -} - -/** - * `write_object()` with a small default buffer for convenience. - * - * @overload - */ -template -inline void write_object(Socket& socket, const T& object) { - std::vector buffer(64); - write_object(socket, object, buffer); -} - -/** - * Deserialize an object by reading it from a socket. This should be used - * together with `write_object`. This will block until the object is available. - * - * @param socket The Boost.Asio socket to read from. - * @param buffer The buffer to read into. This is useful for sending audio and - * chunk data since that can vary in size by a lot. - * - * @return The deserialized object. - * - * @throw std::runtime_error If the conversion to an object was not successful. - * @throw boost::system::system_error If the socket is closed or gets closed - * while reading. - * - * @relates write_object - */ -template -inline T read_object(Socket& socket, std::vector& buffer) { - // See the note above on the use of `uint64_t` instead of `size_t` - std::array message_length; - boost::asio::read(socket, boost::asio::buffer(message_length)); - - // Make sure the buffer is large enough - const size_t size = message_length[0]; - buffer.resize(size); - - // `boost::asio::read/write` will handle all the packet splitting and - // merging for us, since local domain sockets have packet limits somewhere - // in the hundreds of kilobytes - const auto actual_size = - boost::asio::read(socket, boost::asio::buffer(buffer)); - assert(size == actual_size); - - T object; - auto [_, success] = - bitsery::quickDeserialization>>( - {buffer.begin(), size}, object); - - if (BOOST_UNLIKELY(!success)) { - throw std::runtime_error("Deserialization failure in call: " + - std::string(__PRETTY_FUNCTION__)); - } - - return object; -} - -/** - * `read_object()` with a small default buffer for convenience. - * - * @overload - */ -template -inline T read_object(Socket& socket) { - std::vector buffer(64); - return read_object(socket, buffer); -} - -/** - * A single, long-living socket - */ -class SocketHandler { - public: - /** - * Sets up the sockets and start listening on the socket on the listening - * side. The sockets won't be active until `connect()` gets called. - * - * @param io_context The IO context the socket should be bound to. - * @param endpoint The endpoint this socket should connect to or listen on. - * @param listen If `true`, start listening on the sockets. Incoming - * connections will be accepted when `connect()` gets called. This should - * be set to `true` on the plugin side, and `false` on the Wine host side. - * - * @see Sockets::connect - */ - SocketHandler(boost::asio::io_context& io_context, - boost::asio::local::stream_protocol::endpoint endpoint, - bool listen) - : endpoint(endpoint), socket(io_context) { - if (listen) { - boost::filesystem::create_directories( - boost::filesystem::path(endpoint.path()).parent_path()); - acceptor.emplace(io_context, endpoint); - } - } - - /** - * Depending on the value of the `listen` argument passed to the - * constructor, either accept connections made to the sockets on the Linux - * side or connect to the sockets on the Wine side. - */ - void connect() { - if (acceptor) { - acceptor->accept(socket); - } else { - socket.connect(endpoint); - } - } - - /** - * Close the socket. Both sides that are actively listening will be thrown a - * `boost::system_error` when this happens. - */ - void close() { - // The shutdown can fail when the socket is already closed - boost::system::error_code err; - socket.shutdown( - boost::asio::local::stream_protocol::socket::shutdown_both, err); - socket.close(); - } - - /** - * Serialize an object and send it over the socket. - * - * @param object The object to send. - * @param buffer The buffer to use for the serialization. This is used to - * prevent excess allocations when sending audio. - * - * @throw boost::system::system_error If the socket is closed or gets closed - * during sending. - * - * @warning This operation is not atomic, and calling this function with the - * same socket from multiple threads at once will cause issues with the - * packets arriving out of order. - * - * @see write_object - * @see SocketHandler::receive_single - * @see SocketHandler::receive_multi - */ - template - inline void send(const T& object, std::vector& buffer) { - write_object(socket, object, buffer); - } - - /** - * `SocketHandler::send()` with a small default buffer for convenience. - * - * @overload - */ - template - inline void send(const T& object) { - write_object(socket, object); - } - - /** - * Read a serialized object from the socket sent using `send()`. This will - * block until the object is available. - * - * @param buffer The buffer to read into. This is used to prevent excess - * allocations when sending audio. - * - * @return The deserialized object. - * - * @throw std::runtime_error If the conversion to an object was not - * successful. - * @throw boost::system::system_error If the socket is closed or gets closed - * while reading. - * - * @relates SocketHandler::send - * - * @see read_object - * @see SocketHandler::receive_multi - */ - template - inline T receive_single(std::vector& buffer) { - return read_object(socket, buffer); - } - - /** - * `SocketHandler::receive_single()` with a small default buffer for - * convenience. - * - * @overload - */ - template - inline T receive_single() { - return read_object(socket); - } - - /** - * Start a blocking loop to receive objects on this socket. This function - * will return once the socket gets closed. - * - * @param callback A function that gets passed the received object. Since - * we'd probably want to do some more stuff after sending a reply, calling - * `send()` is the responsibility of this function. - * - * @tparam F A function type in the form of `void(T, std::vector&)` - * that does something with the object, and then calls `send()`. The - * reading/writing buffer is passed along so it can be reused for sending - * large amounts of data. - * - * @relates SocketHandler::send - * - * @see read_object - * @see SocketHandler::receive_single - */ - template - void receive_multi(F callback) { - std::vector buffer{}; - while (true) { - try { - auto object = receive_single(buffer); - - callback(std::move(object), buffer); - } catch (const boost::system::system_error&) { - // This happens when the sockets got closed because the plugin - // is being shut down - break; - } - } - } - - private: - boost::asio::local::stream_protocol::endpoint endpoint; - boost::asio::local::stream_protocol::socket socket; - - /** - * Will be used in `connect()` on the listening side to establish the - * connection. - */ - std::optional acceptor; -}; +#include "common.h" /** * Encodes the base behavior for reading from and writing to the `data` argument @@ -391,6 +103,9 @@ class DefaultDataConverter { * sets up asynchronous listeners for the socket endpoint, and then block and * handle events until the main socket is closed. * + * TODO: Factor out the on-demand socket spawning and handling logic so we can + * reuse most of this for the VST3 implementation + * * @tparam Thread The thread implementation to use. On the Linux side this * should be `std::jthread` and on the Wine side this should be `Win32Thread`. */ @@ -879,19 +594,6 @@ class Sockets { SocketHandler host_vst_control; }; -/** - * Generate a unique base directory that can be used as a prefix for all Unix - * domain socket endpoints used in `Vst2PluginBridge`/`Vst2Bridge`. This will - * usually return `/run/user//yabridge--/`. - * - * Sockets for group hosts are handled separately. See - * `../plugin/utils.h:generate_group_endpoint` for more information on those. - * - * @param plugin_name The name of the plugin we're generating endpoints for. - * Used as a visual indication of what plugin is using this endpoint. - */ -boost::filesystem::path generate_endpoint_base(const std::string& plugin_name); - /** * Unmarshall an `EventPayload` back to the representation used by VST2, pass * that value to a callback function (either `AEffect::dispatcher()` for host -> diff --git a/src/plugin/host-process.cpp b/src/plugin/host-process.cpp index 04f858f8..cbde8679 100644 --- a/src/plugin/host-process.cpp +++ b/src/plugin/host-process.cpp @@ -21,8 +21,6 @@ #include #include -#include "../common/communication/vst2.h" - namespace bp = boost::process; namespace fs = boost::filesystem; diff --git a/src/plugin/host-process.h b/src/plugin/host-process.h index 585c55a9..81e1889b 100644 --- a/src/plugin/host-process.h +++ b/src/plugin/host-process.h @@ -25,6 +25,9 @@ #include #include +// TODO: Those host process implementation now directly uses the Vst2Sockets and +// thus requires `communication/vst2.h`. We should create a simple common +// interface for this instead. #include "../common/communication/vst2.h" #include "../common/logging.h" #include "utils.h" diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp index 7d9396a3..4e0c1d4d 100644 --- a/src/wine-host/bridges/group.cpp +++ b/src/wine-host/bridges/group.cpp @@ -21,9 +21,7 @@ #include #include -// TODO: Change this to commucation/common.h after refactoring, and do the same -// thing in other places where we don't need everything from VST2 -#include "../../common/communication/vst2.h" +#include "../../common/communication/common.h" // FIXME: `std::filesystem` is broken in wineg++, at least under Wine 5.8. Any // path operation will thrown an encoding related error