Files
yabridge/src/plugin/host-bridge.cpp
T
Robbert van der Helm 5f584323c2 Monkey patch async pipes foor Boost 1.72
This is an simple workaround and it's much more practical than having to
downgrade Boost since that breaks any application that links to it.
2020-03-14 16:49:38 +01:00

529 lines
20 KiB
C++

// 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/>.
#include "host-bridge.h"
#include <boost/asio/read_until.hpp>
#include <boost/dll/runtime_symbol_info.hpp>
#include <boost/filesystem.hpp>
#include <boost/process/env.hpp>
#include <boost/process/io.hpp>
#include <boost/process/search_path.hpp>
#include <iostream>
#include <random>
#include "../common/communication.h"
#include "../common/events.h"
namespace bp = boost::process;
// I'd rather use std::filesystem instead, but Boost.Process depends on
// 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.
*/
constexpr char alphanumeric_characters[] =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
std::string create_logger_prefix(const fs::path& socket_path);
fs::path find_vst_plugin();
fs::path find_wine_vst_host();
std::optional<fs::path> find_wineprefix();
fs::path generate_endpoint_name();
bp::environment set_wineprefix();
intptr_t dispatch_proxy(AEffect*, int, int, intptr_t, void*, float);
void process_proxy(AEffect*, float**, float**, int);
void process_replacing_proxy(AEffect*, float**, float**, int);
void setParameter_proxy(AEffect*, int, float);
float getParameter_proxy(AEffect*, int);
/**
* Fetch the bridge instance stored in an unused pointer from a VST plugin. This
* is sadly needed as a workaround to avoid using globals since we need free
* function pointers to interface with the VST C API.
*/
HostBridge& get_bridge_instance(const AEffect& plugin) {
return *static_cast<HostBridge*>(plugin.ptr3);
}
HostBridge::HostBridge(audioMasterCallback host_callback)
: vst_host_path(find_wine_vst_host()),
vst_plugin_path(find_vst_plugin()),
// All the fields should be zero initialized because
// `Vst2PluginInstance::vstAudioMasterCallback` from Bitwig's plugin
// bridge will crash otherwise
plugin(),
io_context(),
socket_endpoint(generate_endpoint_name().string()),
socket_acceptor(io_context, socket_endpoint),
host_vst_dispatch(io_context),
vst_host_callback(io_context),
host_vst_parameters(io_context),
host_vst_process_replacing(io_context),
vst_host_aeffect(io_context),
host_callback_function(host_callback),
logger(Logger::create_from_environment(
create_logger_prefix(socket_endpoint.path()))),
wine_stdout(io_context),
wine_stderr(io_context),
vst_host(vst_host_path,
// The Wine VST host needs to know which plugin to load
// and which Unix domain socket to connect to
vst_plugin_path,
socket_endpoint.path(),
bp::env = set_wineprefix(),
bp::std_out = wine_stdout,
bp::std_err = wine_stderr) {
logger.log("Initializing yabridge using '" + vst_host_path.string() + "'");
logger.log("plugin: '" + vst_plugin_path.string() + "'");
logger.log("socket: '" + socket_endpoint.path() + "'");
logger.log("wineprefix: '" +
find_wineprefix().value_or("<default>").string() + "'");
// 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);
socket_acceptor.accept(vst_host_callback);
socket_acceptor.accept(host_vst_parameters);
socket_acceptor.accept(host_vst_process_replacing);
socket_acceptor.accept(vst_host_aeffect);
// Set up all pointers for our `AEffect` struct. We will fill this with data
// from the VST plugin loaded in Wine at the end of this constructor.
plugin.ptr3 = this;
plugin.dispatcher = dispatch_proxy;
plugin.process = process_proxy;
plugin.setParameter = setParameter_proxy;
plugin.getParameter = getParameter_proxy;
plugin.processReplacing = process_replacing_proxy;
// For our communication we use simple threads and blocking operations
// instead of asynchronous IO since communication has to be handled in
// lockstep anyway
host_callback_handler = std::thread([&]() {
try {
while (true) {
passthrough_event(vst_host_callback, &plugin,
host_callback_function,
std::pair<Logger&, bool>(logger, false));
}
} catch (const boost::system::system_error&) {
// This happens when the sockets got closed because the plugin
// is being shut down
}
});
// Print the Wine host's STDOUT and STDERR streams to the log file
async_log_pipe_lines(wine_stdout, wine_stdout_buffer, "[Wine STDOUT] ");
async_log_pipe_lines(wine_stderr, wine_stderr_buffer, "[Wine STDERR] ");
wine_io_handler = std::thread([&]() { io_context.run(); });
// Read the plugin's information from the Wine process. This can only be
// done after we started accepting host callbacks as the plugin might do
// this during initialization.
// XXX: If the plugin has crashed then this read should fail instead of
// blocking indefinitely, check if this is the case
plugin = read_object(vst_host_aeffect, plugin);
}
class DispatchDataConverter : DefaultDataConverter {
public:
DispatchDataConverter(std::vector<uint8_t>& chunk_data)
: chunk(chunk_data) {}
std::optional<EventPayload> read(const int opcode,
const intptr_t value,
const void* data) {
// There are some events that need specific structs that we can't simply
// serialize as a string because they might contain null bytes
switch (opcode) {
// TODO: Add GUI support. These events are just disabled for now to
// ensure everything else works first.
case effEditOpen:
case effEditTop:
case effEditIdle:
case effEditClose:
case effEditGetRect:
std::cerr << "Got opcode "
<< opcode_to_string(true, opcode)
.value_or(std::to_string(opcode))
<< "), ignoring..." << std::endl;
return std::nullopt;
break;
case effGetChunk:
return WantsChunkBuffer();
break;
case effSetChunk:
// When the host passes a chunk it will use the value parameter
// to tell us its length
return std::string(static_cast<const char*>(data), value);
break;
case effProcessEvents:
return DynamicVstEvents(*static_cast<const VstEvents*>(data));
break;
default:
return DefaultDataConverter::read(opcode, value, data);
break;
}
}
void write(const int opcode, void* data, const EventResult& response) {
switch (opcode) {
case effGetChunk:
// Write the chunk data to some publically accessible place in
// `HostBridge` and write a pointer to that struct to the data
// pointer
{
std::string buffer =
std::get<std::string>(response.payload);
chunk.assign(buffer.begin(), buffer.end());
*static_cast<void**>(data) = chunk.data();
}
break;
default:
DefaultDataConverter::write(opcode, data, response);
break;
}
}
intptr_t return_value(const int opcode, const intptr_t original) {
return DefaultDataConverter::return_value(opcode, original);
}
private:
std::vector<uint8_t>& chunk;
};
/**
* Handle an event sent by the VST host. Most of these opcodes will be passed
* through to the winelib VST host.
*/
intptr_t HostBridge::dispatch(AEffect* /*plugin*/,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
DispatchDataConverter converter(chunk_data);
// Some events need some extra handling
// TODO: Handle other things such as GUI itneraction
switch (opcode) {
case effClose:
// TODO: Gracefully close the editor?
// TODO: Check whether the sockets and the endpoint are closed
// correctly
// Allow the plugin to handle its own shutdown
const auto return_value = send_event(
host_vst_dispatch, converter, opcode, index, value, data,
option, std::pair<Logger&, bool>(logger, true));
// Boost.Process will send SIGKILL to the Wien host for us when this
// class gets destroyed. Because the process is running a few
// threads Wine will say something about a segfault (probably
// related to `std::terminate`), but this doesn't seem to have any
// negative impact
// The `stop()` method will cause the IO context to just drop all of
// its work and immediately and not throw any exceptions that would
// have been caused by pipes and sockets being closed
io_context.stop();
// `std::thread`s are not interruptable, and since we're doing
// blocking synchronous reads there's no way to interrupt them. If
// we don't detach them then the runtime will call `std::terminate`
// for us. The workaround here is to simply detach the threads and
// then close all sockets. This will cause them to throw exceptions
// which we then catch and ignore. Please let me know if there's a
// better way to handle this.q
host_callback_handler.detach();
wine_io_handler.detach();
delete this;
return return_value;
break;
}
// TODO: Maybe reuse buffers here when dealing with chunk data
return send_event(host_vst_dispatch, converter, opcode, index, value, data,
option, std::pair<Logger&, bool>(logger, true));
}
void HostBridge::process_replacing(AEffect* /*plugin*/,
float** inputs,
float** outputs,
int sample_frames) {
// The inputs and outputs arrays should be `[num_inputs][sample_frames]` and
// `[num_outputs][sample_frames]` floats large respectfully.
std::vector<std::vector<float>> input_buffers(
plugin.numInputs, std::vector<float>(sample_frames));
for (int channel = 0; channel < plugin.numInputs; channel++) {
std::copy(inputs[channel], inputs[channel] + sample_frames + 1,
input_buffers[channel].begin());
}
const AudioBuffers request{input_buffers, sample_frames};
write_object(host_vst_process_replacing, request, process_buffer);
// /Write the results back to the `outputs` arrays
AudioBuffers response;
response =
read_object(host_vst_process_replacing, response, process_buffer);
assert(response.buffers.size() == static_cast<size_t>(plugin.numOutputs));
for (int channel = 0; channel < plugin.numOutputs; channel++) {
std::copy(response.buffers[channel].begin(),
response.buffers[channel].end(), outputs[channel]);
}
}
float HostBridge::get_parameter(AEffect* /*plugin*/, int index) {
logger.log_get_parameter(index);
const Parameter request{index, std::nullopt};
write_object(host_vst_parameters, request);
const auto response = read_object<ParameterResult>(host_vst_parameters);
logger.log_get_parameter_response(response.value.value());
return response.value.value();
}
void HostBridge::set_parameter(AEffect* /*plugin*/, int index, float value) {
logger.log_set_parameter(index, value);
const Parameter request{index, value};
write_object(host_vst_parameters, request);
// This should not contain any values and just serve as an acknowledgement
const auto response = read_object<ParameterResult>(host_vst_parameters);
logger.log_set_parameter_response();
assert(!response.value.has_value());
}
void HostBridge::async_log_pipe_lines(patched_async_pipe& pipe,
boost::asio::streambuf& buffer,
std::string prefix) {
boost::asio::async_read_until(
pipe, buffer, '\n', [&, prefix](const auto&, size_t) {
std::string line;
std::getline(std::istream(&buffer), line);
logger.log(prefix + line);
async_log_pipe_lines(pipe, buffer, prefix);
});
}
/**
* Create a logger prefix based on the unique socket path for easy
* identification. The socket path contains both the plugin's name and a unique
* identifier.
*
* @param socket_path The path to the socket endpoint in use.
*
* @return A prefix string for log messages.
*/
std::string create_logger_prefix(const fs::path& socket_path) {
std::ostringstream prefix;
prefix << "[" << socket_path.filename().replace_extension().string()
<< "] ";
return prefix.str();
}
/**
* Finds the Wine VST hsot (named `yabridge-host.exe`). 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
* file in the build directory without having to install anything to
* /usr.
* 2. In the regular search path.
*
* @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 host_path =
fs::canonical(boost::dll::this_line_location()).remove_filename() /
yabridge_wine_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);
if (vst_host_path == "") {
throw std::runtime_error("Could not locate '" +
std::string(yabridge_wine_host_name) + "'");
}
return vst_host_path;
}
/**
* Locate the wineprefix this file is located in, if it is inside of a wine
* prefix.
*
* @return Either the path to the wineprefix (containing the `drive_c?`
* directory), or `std::nullopt` if it is not inside of a wine prefix.
*/
std::optional<fs::path> find_wineprefix() {
// Try to locate the wineprefix this .so file is located in by finding the
// first parent directory that contains a directory named `dosdevices`
fs::path wineprefix_path =
boost::dll::this_line_location().remove_filename();
while (wineprefix_path != "") {
if (fs::is_directory(wineprefix_path / "dosdevices")) {
return wineprefix_path;
}
wineprefix_path = wineprefix_path.parent_path();
}
return std::nullopt;
}
/**
* Find the VST plugin .dll file that corresponds to this copy of
* `libyabridge.so`. This should be the same as the name of this file but with a
* `.dll` file extension instead of `.so`.
*
* @return The a path to the accompanying VST plugin .dll file.
* @throw std::runtime_error If no matching .dll file could be found.
*/
fs::path find_vst_plugin() {
fs::path plugin_path = boost::dll::this_line_location();
plugin_path.replace_extension(".dll");
// This function is used in the constructor's initializer list so we have to
// throw when the path could not be found
if (!fs::exists(plugin_path)) {
throw std::runtime_error(
"'" + plugin_path.string() +
"' does not exist, make sure to rename 'libyabridge.so' to match a "
"VST plugin .dll file.");
}
// Also resolve symlinks here, mostly for development purposes
return fs::canonical(plugin_path);
}
/**
* 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
* yet exist since we're using this in the constructor's initializer list.
*
* @return A path to a not yet existing Unix domain socket endpoint.
* @throw std::runtime_error If no matching .dll file could be found.
*/
fs::path generate_endpoint_name() {
const auto plugin_name =
find_vst_plugin().filename().replace_extension("").string();
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);
candidate_endpoint = fs::temp_directory_path() / "yabridge" /
(plugin_name + "-" + random_id + ".sock");
} while (fs::exists(candidate_endpoint));
// Ensure that the parent directory exists so the socket endpoint can be
// created there
fs::create_directories(candidate_endpoint.parent_path());
// TODO: Should probably try creating the endpoint right here and catch any
// exceptions since this could technically result in a race condition
// when two instances of yabridge decide to use the same endpoint name
// at the same time
return candidate_endpoint;
}
/**
* Locate the wineprefix and set the `WINEPREFIX` environment variable if found.
* This way it's also possible to run .dll files outside of a wineprefix using
* the user's default prefix.
*/
bp::environment set_wineprefix() {
auto env(boost::this_process::environment());
const auto wineprefix_path = find_wineprefix();
if (wineprefix_path.has_value()) {
env["WINEPREFIX"] = wineprefix_path->string();
}
return env;
}
// The below functions are proxy functions for the methods defined in
// `Bridge.cpp`
intptr_t dispatch_proxy(AEffect* plugin,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
return get_bridge_instance(*plugin).dispatch(plugin, opcode, index, value,
data, option);
}
void process_proxy(AEffect* plugin,
float** inputs,
float** outputs,
int sample_frames) {
return get_bridge_instance(*plugin).process_replacing(
plugin, inputs, outputs, sample_frames);
}
void process_replacing_proxy(AEffect* plugin,
float** inputs,
float** outputs,
int sample_frames) {
return get_bridge_instance(*plugin).process_replacing(
plugin, inputs, outputs, sample_frames);
}
void setParameter_proxy(AEffect* plugin, int index, float value) {
return get_bridge_instance(*plugin).set_parameter(plugin, index, value);
}
float getParameter_proxy(AEffect* plugin, int index) {
return get_bridge_instance(*plugin).get_parameter(plugin, index);
}