From eb2b12aa2bc5122fe966ea3042deb18af4ce6226 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 24 Aug 2022 14:57:18 +0200 Subject: [PATCH] Add boilerplate for CLAP sockets --- src/common/communication/clap.h | 285 ++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/common/communication/clap.h diff --git a/src/common/communication/clap.h b/src/common/communication/clap.h new file mode 100644 index 00000000..f367dde7 --- /dev/null +++ b/src/common/communication/clap.h @@ -0,0 +1,285 @@ +// yabridge: a Wine plugin bridge +// Copyright (C) 2020-2022 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 + +#include "../logging/clap.h" +#include "../serialization/clap.h" +#include "common.h" + +/** + * Manages all the sockets used for communicating between the plugin and the + * Wine host when hosting a CLAP plugin. + * + * On the plugin side this class should be initialized with `listen` set to + * `true` before launching the Wine plugin host. This will start listening on + * the sockets, and the call to `connect()` will then accept any incoming + * connections. + * + * We'll have a host -> plugin connection for sending control messages (which is + * just a made up term to more easily differentiate between the two directions), + * and a plugin -> host connection to allow the plugin to make callbacks. Both + * of these connections are capable of spawning additional sockets and threads + * as needed. + * + * Every plugin instance gets a dedicated audio thread socket so they can be + * addressed concurrently. + * + * @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`. + */ +template +class ClapSockets final : public Sockets { + public: + /** + * Sets up the sockets using the specified base directory. The sockets won't + * be active until `connect()` gets called. + * + * @param io_context The IO context the sockets should be bound to. Relevant + * when doing asynchronous operations. + * @param endpoint_base_dir The base directory that will be used for the + * Unix domain sockets. + * @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 ClapSockets::connect + */ + ClapSockets(asio::io_context& io_context, + const ghc::filesystem::path& endpoint_base_dir, + bool listen) + : Sockets(endpoint_base_dir), + host_plugin_control_( + io_context, + (base_dir_ / "host_plugin_control.sock").string(), + listen), + plugin_host_callback_( + io_context, + (base_dir_ / "plugin_host_callback.sock").string(), + listen), + io_context_(io_context) {} + + // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) + ~ClapSockets() noexcept override { close(); } + + void connect() override { + host_plugin_control_.connect(); + plugin_host_callback_.connect(); + } + + void close() override { + // Manually close all sockets so we break out of any blocking operations + // that may still be active + host_plugin_control_.close(); + plugin_host_callback_.close(); + + // This map should be empty at this point, but who knows + std::lock_guard lock(audio_thread_sockets_mutex_); + for (auto& [instance_id, socket] : audio_thread_sockets_) { + socket.close(); + } + } + + /** + * Connect to the dedicated audio thread socket socket for a plugin + * instance. This should be called on the plugin side after instantiating + * the plugin. + * + * @param instance_id The object instance identifier of the socket. + */ + void add_audio_thread_and_connect(size_t instance_id) { + std::lock_guard lock(audio_thread_sockets_mutex_); + audio_thread_sockets_.try_emplace( + instance_id, io_context_, + (base_dir_ / ("host_plugin_audio_thread_" + + std::to_string(instance_id) + ".sock")) + .string(), + false); + + audio_thread_sockets_.at(instance_id).connect(); + } + + /** + * Create and listen on a dedicated audio thread socket for a plugin object + * instance. The calling thread will block until the socket has been closed. + * This should be called from the Wine plugin host side after instantiating + * the plugin. + * + * @param instance_id The object instance identifier of the socket. + * @param socket_listening_latch A promise we'll set a value for once the + * socket is being listened on so we can wait for it. Otherwise it can be + * that the native plugin already tries to connect to the socket before + * Wine plugin host is even listening on it. + * @param cb An overloaded function that can take every type `T` in the + * `ClapAudioThreadControlRequest` variant and then returns `T::Response`. + * + * @tparam F A function type in the form of `T::Response(T)` for every `T` + * in `ClapAudioThreadControlRequest::Payload`. + */ + template + void add_audio_thread_and_listen(size_t instance_id, + std::promise& socket_listening_latch, + F&& callback) { + { + std::lock_guard lock(audio_thread_sockets_mutex_); + audio_thread_sockets_.try_emplace( + instance_id, io_context_, + (base_dir_ / ("host_plugin_audio_thread_" + + std::to_string(instance_id) + ".sock")) + .string(), + true); + } + + socket_listening_latch.set_value(); + audio_thread_sockets_.at(instance_id).connect(); + + // This `true` indicates that we want to reuse our serialization and + // receiving buffers for all calls. This slightly reduces the amount of + // allocations in the audio processing loop. + audio_thread_sockets_.at(instance_id) + .template receive_messages(std::nullopt, + std::forward(callback)); + } + + /** + * If `instance_id` is in `audio_thread_sockets_`, then close its socket and + * remove it from the map. This is called when handling + * `clap_plugin::destroy` on both the plugin and the Wine sides. + * + * @param instance_id The object instance identifier of the socket. + * + * @return Whether the socket was closed and removed. Returns false if it + * wasn't in the map. + */ + bool remove_audio_thread(size_t instance_id) { + std::lock_guard lock(audio_thread_sockets_mutex_); + if (audio_thread_sockets_.contains(instance_id)) { + audio_thread_sockets_.at(instance_id).close(); + audio_thread_sockets_.erase(instance_id); + + return true; + } else { + return false; + } + } + + /** + * Send a message from the native plugin to the Wine plugin host to handle + * an audio thread function call. Since those functions are called from a + * hot loop we want every instance to have a dedicated socket and thread for + * handling those. These calls also always reuse buffers to minimize + * allocations. + * + * @tparam T Some object in the `ClapAudioThreadControlRequest` variant. All + * of these objects need to have an `instance_id` field. + */ + template + typename T::Response send_audio_thread_message( + const T& object, + std::optional> logging) { + typename T::Response response_object; + return receive_audio_thread_message_into(object, response_object, + object.instance_id, logging); + } + + /** + * Overload for use with `MessageReference`, since we cannot + * directly get the instance ID there. + */ + template + typename T::Response send_audio_thread_message( + const MessageReference& object_ref, + std::optional> logging) { + typename T::Response response_object; + return receive_audio_thread_message_into( + object_ref, response_object, object_ref.get().instance_id, logging); + } + + /** + * Alternative to `send_audio_thread_message()` for use with + * `MessageReference`, where we also want deserialize into an existing + * object to prevent allocations. Used during audio processing. + * + * TODO: Think of a better name for this + */ + template + typename T::Response& receive_audio_thread_message_into( + const MessageReference& request_ref, + typename T::Response& response_ref, + std::optional> logging) { + return receive_audio_thread_message_into( + request_ref, response_ref, request_ref.get().instance_id, logging); + } + + /** + * For sending messages from the host to the plugin. After we have a better + * idea of what our communication model looks like we'll probably want to + * provide an abstraction similar to `Vst2EventHandler`. This only handles + * main thread function calls. Audio thread calls are done using a dedicated + * socket per plugin instance. + * + * This will be listened on by the Wine plugin host when it calls + * `receive_multi()`. + */ + TypedMessageHandler + host_plugin_control_; + + /** + * For sending callbacks from the plugin back to the host. + */ + TypedMessageHandler + plugin_host_callback_; + + private: + /** + * The actual implementation for `send_audio_thread_message` and + * `receive_audio_thread_message_into`. Here we keep a thread local + * static variable for our buffers sending. + */ + template + typename T::Response& receive_audio_thread_message_into( + const T& object, + typename T::Response& response_object, + size_t instance_id, + std::optional> logging) { + thread_local SerializationBuffer<256> audio_thread_buffer{}; + + return audio_thread_sockets_.at(instance_id) + .receive_into(object, response_object, logging, + audio_thread_buffer); + } + + asio::io_context& io_context_; + + /** + * Every plugin instance gets a dedicated audio thread socket. These + * functions are always called in a hot loop, so there should not be any + * waiting or additional thread or socket creation happening there. + * + * TODO: All plugin instances have a socket for re-entrant audio thread + * function calls. This is probably not needed. It's probably useful + * to investgate why this socket was added back to the VST3 audio + * threads. + */ + std::unordered_map< + size_t, + TypedMessageHandler> + audio_thread_sockets_; + std::mutex audio_thread_sockets_mutex_; +};