// bridge_marshal.h — PURE marshalling helper for the REAPER VST-host bridge read. // NO VST3, NO REAPER types at the boundary. // // The bridge shell (reaper_bridge.cpp) resolves REAPER API functions by name over the // host callback and invokes them; the one fiddly-and-easy-to-get-wrong part around // GetProjExtState — interpreting its int return against the buffer it filled — is pure // and unit-tested here. Mirror of capture_paths / wav_trim splitting the arithmetic out // of a REAPER-facing shell. // // The S1 spike ALSO carried a string-scan JSON reader (extractJsonStringField) as a // stand-in until the instrument could parse the bank properly. S4 retired it: the // instrument now parses the "reasampler" bank blob through the SHARED bank_book / // bank_model JSON path (sample_map.cpp), so there is no second JSON parser. This module // is back to its one honest job — the API-return decode. // // Verified against vendor/reaper-sdk/sdk/reaper_plugin_functions.h: // int GetProjExtState (ReaProject*, extname, key, valOutNeedBig, valOutNeedBig_sz); // -- returns the length written (0 when the key is absent). #pragma once #include #include #include #include namespace reasampler::instrument::map { // Interpret a GetProjExtState result: the int return value (bytes the API reports for // the key) and the buffer it filled. Returns the value only when the API reported a // non-empty result AND the buffer is non-empty — REAPER writes 0 and leaves the buffer // untouched for an absent key, and we must not treat stale buffer contents as a hit. // // `apiReturn` is GetProjExtState's return; `buffer` is the NUL-terminated string it // wrote (already truncated to the C string by the caller). std::optional decodeGetProjExtState(int apiReturn, const std::string& buffer); // --------------------------------------------------------------------------- // The GetProjExtState GROW-LOOP retry policy (Q-W5 rider, T2-04). // --------------------------------------------------------------------------- // GetProjExtState writes into a caller-supplied buffer with no documented // query-the-size call, so a large value (bank blob, usage record) must be read by // growing a buffer until the value fits strictly inside it. Three shells carried // hand-rolled copies of that loop (persist's ext-state reads, usage_scan's // prune-safety-adjacent record read, reaper_bridge's VST-side bank read); the ONE // policy now lives here so the retry/termination rules cannot drift. The fiddly // part is the termination taxonomy, which each caller folds differently: // // * Absent — the API returned <= 0 on some attempt: the key holds no value. // (persist -> "" empty bank; usage_scan / bridge -> nullopt) // * Complete — the written C string fits STRICTLY inside the buffer (size+1 < // cap), so it cannot have been clipped: `value` is the whole value. // * Overflow — the value never fit under the 16 MB ceiling: it is unreadable // WHOLE, which is NOT the same as absent. (persist warns on the // console; usage_scan folds it to the prune fail-safe abort) // // `read` is one GetProjExtState-shaped attempt: int read(char* buf, int cap), // returning the API's int. A template, statically dispatched per call site — no // virtual calls, no std::function (the §3 performance guardrail); the caller binds // the project/namespace/key (or a resolved function pointer, VST side) in a lambda. struct GrowingExtStateRead { enum class Status { Absent, Complete, Overflow }; Status status = Status::Absent; int apiReturn = 0; // the FINAL attempt's return (<= 0 iff Absent); feeds // decodeGetProjExtState on the bridge path unchanged std::string value; // the whole value; meaningful only when Complete }; template GrowingExtStateRead readProjExtStateGrowing(ReadFn&& read) { // Start generous; grow ×4 if REAPER reports the value may have been clipped // (the return is the value length; equal-to-capacity-minus-NUL is ambiguous, // so only a strict fit terminates). Ceiling 16 MB — give up rather than loop // forever on a pathological value. GrowingExtStateRead result; for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) { std::vector buf(static_cast(cap), '\0'); const int rv = read(buf.data(), cap); result.apiReturn = rv; if (rv <= 0) { result.status = GrowingExtStateRead::Status::Absent; return result; } buf[static_cast(cap) - 1] = '\0'; // defensive: guard against a read() that fills the buffer without honoring NUL-termination within cap std::string s(buf.data()); if (static_cast(s.size()) + 1 < cap) { result.status = GrowingExtStateRead::Status::Complete; result.value = std::move(s); return result; } // else: possibly truncated -> grow and retry. } result.status = GrowingExtStateRead::Status::Overflow; return result; } } // namespace reasampler::instrument::map