Merge branch 'feature/bitbridge'

This commit is contained in:
Robbert van der Helm
2020-04-30 18:47:29 +02:00
14 changed files with 328 additions and 83 deletions
+30 -8
View File
@@ -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
+19
View File
@@ -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
-17
View File
@@ -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'
+65 -21
View File
@@ -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
+9 -1
View File
@@ -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.'
)
+10 -3
View File
@@ -33,7 +33,8 @@ template <typename B>
using InputAdapter = bitsery::InputBufferAdapter<B>;
/**
* 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_t, 1>{size}));
boost::asio::buffer(std::array<uint64_t, 1>{size}));
const size_t bytes_written =
boost::asio::write(socket, boost::asio::buffer(buffer, size));
assert(bytes_written == size);
@@ -79,7 +85,8 @@ template <typename T, typename Socket>
inline T& read_object(Socket& socket,
T& object,
std::vector<uint8_t> buffer = std::vector<uint8_t>(64)) {
std::array<size_t, 1> message_length;
// See the note above on the use of `uint64_t` instead of `size_t`
std::array<uint64_t, 1> message_length;
boost::asio::read(socket, boost::asio::buffer(message_length));
// Make sure the buffer is large enough
+30
View File
@@ -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 <https://www.gnu.org/licenses/>.
#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@";
+13
View File
@@ -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(
+5 -2
View File
@@ -209,13 +209,16 @@ void passthrough_event(boost::asio::local::stream_protocol::socket& socket,
[&](const std::vector<uint8_t>& buffer) -> void* {
return const_cast<uint8_t*>(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<void*>(window_handle);
// Make sure to convert the window ID first to `size_t` in
// case this is the 32-bit host.
return reinterpret_cast<void*>(
static_cast<size_t>(window_handle));
},
[&](const AEffect&) -> void* { return nullptr; },
[&](DynamicVstEvents& events) -> void* {
+3 -1
View File
@@ -176,7 +176,9 @@ void Logger::log_event(bool is_dispatch,
[&](const std::vector<uint8_t>& buffer) {
message << "<" << buffer.size() << " byte chunk>";
},
[&](const intptr_t&) { message << "<nullptr>"; },
[&](const native_size_t& window_id) {
message << "<window " << window_id << ">";
},
[&](const AEffect&) { message << "<nullptr>"; },
[&](const DynamicVstEvents& events) {
message << "<" << events.events.size() << " midi_events>";
+23 -7
View File
@@ -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<size_t, uint64_t>);
static_assert(std::is_same_v<intptr_t, int64_t>);
#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 <class... Ts>
@@ -248,7 +265,7 @@ struct WantsString {};
using EventPayload = std::variant<std::nullptr_t,
std::string,
std::vector<uint8_t>,
size_t,
native_size_t,
AEffect,
DynamicVstEvents,
WantsChunkBuffer,
@@ -270,7 +287,9 @@ void serialize(S& s, EventPayload& payload) {
[](S& s, std::vector<uint8_t>& 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
+93 -16
View File
@@ -30,6 +30,7 @@
#endif
// Generated inside of build directory
#include <src/common/config/config.h>
#include <src/common/config/version.h>
#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<fs::path> 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("<default>").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(" <none>");
#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<char*>(&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<char*>(&pe_signature), sizeof(pe_signature));
file.read(reinterpret_cast<char*>(&machine_type), sizeof(machine_type));
constexpr char expected_pe_signature[4] = {'P', 'E', '\0', '\0'};
if (pe_signature !=
*reinterpret_cast<const uint32_t*>(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
+16 -4
View File
@@ -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
+12 -3
View File
@@ -17,6 +17,7 @@
#include <iostream>
// Generated inside of build directory
#include <src/common/config/config.h>
#include <src/common/config/version.h>
#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 <vst_plugin_dll> <unix_domain_socket>"
<< std::endl;
std::cerr << "Usage: "
#ifdef __i386__
<< yabridge_wine_host_name_32bit
#else
<< yabridge_wine_host_name
#endif
<< " <vst_plugin_dll> <unix_domain_socket>" << 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);