diff --git a/src/common/mutual-recursion.h b/src/common/mutual-recursion.h
new file mode 100644
index 00000000..be44e8c9
--- /dev/null
+++ b/src/common/mutual-recursion.h
@@ -0,0 +1,191 @@
+// yabridge: a Wine VST bridge
+// Copyright (C) 2020-2021 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
+
+#ifdef __WINE__
+#include "../wine-host/boost-fix.h"
+#endif
+#include
+#include
+
+/**
+ * A helper to allow mutually recursive calling sequences with remote function
+ * calls. Some plugins (and hosts) are very picky about which thread a function
+ * call is coming from. This becomes an issue when the other side calls another
+ * function in response to a function call, and when that other function _has_
+ * to be handled on the same thread that called the first function. An example
+ * of this is a VST3 plugin requesting a resize from the host. In response to
+ * this the host will ask the plugin again for its current size, after which the
+ * host will inform the plugin about its current size, and only then will the
+ * original function call return. The issue here is that all of those function
+ * calls have to be handled from both the plugin's and the host's GUI thread.
+ * This helper lets you perform (potentially) mutually recursive function calls
+ * where we'll spawn a new thread to do the blocking socket operations, and it
+ * also lets you handle (potentially) mutually recursive function calls by
+ * executing those on the original calling thread that initiated the mutually
+ * recursive call sequence. For illustration, this looks like this:
+ *
+ * ```
+ * thread 1: fork(fn)-\------------------/--foo()--\-----------/-
+ * thread ?: \ handle(foo)--/ \--... /
+ * thread 2: \-----waiting for fn() to return-----/
+ * ```
+ *
+ * Here `fork(fn)` will call the function `fn` on a new thread (which presumably
+ * does some blocking socket operations), and `handle(foo)` will call `foo()` on
+ * the thread that originally called `fork(fn)`. If the function passed to
+ * `handle()` also calls `fork()` (or more likely, the function pass to
+ * `handle()` calls an unmanaged plugin/host function that ends up performing a
+ * mutually recursive callback), then this sequence allows for arbitrarily
+ * nested mutual recursion.
+ *
+ * @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 MutualRecursionHelper {
+ public:
+ /**
+ * Run `fn` from a new thread, while handling calls to `handle()` and
+ * `maybe_handle()` on this thread. See the docstring on
+ * `MutualRecursionHelper` for more information on this mechanism.
+ *
+ * @param fn A (blocking) function that should be called on another thread..
+ * This function will normally send a message to the other side using
+ * sockets, and it will then idly wait for a response.
+ *
+ * @return The return value of `fn`.
+ */
+ template F>
+ std::invoke_result_t fork(F fn) {
+ using Result = std::invoke_result_t;
+
+ // This IO context will accept incoming calls from `handle()` and
+ // `maybe_handle()` until the function returns. We keep these on a stack
+ // as we need to support multiple levels of mutual recursion. This can
+ // for instance happen during `IPlugView::attached() ->
+ // IPlugFrame::resizeView() -> IPlugView::onSize()`.
+ std::shared_ptr current_io_context =
+ std::make_shared();
+ {
+ std::unique_lock lock(mutual_recursion_contexts_mutex);
+ mutual_recursion_contexts.push_back(current_io_context);
+ }
+
+ // Instead of directly stopping the IO context, we'll reset this work
+ // guard instead. This prevents us from accidentally cancelling any
+ // outstanding tasks.
+ auto work_guard = boost::asio::make_work_guard(*current_io_context);
+
+ // We will call the function from another thread so we can handle calls
+ // to `handle()`/`maybe_handle()` from this thread
+ std::promise response_promise{};
+ Thread sending_thread([&]() {
+ const Result response = fn();
+
+ // Stop accepting additional work to be run from the calling thread
+ // once `fn` returns (and we'll likely have gotten a response from
+ // the other side). By resetting the work guard we do not cancel any
+ // pending tasks, but `current_io_context->run()` will stop blocking
+ // eventually.
+ std::lock_guard lock(mutual_recursion_contexts_mutex);
+ work_guard.reset();
+ mutual_recursion_contexts.erase(
+ std::find(mutual_recursion_contexts.begin(),
+ mutual_recursion_contexts.end(), current_io_context));
+
+ response_promise.set_value(response);
+ });
+
+ // Accept work from the other thread until we receive a response, at
+ // which point the context will be stopped
+ current_io_context->run();
+
+ return response_promise.get_future().get();
+ }
+
+ /**
+ * If another thread is currently calling `fork()`, then `fn` will be called
+ * from that same thread. Otherwise `fn` will be called directly. See the
+ * docstring on `MutualRecursionHelper`.
+ *
+ * @param fn The function to call on the mutual recursion thread, if that
+ * exists. This function may (indirectly) call `fork()` again to do nested
+ * mutual recursion.
+ *
+ * @return The result of `fn`, if it returns anything.
+ *
+ * @tparam F Some callable function that doesn't take any parameters.
+ */
+ template F>
+ std::invoke_result_t handle(F&& fn) {
+ // If we're not currently engaged in some mutually recursive calling
+ // sequence, then we'll execute the function on this thread
+ if (const auto result = maybe_handle(std::forward(fn))) {
+ return *result;
+ } else {
+ return fn();
+ }
+ }
+
+ /**
+ * The same as `handle()`, but `fn` will only executed if we're currently
+ * doing a mutually recursive function call through `fork()`. If no thread
+ * is currently calling `fork()`, then this will return an `std::nullopt`
+ * and `fn` won't be called and the caller must call `fn` itself.
+ *
+ * @see handle
+ */
+ template F>
+ std::optional> maybe_handle(F&& fn) {
+ using Result = std::invoke_result_t;
+
+ std::unique_lock mutual_recursion_lock(mutual_recursion_contexts_mutex);
+ if (mutual_recursion_contexts.empty()) {
+ return std::nullopt;
+ }
+
+ // This function is only used in synchronous contexts, so we'll just
+ // pretend that we're not doing any async things here
+ std::packaged_task do_call(std::forward(fn));
+ std::future do_call_response = do_call.get_future();
+ boost::asio::dispatch(*mutual_recursion_contexts.back(),
+ std::move(do_call));
+ mutual_recursion_lock.unlock();
+
+ return do_call_response.get();
+ }
+
+ private:
+ /**
+ * These IO contexts will let us call functions from the thread that's
+ * currently calling `fork()` while we're waiting for the passed function to
+ * return. We need an entire stack of these to be able to support deeply
+ * nested mutual recursion, how fun! If `fork()` is being called multiple
+ * times from the same thread (in a mutual recursion sequence), this stack
+ * will contain multiple IO contexts. In that case the last context is the
+ * active one. If the stack is empty, then there's currently no mutual
+ * recursion going on.
+ */
+ std::vector>
+ mutual_recursion_contexts;
+ std::mutex mutual_recursion_contexts_mutex;
+};