diff --git a/README.md b/README.md index cdb5cb82..9cf56b44 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # yabridge -Yet Another way to use Windows VST2 plugins in Linux VST hosts. This project -aims to be as transparent as possible to achieve the best possible plugin -compatibility while also being easy to debug and maintain. +Yet Another way to use Windows VST2 plugins in Linux VST hosts, including an +optional bitbridge to run 32-bit Windows plugins in 64-bit Linux hosts. This +project aims to be as transparent as possible to achieve the best possible +plugin compatibility while also being easy to debug and maintain. ## TODOs @@ -13,7 +14,8 @@ There are a few things that should be done before releasing this, including: - Add missing details if any to the architecture section. - Rewrite parts of this readme. - Briefly verify that this also works fine in Reaper and Ardour. -- Document wine32 support. +- Finish 32-bit bitbridge. + - Upstream the updated `lib32-boost-libs` PKGBUILD. ## Tested with @@ -114,19 +116,39 @@ the following dependencies: - Boost - xcb -The following dependencies are included as a Meson wrap: +The following dependencies are included in the repository as a Meson wrap: - bitsery The project can then be compiled as follows: ```shell -meson setup --buildtype=release --cross-file cross-wine64.conf build +meson setup --buildtype=release --cross-file cross-wine.conf build ninja -C build ``` -When developing or debugging yabridge you can change the build type to either -`debug` enable debug symbols and disable optimizations. +### 32-bit bitbridge + +It's also possible to compile a 32-bit host application for yabridge that's +compatible with 32 bit plugins such as old SynthEdit plugins. This will allow +yabridge to act as a bitbirdge, allowing you to run old 32-bit only Windows VST2 +plugins in a modern 64-bit Linux VST host. For this you'll need to have +installed the 32 bit versions of the Boost and XCB libraries. This can be +enabled as follows: + +```shell +# On an existing build +meson configure build -Duse-bitbridge=true +# Configure a new build from scratch +meson setup --buildtype=release --cross-file cross-wine.conf -Duse-bitbridge=true build + +ninja -C build +``` + +This will produce two files called `yabridge-host-32.exe` and +`yabridge-host-32.exe.so`. Yabridge will detect whether the plugin you're trying +to load is 32-bit or 64-bit, and will run either `yabridge-host.exe` or +`yabridge-host-32.exe` accordingly. ## Debugging diff --git a/cross-wine.conf b/cross-wine.conf new file mode 100644 index 00000000..87b6dc4f --- /dev/null +++ b/cross-wine.conf @@ -0,0 +1,19 @@ +[binaries] +c = 'winegcc' +cpp = 'wineg++' +ar = 'ar' +strip = 'strip' + +[properties] +# These would be the arguments for 64-bit compilation. We'll set the `-m64` flag +# (or `-m32`) in our meson.build to allow the 32-bit bitbridge to be built in +# the same build directory as the plugin and the regular 64-bit version of the +# host application to ensure that they are always in sync. +# https://github.com/mesonbuild/meson/issues/5125 + +# c_args = ['-m64'] +# cpp_args = ['-m64'] +# cpp_link_args = ['-m64', '-mwindows'] + +cpp_link_args = ['-mwindows'] +needs_exe_wrapper = true diff --git a/cross-wine64.conf b/cross-wine64.conf deleted file mode 100644 index fa7247e4..00000000 --- a/cross-wine64.conf +++ /dev/null @@ -1,17 +0,0 @@ -[binaries] -c = 'winegcc' -cpp = 'wineg++' -ar = 'ar' -strip = 'strip' - -[properties] -c_args=['-m64'] -cpp_args=['-m64'] -cpp_link_args=['-m64', '-mwindows'] -needs_exe_wrapper = true - -[host_machine] -system = 'linux' -cpu_family = 'x86_64' -cpu = 'x86_64' -endian = 'little' diff --git a/meson.build b/meson.build index aee1352b..58aa0619 100644 --- a/meson.build +++ b/meson.build @@ -12,26 +12,36 @@ winelib_check = '''#ifndef __WINE__ #error 1 #endif''' if not meson.get_compiler('cpp').compiles(winelib_check) - error('You need to set up a cross compiler, check the README for more information.') + error('You need to set up a cross compiler, check the README for compilation instructions.') endif +# Depending on the `use-bitbridge` flag we'll enable building a second 32-bit +# host application that can act as a bit bridge for using 32-bit Windows plugins +# in 64-bit Linux VST hsots. The plugin will determine which host application to +# use based on the `.dll` file it's trying to load. +# This setup is necessary until Meson provides a way to have multiple +# cross-builds for a single build directory: +# https://github.com/mesonbuild/meson/issues/5125 +host_name_64bit = 'yabridge-host' +host_name_32bit = 'yabridge-host-32' + # This provides an easy way to start the Wine VST host using winedbg since it # can be quite a pain to set up compiler_options = [] if get_option('use-winedbg') compiler_options += '-DUSE_WINEDBG' endif - -# The application consists of a VST plugin (yabridge) that calls a winelib -# program (yabridge-host) that can host Windows VST plugins. More information -# about the way these two components work together can be found in the readme -# file. +if get_option('use-bitbridge') + compiler_options += '-DUSE_BITBRIDGE' +endif # Generate header files for configuration variables such as the current git tag -# and the last commit hash +# and the name of the host binary subdir('src/common/config') -boost_dep = dependency('boost', modules : ['filesystem']) +# Statically link against Boost.Filesystem, otherwise it becomes impossible to +# distribute a prebuilt version of yabridge +boost_dep = dependency('boost', modules : ['filesystem'], static : true) bitsery_dep = subproject('bitsery').get_variable('bitsery_dep') threads_dep = dependency('threads') # The built in threads dependency does not know how to handle winegcc @@ -40,6 +50,11 @@ xcb_dep = dependency('xcb') include_dir = include_directories('src/include') +# The application consists of a VST plugin (yabridge) that calls a winelib +# program (yabridge-host) that can host Windows VST plugins. More information +# about the way these two components work together can be found in the readme +# file. + shared_library( 'yabridge', [ @@ -56,21 +71,50 @@ shared_library( link_args : ['-ldl'] ) +host_sources = [ + 'src/common/logging.cpp', + 'src/common/serialization.cpp', + 'src/wine-host/editor.cpp', + 'src/wine-host/editor.cpp', + 'src/wine-host/plugin-bridge.cpp', + 'src/wine-host/vst-host.cpp', + 'src/wine-host/utils.cpp', + version_header, +] + executable( - 'yabridge-host', - [ - 'src/common/logging.cpp', - 'src/common/serialization.cpp', - 'src/wine-host/editor.cpp', - 'src/wine-host/editor.cpp', - 'src/wine-host/plugin-bridge.cpp', - 'src/wine-host/vst-host.cpp', - 'src/wine-host/utils.cpp', - version_header, - ], + host_name_64bit, + host_sources, native : false, include_directories : include_dir, dependencies : [boost_dep, bitsery_dep, wine_threads_dep, xcb_dep], - cpp_args : compiler_options, - link_args : [] + cpp_args : compiler_options + ['-m64'], + link_args : ['-m64'] ) + +if get_option('use-bitbridge') + message('Bitbridge enabled, configuring a 32-bit host application') + + # I honestly have no idea what the correct way to have `find_dependency()` use + # `/usr/lib32` instead of `/usr/lib` is. If anyone does know, please tell me! + winegcc = meson.get_compiler('cpp', native : false) + boost_dep = [winegcc.find_library('boost_filesystem')] + xcb_dep = [winegcc.find_library('xcb')] + + executable( + host_name_32bit, + host_sources, + native : false, + include_directories : include_dir, + dependencies : [boost_dep, bitsery_dep, wine_threads_dep, xcb_dep], + # FIXME: 32-bit winegcc defines `__stdcall` differently than the 64-bit + # version, and one of the changes is the inclusion of + # `__atribute__((__force_align_arg_pointer__))`. For whetever reason + # this causes GCC to complain when using function pointers with the + # `__stdcall` calling convention in template arguments, although it + # otherwise works just fine. We don't ignore any warnings in the + # regular host application so this should not cause any issues! + cpp_args : compiler_options + ['-m32', '-Wno-ignored-attributes'], + link_args : ['-m32'] + ) +endif diff --git a/meson_options.txt b/meson_options.txt index 0cf248c6..fab1ccee 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,6 +1,14 @@ +option( + 'use-bitbridge', + type : 'boolean', + value : false, + description : '''Build a 32-bit host application for hosting 32-bit plugins. + See the readme for full instructions on how to use this.''' +) + option( 'use-winedbg', type : 'boolean', value : false, - description: 'Whether to run the Wien VST host with GDB attached. Might not always be reliable.' + description : 'Whether to run the Wien VST host with GDB attached. Might not always be reliable.' ) diff --git a/src/common/communication.h b/src/common/communication.h index 54c88ef9..4cc5086c 100644 --- a/src/common/communication.h +++ b/src/common/communication.h @@ -33,7 +33,8 @@ template using InputAdapter = bitsery::InputBufferAdapter; /** - * Serialize an object using bitsery and write it to a socket. + * 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. @@ -53,8 +54,13 @@ inline void write_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})); + boost::asio::buffer(std::array{size})); const size_t bytes_written = boost::asio::write(socket, boost::asio::buffer(buffer, size)); assert(bytes_written == size); @@ -79,7 +85,8 @@ template inline T& read_object(Socket& socket, T& object, std::vector buffer = std::vector(64)) { - std::array message_length; + // 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 diff --git a/src/common/config/config.h.in b/src/common/config/config.h.in new file mode 100644 index 00000000..8f225016 --- /dev/null +++ b/src/common/config/config.h.in @@ -0,0 +1,30 @@ +// 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 + +/** + * The name of the wine host name, e.g. `yabridge-host.exe` for the regular 64 + * bit build. + */ +constexpr char yabridge_wine_host_name[] = "@host_binary_64bit@"; + +/** + * The name of the 32-bit wine host name, e.g. `yabridge-host-32.exe`.` This is + * used as a bitbridge to be able to load legacy 32-bit only Windows plugins + * from a 64-bit Linux host. + */ +constexpr char yabridge_wine_host_name_32bit[] = "@host_binary_32bit@"; diff --git a/src/common/config/meson.build b/src/common/config/meson.build index 5513065a..eb2cd4f6 100644 --- a/src/common/config/meson.build +++ b/src/common/config/meson.build @@ -1,3 +1,16 @@ +# Contains constants determined while configuring the build. As an alternative +# to preprocessor macros. +config_header = configure_file( + input : 'config.h.in', + output : 'config.h', + configuration : configuration_data( + { + 'host_binary_32bit': host_name_32bit + '.exe', + 'host_binary_64bit': host_name_64bit + '.exe', + } + ) +) + # Generate a file containing the last annotated git tag and possibly also the # amount of commits since then and the hash of the last commit version_header = vcs_tag( diff --git a/src/common/events.h b/src/common/events.h index ab861d6a..c78403bb 100644 --- a/src/common/events.h +++ b/src/common/events.h @@ -209,13 +209,16 @@ void passthrough_event(boost::asio::local::stream_protocol::socket& socket, [&](const std::vector& buffer) -> void* { return const_cast(buffer.data()); }, - [&](size_t& window_handle) -> void* { + [&](native_size_t& window_handle) -> void* { // This is the X11 window handle that the editor should // reparent itself to. We have a special wrapper around the // dispatch function that intercepts `effEditOpen` events // and creates a Win32 window and then finally embeds the // X11 window Wine created into this wnidow handle. - return reinterpret_cast(window_handle); + // Make sure to convert the window ID first to `size_t` in + // case this is the 32-bit host. + return reinterpret_cast( + static_cast(window_handle)); }, [&](const AEffect&) -> void* { return nullptr; }, [&](DynamicVstEvents& events) -> void* { diff --git a/src/common/logging.cpp b/src/common/logging.cpp index a9966c90..5d74efc5 100644 --- a/src/common/logging.cpp +++ b/src/common/logging.cpp @@ -176,7 +176,9 @@ void Logger::log_event(bool is_dispatch, [&](const std::vector& buffer) { message << "<" << buffer.size() << " byte chunk>"; }, - [&](const intptr_t&) { message << ""; }, + [&](const native_size_t& window_id) { + message << ""; + }, [&](const AEffect&) { message << ""; }, [&](const DynamicVstEvents& events) { message << "<" << events.events.size() << " midi_events>"; diff --git a/src/common/serialization.h b/src/common/serialization.h index 48f111af..1a49ea5e 100644 --- a/src/common/serialization.h +++ b/src/common/serialization.h @@ -57,6 +57,23 @@ constexpr size_t max_midi_events = max_buffer_size / sizeof(size_t); */ constexpr size_t binary_buffer_size = 50 << 20; +// The plugin should always be compiled to a 64-bit version, but the host +// application can also be 32-bit to allow using 32-bit legacy Windows VST in a +// modern Linux VST host. Because of this we have to make sure to always use +// 64-bit integers in places where we would otherwise use `size_t` and +// `intptr_t`. Otherwise the binary serialization would break. The 64 <-> 32 bit +// conversion for the 32-bit host application won't cause any issues for us +// since we can't directly pass pointers between the plugin and the host anyway. + +#ifndef __WINE__ +// Sanity check for the plugin, both the 64 and 32 bit hosts should follow these +// conventions +static_assert(std::is_same_v); +static_assert(std::is_same_v); +#endif +using native_size_t = uint64_t; +using native_intptr_t = int64_t; + // The cannonical overloading template for `std::visitor`, not sure why this // isn't part of the standard library template @@ -248,7 +265,7 @@ struct WantsString {}; using EventPayload = std::variant, - size_t, + native_size_t, AEffect, DynamicVstEvents, WantsChunkBuffer, @@ -270,7 +287,9 @@ void serialize(S& s, EventPayload& payload) { [](S& s, std::vector& buffer) { s.container1b(buffer, binary_buffer_size); }, - [](S& s, size_t& window_handle) { s.value8b(window_handle); }, + [](S& s, native_size_t& window_handle) { + s.value8b(window_handle); + }, [](S& s, AEffect& effect) { s.object(effect); }, [](S& s, DynamicVstEvents& events) { s.container( @@ -292,10 +311,7 @@ void serialize(S& s, EventPayload& payload) { struct Event { int opcode; int index; - // TODO: This is an intptr_t, if we want to support 32 bit Wine plugins all - // of these these intptr_t types should be replace by `uint64_t` to - // remain compatible with the Linux VST plugin. - intptr_t value; + native_intptr_t value; float option; /** * The event dispatch function has a void pointer parameter that's often @@ -371,7 +387,7 @@ struct EventResult { /** * The result that should be returned from the dispatch function. */ - intptr_t return_value; + native_intptr_t return_value; /** * Events typically either just return their return value or write a string * into the void pointer, but sometimes an event response should forward diff --git a/src/plugin/host-bridge.cpp b/src/plugin/host-bridge.cpp index 6398dff5..e8a07531 100644 --- a/src/plugin/host-bridge.cpp +++ b/src/plugin/host-bridge.cpp @@ -30,6 +30,7 @@ #endif // Generated inside of build directory +#include #include #include "../common/communication.h" @@ -40,11 +41,6 @@ namespace bp = boost::process; // boost::filesystem namespace fs = boost::filesystem; -/** - * The name of the wine VST host binary. - */ -constexpr auto yabridge_wine_host_name = "yabridge-host.exe"; - /** * Used for generating random identifiers. */ @@ -53,7 +49,8 @@ constexpr char alphanumeric_characters[] = std::string create_logger_prefix(const fs::path& socket_path); fs::path find_vst_plugin(); -fs::path find_wine_vst_host(); +PluginArchitecture find_plugin_architecture(fs::path); +fs::path find_wine_vst_host(PluginArchitecture plugin_arch); std::optional find_wineprefix(); fs::path generate_endpoint_name(); bp::environment set_wineprefix(); @@ -74,8 +71,9 @@ HostBridge& get_bridge_instance(const AEffect& plugin) { } HostBridge::HostBridge(audioMasterCallback host_callback) - : vst_host_path(find_wine_vst_host()), - vst_plugin_path(find_vst_plugin()), + : vst_plugin_path(find_vst_plugin()), + vst_plugin_arch(find_plugin_architecture(vst_plugin_path)), + vst_host_path(find_wine_vst_host(vst_plugin_arch)), // All the fields should be zero initialized because // `Vst2PluginInstance::vstAudioMasterCallback` from Bitwig's plugin // bridge will crash otherwise @@ -130,6 +128,21 @@ HostBridge::HostBridge(audioMasterCallback host_callback) logger.log("wineprefix: '" + find_wineprefix().value_or("").string() + "'"); + // Include a list of enabled compile-tiem features, mostly to make debug + // logs more useful + logger.log(""); + logger.log("Enabled features:"); +#ifdef USE_BITBRIDGE + logger.log("- bitbridge support"); +#endif +#ifdef USE_WINEDBG + logger.log("- winedbg"); +#endif +#if !(defined(USE_BITBRIDGE) || defined(USE_WINEDBG)) + logger.log(" "); +#endif + logger.log(""); + // It's very important that these sockets are connected to in the same // order in the Wine VST host socket_acceptor.accept(host_vst_dispatch); @@ -480,8 +493,8 @@ std::string create_logger_prefix(const fs::path& socket_path) { } /** - * Finds the Wine VST hsot (named `yabridge-host.exe`). For this we will search - * in two places: + * Finds the Wine VST hsot (either `yabridge-host.exe` or `yabridge-host.exe` + * depending on the plugin). For this we will search in two places: * * 1. Alongside libyabridge.so if the file got symlinked. This is useful * when developing, as you can simply symlink the the libyabridge.so @@ -489,23 +502,31 @@ std::string create_logger_prefix(const fs::path& socket_path) { * /usr. * 2. In the regular search path. * + * @param plugin_arch The architecture of the plugin, either 64-bit or 32-bit. + * Used to determine which host application to use, if available. + * * @return The a path to the VST host, if found. * @throw std::runtime_error If the Wine VST host could not be found. */ -fs::path find_wine_vst_host() { +fs::path find_wine_vst_host(PluginArchitecture plugin_arch) { + auto host_name = yabridge_wine_host_name; + if (plugin_arch == PluginArchitecture::vst_32) { + host_name = yabridge_wine_host_name_32bit; + } + fs::path host_path = fs::canonical(boost::dll::this_line_location()).remove_filename() / - yabridge_wine_host_name; + host_name; if (fs::exists(host_path)) { return host_path; } // Bosot will return an empty path if the file could not be found in the // search path - const fs::path vst_host_path = bp::search_path(yabridge_wine_host_name); + const fs::path vst_host_path = bp::search_path(host_name); if (vst_host_path == "") { - throw std::runtime_error("Could not locate '" + - std::string(yabridge_wine_host_name) + "'"); + throw std::runtime_error("Could not locate '" + std::string(host_name) + + "'"); } return vst_host_path; @@ -555,10 +576,66 @@ fs::path find_vst_plugin() { "VST plugin .dll file."); } - // Also resolve symlinks here, mostly for development purposes + // Also resolve symlinks here return fs::canonical(plugin_path); } +/** + * Determine the architecture of a VST plugin (or rather, a .dll file) based on + * it's header values. + * + * See https://docs.microsoft.com/en-us/windows/win32/debug/pe-format for more + * information on the PE32 format. + * + * @param plugin_path The path to the .dll file we're going to check. + * + * @return The detected architecture. + * @throw std::runtime_error If the file is not a .dll file. + */ +PluginArchitecture find_plugin_architecture(fs::path plugin_path) { + std::ifstream file(plugin_path, std::ifstream::binary | std::ifstream::in); + + // The linker will place the offset where the PE signature is placed at the + // end of the MS-DOS stub, at this offset + uint32_t pe_signature_offset; + file.seekg(0x3c); + file.read(reinterpret_cast(&pe_signature_offset), + sizeof(pe_signature_offset)); + + // The PE32 signature will be followed by a magic number. + // file >> pe_signature_offset; + uint32_t pe_signature; + uint16_t machine_type; + file.seekg(pe_signature_offset); + file.read(reinterpret_cast(&pe_signature), sizeof(pe_signature)); + file.read(reinterpret_cast(&machine_type), sizeof(machine_type)); + + constexpr char expected_pe_signature[4] = {'P', 'E', '\0', '\0'}; + if (pe_signature != + *reinterpret_cast(expected_pe_signature)) { + throw std::runtime_error("'" + plugin_path.string() + + "' is not a valid .dll file"); + } + + // These constants are specified in + // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types + switch (machine_type) { + case 0x014c: // IMAGE_FILE_MACHINE_I386 + return PluginArchitecture::vst_32; + break; + case 0x8664: // IMAGE_FILE_MACHINE_AMD64 + case 0x0000: // IMAGE_FILE_MACHINE_UNKNOWN + return PluginArchitecture::vst_64; + break; + default: + throw std::runtime_error( + "'" + plugin_path.string() + + "' does not have a supported architecture: " + + std::to_string(machine_type)); + break; + } +} + /** * Generate a unique name for the Unix domain socket endpoint based on the VST * plugin's name. This will also generate the parent directory if it does not diff --git a/src/plugin/host-bridge.h b/src/plugin/host-bridge.h index 26c65fa3..4b4ed5e3 100644 --- a/src/plugin/host-bridge.h +++ b/src/plugin/host-bridge.h @@ -45,6 +45,12 @@ class patched_async_pipe : public boost::process::async_pipe { typedef typename handle_type::executor_type executor_type; }; +/** + * A tag to differentiate between 32 and 64-bit plugins, used to determine which + * host application to use. + */ +enum class PluginArchitecture { vst_32, vst_64 }; + /** * This handles the communication between the Linux native VST plugin and the * Wine VST host. The functions below should be used as callback functions in an @@ -90,14 +96,20 @@ class HostBridge { float get_parameter(AEffect* plugin, int index); void set_parameter(AEffect* plugin, int index, float value); - /** - * The path to `yabridge-host.exe`. - */ - const boost::filesystem::path vst_host_path; /** * The path to the .dll being loaded in the Wine VST host. */ const boost::filesystem::path vst_plugin_path; + /** + * Whether the plugin is 64-bit or 32-bit. + */ + const PluginArchitecture vst_plugin_arch; + /** + * The path to the host application (i.e. a path to either + * `yabridge-host.exe` or `yabridge-host-32.exe`). The host application will + * be chosen depending on the architecture of the VST plugin .dll file. + */ + const boost::filesystem::path vst_host_path; /** * This AEffect struct will be populated using the data passed by the Wine diff --git a/src/wine-host/vst-host.cpp b/src/wine-host/vst-host.cpp index ccb18b21..274ac272 100644 --- a/src/wine-host/vst-host.cpp +++ b/src/wine-host/vst-host.cpp @@ -17,6 +17,7 @@ #include // Generated inside of build directory +#include #include #include "plugin-bridge.h" @@ -26,9 +27,14 @@ int main(int argc, char* argv[]) { // socket to connect to in plugin/bridge.cpp as the first two arguments of // this process. if (argc < 3) { - std::cerr - << "Usage: yabridge-host.exe " - << std::endl; + std::cerr << "Usage: " +#ifdef __i386__ + << yabridge_wine_host_name_32bit +#else + << yabridge_wine_host_name +#endif + << " " << std::endl; + return 1; } @@ -36,6 +42,9 @@ int main(int argc, char* argv[]) { const std::string socket_endpoint_path(argv[2]); std::cerr << "Initializing yabridge host version " << yabridge_git_version +#ifdef __i386__ + << " (32-bit compatibility mode)" +#endif << std::endl; try { PluginBridge bridge(plugin_dll_path, socket_endpoint_path);