99 lines
5.1 KiB
C++
99 lines
5.1 KiB
C++
// 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 <optional>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
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<std::string> 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 <class ReadFn>
|
||
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<char> buf(static_cast<std::size_t>(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<std::size_t>(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<int>(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
|