Files
feedBack-desktop/src/audio/addon/PluginModulePin.h
T
OmikronApexandClaude Fable 5 4ac1b60766 fix(audio): header-only module pin; exception-safe ops; CI link fix
CI: slopsmith-vst-host (sandbox-e2e's own CMake project) and macOS's
slopsmith-vst-scan compile VSTHost.cpp without LifecycleExecutor.cpp and
failed to link pinPluginModuleForever. Move the pin to a header-only
PluginModulePin.h so every target that compiles VSTHost.cpp gets it with
no extra source-list entry; drop the now-unneeded LifecycleExecutor.cpp
from VST_HOST_SOURCES.

CodeRabbit review:
- Pin by full module path, not base name: enumerate loaded modules
  (PSAPI_VERSION=2 K32 exports, no psapi.lib) and pin every one whose
  path starts with the plugin identifier — two plugins sharing a DLL base
  name can no longer pin the wrong module, and the bundle-dir identifier
  now matches the inner Contents/<arch>/ file it actually loaded.
- Ops are exception-safe: a throwing op logs and still signals its
  waiter instead of leaving an unbounded caller blocked forever.
- Documented why the message-thread inline path is intentional (nested
  submissions are part of the outer op; dispatch-and-wait on the message
  thread would self-deadlock; FIFO is the contract between off-thread
  submitters).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 00:01:52 +02:00

100 lines
3.5 KiB
C++

#pragma once
// Plugin-module pinning — P0 item 4 of the audio architecture guide (§12).
//
// JUCE refcounts VST3 modules and unloads them when the last instance dies;
// a window/timer message already queued to that module's code then fires
// into unmapped or reused pages — the CFG fail-fast
// (0xc0000409 / FAST_FAIL_GUARD_ICALL_CHECK_FAILURE) in the 2026-07-17
// field dumps. Pinning tells the loader to never unload the module for the
// life of the process.
//
// Header-only ON PURPOSE: VSTHost.cpp is compiled into four different
// targets across three CMake projects (the addon, slopsmith-vst-host in
// two projects, slopsmith-vst-scan on macOS); an out-of-line definition
// broke every link that didn't add the extra .cpp (PR #120 CI).
//
// The pin matches loaded modules by PATH, not base name: the identifier we
// get is usually the bundle directory (…\Foo.vst3\), while the loader knows
// the inner …\Contents\x86_64-win\Foo.vst3 file — and two plugins may share
// a base name. Enumerate loaded modules and pin every one whose full path
// starts with the normalized identifier (K32* exports so no psapi.lib).
// No-op off Windows and for modules that are not currently loaded (e.g.
// sandboxed plugins — the child process owns those).
#include <cstdio>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
// PSAPI_VERSION 2 maps EnumProcessModules/GetModuleFileNameExW onto the
// kernel32-exported K32* variants — no psapi.lib link needed (matters for
// the extra targets this header serves; see the header-only note above).
#ifndef PSAPI_VERSION
#define PSAPI_VERSION 2
#endif
#include <psapi.h>
#include <vector>
#include <cwctype>
#endif
#include <juce_core/juce_core.h>
namespace slopsmith::addon {
inline void pinPluginModuleForever(const char* fileOrIdentifierUtf8)
{
#if defined(_WIN32)
const juce::String wanted =
juce::String::fromUTF8(fileOrIdentifierUtf8).replaceCharacter('/', '\\');
if (wanted.isEmpty())
return;
auto startsWithIgnoreCase = [](const wchar_t* full, const wchar_t* prefix) {
while (*prefix != 0)
{
if (*full == 0
|| std::towlower(static_cast<wint_t>(*full))
!= std::towlower(static_cast<wint_t>(*prefix)))
return false;
++full;
++prefix;
}
return true;
};
std::vector<HMODULE> modules(1024);
DWORD needed = 0;
if (!EnumProcessModules(GetCurrentProcess(), modules.data(),
static_cast<DWORD>(modules.size() * sizeof(HMODULE)),
&needed))
return;
modules.resize(needed / sizeof(HMODULE));
for (HMODULE mod : modules)
{
wchar_t modPath[MAX_PATH * 2] = {};
if (GetModuleFileNameExW(GetCurrentProcess(), mod, modPath,
static_cast<DWORD>(sizeof(modPath) / sizeof(modPath[0]))) == 0)
continue;
if (!startsWithIgnoreCase(modPath, wanted.toWideCharPointer()))
continue;
HMODULE pinned = nullptr;
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN, modPath, &pinned))
fprintf(stderr, "[lifecycle] pinned plugin module '%s' for process "
"lifetime\n",
juce::String(modPath).toRawUTF8());
}
#else
(void) fileOrIdentifierUtf8;
#endif
}
} // namespace slopsmith::addon