Files
reasampler/src/core/instrument/map/bridge_marshal.h
T

99 lines
5.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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