diff --git a/meson.build b/meson.build index e8a7132d..dd1d6904 100644 --- a/meson.build +++ b/meson.build @@ -314,16 +314,15 @@ if with_clap dependencies : clap_plugin_deps, cpp_args : compiler_options, ) - # TODO: Chainloader - # shared_library( - # 'yabridge-chainloader-clap', - # clap_chainloader_sources, - # native : true, - # dependencies : chainloader_deps, - # cpp_args : compiler_options + chainloader_compiler_options, - # # See above - # override_options : ['b_lto=true'], - # ) + shared_library( + 'yabridge-chainloader-clap', + clap_chainloader_sources, + native : true, + dependencies : clap_chainloader_deps, + cpp_args : compiler_options + chainloader_compiler_options, + # See above + override_options : ['b_lto=true'], + ) endif if with_vst3 diff --git a/src/chainloader/clap-chainloader.cpp b/src/chainloader/clap-chainloader.cpp new file mode 100644 index 00000000..2a1396d0 --- /dev/null +++ b/src/chainloader/clap-chainloader.cpp @@ -0,0 +1,159 @@ +// 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 . + +#include +#include +#include + +#include +#include + +// Generated inside of the build directory +#include + +#include "../common/linking.h" +#include "../common/utils.h" +#include "utils.h" + +// These chainloader libraries are tiny, mostly dependencyless libraries that +// `dlopen()` the actual `libyabridge-{vst2,vst3}.so` files and forward the +// entry point function calls from this library to those. Or technically, these +// libraries use dedicated entry point functions because multiple chainloader +// libraries may all dynamically link to the exact same plugin library, so we +// can't store any bridge information in a global there. This approach avoids +// wasting disk space on copies on file systems that don't support reflinking, +// but more importantly it also avoids the need to rerun `yabridgectl sync` +// whenever yabridge is updated. This is even more important when considering +// distro packaging, because updates to Boost might require the package to be +// rebuilt, which in turn would also require a resync. + +namespace fs = ghc::filesystem; + +// These functions are loaded from `libyabridge-clap.so` the first time +// `clap_entry.init` gets called +using ClapPluginBridge = void; + +ClapPluginBridge* (*yabridge_bridge_init)(const char* plugin_path) = nullptr; +void (*yabridge_bridge_free)(ClapPluginBridge* instance) = nullptr; +const void* (*yabridge_module_get_plugin_factory)(ClapPluginBridge* instance, + const char* factory_id) = + nullptr; + +/** + * The bridge instance for this chainloader. This is initialized when + * `clap_entry.init` first gets called. + */ +std::unique_ptr bridge( + nullptr, + nullptr); +/** + * The number of active instances. Incremented when `clap_entry_init()` is + * called, decremented when `clap_entry_exit()` is called. We'll initialize the + * bridge when this is first incremented from 0, and we'll free the bridge again + * when a `clap_entry_exit()` call causes this to return back to 0. + */ +std::atomic_size_t active_instances = 0; + +/** + * The first time one of the exported functions from this library gets called, + * we'll need to load the corresponding `libyabridge-*.so` file and fetch the + * the entry point functions from that file. + */ +bool initialize_library() { + static void* library_handle = nullptr; + static std::mutex library_handle_mutex; + + std::lock_guard lock(library_handle_mutex); + + // There should be no situation where this library gets loaded and then two + // threads immediately start calling functions, but we'll handle that + // situation just in case it does happen + if (library_handle) { + return true; + } + + library_handle = find_plugin_library(yabridge_clap_plugin_name); + if (!library_handle) { + return false; + } + +#define LOAD_FUNCTION(name) \ + do { \ + (name) = \ + reinterpret_cast(dlsym(library_handle, #name)); \ + if (!(name)) { \ + log_failing_dlsym(yabridge_clap_plugin_name, #name); \ + return false; \ + } \ + } while (false) + + LOAD_FUNCTION(yabridge_bridge_init); + LOAD_FUNCTION(yabridge_bridge_free); + LOAD_FUNCTION(yabridge_module_get_plugin_factory); + +#undef LOAD_FUNCTION + + return true; +} + +bool clap_entry_init(const char* /*plugin_path*/) { + // This function can be called multiple times, so we should make sure to + // only initialize the bridge on the first call + if (active_instances.fetch_add(1, std::memory_order_seq_cst) == 0) { + if (!initialize_library()) { + return false; + } + + // You can't change the deleter function with `.reset()` so we'll need + // this abomination instead + // XXX: The host also provides us with the plugin path which we could + // just use instead. Should we? The advantage of doing it this way + // instead is that we'll have consistent behavior between all + // plugin formats. + const fs::path this_plugin_path = get_this_file_location(); + bridge = + decltype(bridge)(yabridge_bridge_init(this_plugin_path.c_str()), + yabridge_bridge_free); + if (!bridge) { + return false; + } + } + + return true; +} + +void clap_entry_deinit() { + // We'll free the bridge when this exits brings the reference count back to + // zero + if (active_instances.fetch_sub(1, std::memory_order_seq_cst) == 1) { + bridge.reset(); + } +} + +const void* clap_entry_get_factory(const char* factory_id) { + // The host should have called `clap_entry.init` first + assert(bridge); + assert(factory_id); + + return yabridge_module_get_plugin_factory(bridge.get(), factory_id); +} + +CLAP_EXPORT const clap_plugin_entry_t clap_entry = { + .clap_version = CLAP_VERSION_INIT, + .init = clap_entry_init, + .deinit = clap_entry_deinit, + .get_factory = clap_entry_get_factory, +}; diff --git a/src/chainloader/meson.build b/src/chainloader/meson.build index d2b5387a..2b9bfff6 100644 --- a/src/chainloader/meson.build +++ b/src/chainloader/meson.build @@ -9,6 +9,10 @@ chainloader_deps = [ rt_dep, ] +if with_clap + clap_chainloader_deps = chainloader_deps + [clap_dep] +endif + vst2_chainloader_sources = files( '../common/logging/common.cpp', '../common/linking.cpp', @@ -18,6 +22,17 @@ vst2_chainloader_sources = files( 'vst2-chainloader.cpp', ) +if with_clap + clap_chainloader_sources = files( + '../common/logging/common.cpp', + '../common/linking.cpp', + '../common/notifications.cpp', + '../common/process.cpp', + 'utils.cpp', + 'clap-chainloader.cpp', + ) +endif + if with_vst3 vst3_chainloader_sources = files( '../common/logging/common.cpp', diff --git a/src/common/config/config.h.in b/src/common/config/config.h.in index 779c87ce..712655a5 100644 --- a/src/common/config/config.h.in +++ b/src/common/config/config.h.in @@ -16,6 +16,11 @@ #pragma once +/** + * The name of yabridge's CLAP library, e.g. `libyabridge-clap.so`. + */ +constexpr char yabridge_clap_plugin_name[] = "@clap_plugin_name@"; + /** * The name of yabridge's VST2 library, e.g. `libyabridge-vst2.so`. */