Write a custom frame¶
The frames the library ships find their end by a length field, a count, the id or a fixed size. A protocol that ends its messages some other way, a line feed here, needs a frame that scans for it. This is the Cboe TOP feed from the market data example: ASCII lines, one message per line, no length anywhere.
Source: example/markets/cboe.hpp, CMake target markets.
The view¶
A frame view is three steps: read the header, settle the payload's extent, build the payload view. Deriving
from rbe::frames::dsrl::base_frame gives the first and third in their default form through the same
building blocks the library uses (header<H>::make, try_construct_payload), and the derived class writes
the one that differs.
/// One TOP message as it sits on the wire: `message` plus its terminating LF and any LF padding after it.
///
/// TOP is line-oriented, so the end of a message is readable without knowing its type: the first LF
/// after the header. `line_view` is a custom frame that keeps the default header (step 1) and the default
/// payload construction (step 3, an `any` over the line, LF included) and swaps only the extent (step 2)
/// for that scan. Two things follow: `length()` covers the terminator and the padding, so `many<line>`
/// advances line by line, and the extent does not depend on the id, so an unknown `msg_type` is yielded
/// as an `any` over its own line and iteration goes on past it.
class line_view : public rbe::frames::dsrl::base_frame<Header, messages::dsrl_type> {
using base = rbe::frames::dsrl::base_frame<Header, messages::dsrl_type>;
public:
/// The scan for the terminator is not detectable from the shape, and the shape would get it wrong. For
/// this case is necessary to declare the delimiting concept explicitly.
using delimiting_concept = rbe::frames::self_delimited_tag;
static constexpr auto make(buffer_type const buf, std::size_t const scanned_offset = 0) -> std::optional<line_view> {
auto const hdr = header_return_type::make(buf); // step 1
if (not hdr.has_value()) {
return std::nullopt;
}
auto const rest = buf.subspan(hdr->length());
auto const extent = line_extent(rest, scanned_offset); // step 2: the only custom part
if (not extent.has_value()) {
return std::nullopt; // incomplete line: wait for more bytes
}
auto const line = rest.first(*extent);
return rbe::frames::dsrl::try_construct_payload<payload_type>(*hdr, line) // step 3
.transform([&](payload_return_type const p) { return line_view {*hdr, p, hdr->length() + line.size()}; });
}
/// precondition: buf holds a whole line
constexpr explicit line_view(buffer_type const buf) : line_view(*make(buf)) { }
[[nodiscard]] constexpr auto length() const -> size_type { return line_length_; }
private:
/// up to and including the terminating LF, then every LF that follows it; nullopt if the line is not complete yet
static constexpr auto line_extent(buffer_type const rest, std::size_t const scanned_offset = 0)
-> std::optional<size_type> {
auto const safe_offset = std::min(scanned_offset, rest.size());
auto const unsearched = rest.subspan(safe_offset);
auto const lf = std::ranges::find(unsearched, std::byte {'\n'});
if (lf == unsearched.end()) {
return std::nullopt;
}
// Calculate total extent from the beginning of 'rest', not from 'unsearched'
return std::make_optional(std::ranges::distance(rest.begin(), lf) + 1);
}
constexpr line_view(header_return_type const hdr, payload_return_type const p, size_type const n) :
base(hdr, p), line_length_(n) { }
size_type line_length_;
};
What the derived class must provide:
static make(buffer) -> std::optional<view>: the checking constructor, which the library calls to walk a run and to implementrbe::try_deserialize.using delimiting_concept = ...: which delimiting category the frame is in. The library cannot deduce it for an extent it does not compute:self_delimited_tagsays "the frame finds its own end".length(): the frame's extent in bytes, if it can be answered in O(1). It is what lets amanyadvance from one line to the next.
Everything else, header(), payload(), flatten(), the spans and the structured binding, comes from
base_frame.
The vocabulary type¶
The view is what runs; the vocabulary type is what composes. It names the header, the payload, the
category and the view, and that is enough for rbe::frames::many<line> and rbe::deserialize<line> to
work.
/// The vocabulary serder for `line_view`: what makes `rbe::frames::many<line>` compose, exactly as `rbe::frames::blob`
/// or `rbe::frames::frame` do. It keeps the header-plus-payload shape, so the delimiting concepts classify it.
struct line {
using header_type = Header;
using payload_type = messages;
using delimiting_concept = rbe::frames::self_delimited_tag; ///< the terminator scan of line_view, see above
using dsrl_type = line_view;
};
/// A run of TOP lines, LF padding included.
using lines = rbe::frames::many<line>;
Using it¶
/// Cboe TOP: a line-oriented ASCII feed. `line` is a custom frame that delimits by the LF, so `many<line>` walks
/// the stream line by line and an unknown message type is yielded as an unknown id and iteration goes on.
void cboe_top_lines() {
using namespace cboe::top;
auto const stream = wire(
Header {.msg_type = message_type_t::seconds}, Seconds {.seconds = {'3', '6', '0', '0', '0'}}, std::array {'\n'},
Header {.msg_type = static_cast<message_type_t>('?')}, std::array {'x', 'y', '\n'},
Header {.msg_type = message_type_t::milliseconds}, Milliseconds {.milliseconds = {'0', '4', '2'}},
std::array {'\n'}
);
std::println("cboe top:");
for (auto const [header, payload]: stream | rbe::views::many<line_view>()) {
payload.match( // clang-format off
[](proxy<Seconds> const s) { std::println(" seconds {}", as_text(s.field<"seconds">().as_span())); },
[](proxy<Milliseconds> const ms) { std::println(" milliseconds {}", as_text(ms.field<"milliseconds">().as_span())); },
[](rbe::unmatched auto const other) { std::println(" skipped a line, id known: {}", other.known_id); }
); // clang-format on
}
}
An unknown message type is not an error here: the extent does not depend on the id, so the line is yielded
as an any with an unknown id and iteration moves on to the next line.
See also: Frames reference, custom frames.