refactor(audio): extract AddonContext + NapiHelpers, guard raw N-API args (phase 6)

AddonContext (src/audio/addon/): engine/vstHost lifetime + snapshot rule,
the JUCE message thread with the macOS no-pump fork quarantined into ONE
file, the shutdown latch (exposed as isShuttingDown), doShutdown with a UI
teardown hook (NodeAddon points it at the editor-window nuke, #56), and the
pending-async-load registry. NodeAddon keeps using-declarations so the
binding bodies are unchanged. Also fixes SetBackingSpeed's bare `engine`
dereference — the one binding that dodged the file's own snapshot rule.

NapiHelpers (typed extractors argInt/argSlotId/argFiniteFloat/argBool/
argMidiChannel/argMidiByte) + rewrites of the unguarded bindings — the
deep-read §2 fix, done once: SetParameter/SetBypass/RemoveProcessor/
MoveProcessor/SetMultiBypass/SendMidiToSlot/SetGain plus the St-1 routing
quartet (SetPan/SetPostGain/SetBranch/SetBranchSrc). NaN slot ids no longer
coerce to slot 0; MIDI channel/program are range-checked before JUCE.

New gate: tests/napi-arg-fuzz.test.js — table-driven garbage (NaN/Inf/
negative/string/missing/object) against the real addon; chain state must be
byte-identical after the storm and a valid call must still apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-14 01:41:07 +02:00
co-authored by Claude Fable 5
parent 6b0bfb7be3
commit d4e0bfc272
6 changed files with 527 additions and 295 deletions
+66
View File
@@ -0,0 +1,66 @@
#pragma once
// AddonContext — engine/vstHost lifetime, the JUCE message thread, the
// shutdown latch, and the pending-async-load registry (TLC plan phase 6 /
// §3.1). Moved verbatim from NodeAddon.cpp; this quarantines the JUCE_MAC
// platform fork (no dispatch loop — see startJuceMessageThread) into ONE
// file instead of a branch inside every load path.
//
// engine / vstHost — shared_ptr (not unique_ptr) so worker threads can take
// a stable snapshot that keeps the object alive for the duration of their
// work, even if the message thread reassigns the global mid-operation. This
// matters most for the async VST load: createPluginInstanceAsync's JUCE
// continuation must not have VSTHost / its formatManager torn out from
// under it mid-load.
//
// Enforced rule: every *dereference* of engine / vstHost goes through a
// local snapshot (snapshotEngine / snapshotVstHost). The only code touching
// the bare globals is the snapshot helpers and the mutex-guarded writes in
// initialize / doShutdown.
#include "../AudioEngine.h"
#include "../VSTHost.h"
#include <juce_events/juce_events.h>
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
namespace slopsmith::addon {
std::shared_ptr<AudioEngine> snapshotEngine();
std::shared_ptr<VSTHost> snapshotVstHost();
// Start the pump + create engine/vstHost on the message thread (inline on
// macOS). `uiTeardownHook` runs on the message thread at the START of
// shutdown, BEFORE engine.reset() frees the processors — NodeAddon points it
// at destroyAllPluginEditorWindowsOnMessageThread (use-after-free; #56).
void initialize(std::function<void()> uiTeardownHook);
void doShutdown();
// Dispatch `func` on the JUCE message thread and wait (bounded 15 s).
// macOS: executes inline on the caller thread — no background pump exists
// (AppKit owns the real main thread; see the fork note in the .cpp).
void dispatchOnMessageThreadImpl(std::function<void()> func);
template <typename Func>
inline void dispatchOnMessageThread(Func&& func)
{
dispatchOnMessageThreadImpl(std::function<void()>(std::forward<Func>(func)));
}
// Pending-async-load registry: LoadVSTWorker / LoadPresetWorker block on a
// WaitableEvent until the message-thread continuation fires; doShutdown
// signals every registered event so no worker waits forever once the pump
// is gone.
// Whether doShutdown has begun (acquire). The load workers gate on this
// after registering their pending event, catching the register-vs-shutdown
// race in both directions.
bool isShuttingDown();
void registerPendingLoad(std::shared_ptr<juce::WaitableEvent> evt);
void unregisterPendingLoad(const std::shared_ptr<juce::WaitableEvent>& evt);
void cancelAllPendingLoads();
} // namespace slopsmith::addon