webcpp::document brings JavaScript programmers the wonders of C++ without the risks of C++. It is
a header-only JSON document for C++20, one of the libraries of
webcpp, whose behaviour is JavaScript’s: what JSON.parse,
property access, assignment, delete, the array methods and JSON.stringify do in JavaScript,
document does in C++, with the same key order and the same text out, so that a JavaScript peer
and a C++ program agree byte for byte.
Its tree lives in one relocatable image, built through an allocator, so that a document can live in a memory segment, be made durable, be shared between threads and processes, and travel as binary and be opened where it lands, with no parse. It reads and writes JSON and NDJSON streams, keeps numbers exact, seals images with a CRC, and answers queries over ordered keys, from C++ and from JavaScript. Its core never throws: every operation that can fail returns its error as a value.
This page teaches document concept by concept, from JavaScript’s side: what each part does, its smallest example, its variations and its rules. Every C++ program it shows was compiled and run, natively and, where the program does not need Boost.Asio, Boost.Cobalt, threads, shared memory or xactor, on emscripten and for wasm32-wasip2 and wasm32-wasip3 too (Testing), and printed the output shown under it, which is what Node prints for the same JavaScript wherever the page does not say otherwise. The listings of the JavaScript module are copied from the tests that run it. The reference that closes it lists every name the headers declare.
Introduction
webcpp::document brings JavaScript programmers the wonders of C++ without the risks of C++.
The wonders are what JavaScript does not give:
-
JSON and NDJSON streams, read as the bytes arrive, in chunks of any size (Streams of documents).
-
Exact numbers: a number is stored as the decimal that was written, and
getDecimalreads it back in full (Numbers). -
Images sealed by a CRC, so that a byte changed by accident is refused when an image is opened (Sealed images).
-
Memory you manage: one relocatable image,
reserveandcapacity,shrink_to_fitandcompact, pools of blocks, and no garbage collector in the way (Images). -
Shared documents, counted by reference and copied on write, between threads and between processes (Sharing).
-
Binary transmission: an image is sent as it is and opened in place, with no parse and no allocation on receipt (Images on the wire).
-
Ordered-index queries: lower bounds, equal ranges and prefix queries over an object’s keys, with no sorting (Keys in order).
The risks are the ones of C++: a pointer that dangles, a buffer that overflows, behaviour that is undefined. document leaves them out. A handle never fails, a write completes or changes nothing, the core never throws, and a full buffer is an error value: a failure is a value the program tests, and not a crash (Handles).
Where it wins outright is binary transmission, where moving an image costs nothing or a memory copy: between a page and its Worker, between the threads of a process, through shared memory between processes, and from a socket into a pool block on a machine or a fast local link whose receiver’s CPU is the bottleneck (What C++ gives JavaScript). It is honest about where it loses. It parses more slowly than Boost.JSON. An image is many times larger than its JSON text, so over a network the JSON text, compressed, is the smaller payload. And a seal is a CRC against accidental damage, not authentication: never open an image from a source you do not trust (Where document does not win).
The JavaScript programmer also keeps JavaScript’s ways with JSON. A web application speaks JSON with
JavaScript, and JavaScript decides what that JSON means: the order in which an object’s keys come
out, what JSON.stringify writes for a number, what splice removes. A C++ program that is to
agree with a JavaScript peer byte for byte needs a JSON document that makes the same decisions.
document is that document: what JSON.parse, property access, assignment, delete, the array
methods and JSON.stringify do in JavaScript, it does in C++, with the same key order and the same
text out. Its module for JavaScript is taught in Using it from JavaScript.
Why not Boost.JSON
Boost.JSON is the JSON library of Boost, and document reads JSON with its parser. Its document,
boost::json::value, is another matter, for the closer compatibility with JavaScript that
document was written for. Measured on Boost 1.92:
-
Key order.
boost::json::objectkeeps keys in the order they were inserted. JavaScript puts the keys that are array indices first, ascending, then the others in the order they were created:JSON.parse('{"b":1,"2":2,"a":3,"1":4}')holds{"1":4,"2":2,"b":1,"a":3}(Key order). -
Erasing reorders.
object::erasemoves the last member into the erased one’s place: erasingafrom{"a":1,"b":2,"c":3,"d":4}leaves{"d":4,"b":2,"c":3}, where JavaScript’sdeleteleaves{"b":2,"c":3,"d":4}. -
Numbers are doubles.
0.1000000000000000000000000000000001is read into a double and written back as1E-1. JavaScript rounds it too, to0.1; document keeps it exact by default, and writes every number as JavaScript writes it (Numbers). -
Hash flooding. An object’s index hashes its keys, so a crafted input of 4 MB, keys that collide, took 9.23 s to parse. Keys come from the network; document indexes them in ordered trees, which no input degrades.
-
Pointers. A
boost::json::valueholds pointers, so it can neither live in a memory segment shared between processes nor travel as binary. document’s tree lives in one relocatable image, every pointer in it an offset (Images).
document uses Boost.JSON’s SAX parser, boost::json::basic_parser, and nothing else of it.
Compared with Boost.JSON puts the two side by side, program by program, and says what Boost.JSON does better.
A first document
This program parses a JSON text, reads two members, writes one, pushes an element onto an array, and writes the document back with an indent of 2. It begins with the library’s header and an alias for its namespace:
#include <webcpp/document.hpp>
#include <cstdint>
#include <iostream>
#include <string_view>
namespace wd = webcpp::document;
wd::parse is JSON.parse. Its result holds the document, or the error that made none: the core
of document never throws, and every operation that can fail returns wd::result, Boost.System’s
result:
// JSON.parse, whose result holds the document or the error.
wd::result<wd::document> made =
wd::parse(R"({"name":"Ada","born":1815,"langs":["Analytical"]})");
if (made.has_error()) {
std::cout << "error: " << made.error().message() << '\n';
return 1;
}
wd::document& doc = *made;
doc["name"] is a handle, the place doc.name names. value_or is JavaScript’s ??: the value
as the type of the fallback, or the fallback when nothing is there, or when what is there is not
of that type:
// doc.name ?? "nobody", and doc.born ?? 0.
std::cout << doc["name"].value_or(std::string_view("nobody")) << " was born in "
<< doc["born"].value_or(std::int64_t{0}) << '\n';
set is assignment, and push is Array.prototype.push. Each returns its result, which converts
to true on success; a write that fails changes nothing:
// doc.known = true; doc.langs.push("Note G"); each write returns its result.
if (!doc["known"].set(true) || !doc["langs"].push("Note G")) {
std::cout << "error: the document is full\n";
return 1;
}
stringify(2) is JSON.stringify(doc, null, 2):
// JSON.stringify(doc, null, 2).
std::cout << doc.stringify(2) << '\n';
It prints:
Ada was born in 1815
{
"name": "Ada",
"born": 1815,
"langs": [
"Analytical",
"Note G"
],
"known": true
}
The new member known comes last, as JavaScript adds it, and the text is byte for byte what Node
writes for the same operations.
Requirements
-
A C++20 compiler. document’s tests and examples are built and run natively with Clang and GCC, on emscripten under node, and for wasm32-wasip2 and wasm32-wasip3 with the Clang of wasi-sdk, under wasmtime (Testing).
-
Boost 1.92 or newer: Interprocess for the image’s heap and its offsets, MultiIndex for the tree’s indices, Decimal for exact numbers, System for
resultand the error codes, Charconv for reading and writing doubles, and JSON for its parser. document is header-only; of Boost, only JSON’s parser and Charconv are compiled, once, by webcpp’s build. A b2 project uses document through/webcpp/document//document, which brings all of them. -
webcpp’s configuration, config, whose
<webcpp/config.hpp>every header of document includes first. -
Exceptions are the program’s choice. The core never throws, in a build with exceptions or without; the sugar that throws as JavaScript does exists only with them (The sugar that throws).
-
Optional: Boost.Asio, for the readers and writers of streams and images, and Boost.Cobalt for its coroutines, in
<webcpp/document/asio.hpp>, which builds natively only (With Boost.Asio); xactor for the actor example (In an actor system); Emscripten, for the JavaScript module (Using it from JavaScript). -
Nothing else: no threads, no clock, no file, no global or static mutable state.
Reading this page
<webcpp/document.hpp> includes every public header, whose names are in the namespace
webcpp::document. The code of this page writes them wd::parse and wd::document, as after
namespace wd = webcpp::document;.
What C++ gives JavaScript lists what C++ gives JavaScript, and Using it from JavaScript teaches the module that brings it to a page, a Worker and Node. From JavaScript sets JavaScript’s ways with JSON beside document’s, and lists where the two differ on purpose. Handles teaches the handles that read and write a document; Numbers, Strings, dates and bytes and Key order the values they hold and the order of an object’s keys; Arrays the array methods. Writing JSON, Parsing, Streams of documents and Selectors write and read JSON text, whole or in pieces, and Containers shows the containers a C++ program walks. Traits chooses what a document stores; Images shows the binary image, Images on the wire sends it, and Sharing shares it. Compared with Boost.JSON compares document with Boost.JSON, The sugar that throws teaches the sugar that throws, and Errors lists every error. Testing shows how document is tested, Benchmarks what it costs, and the reference closes the page.
What C++ gives JavaScript
webcpp::document brings JavaScript programmers the wonders of C++ without the risks of C++. A JavaScript programmer who reaches for C++ gets control over memory, exact arithmetic, shared memory and bytes that move without a parse, and pays for them with pointers that dangle, buffers that overflow and behaviour that is undefined. document keeps the first and refuses the second: a handle never fails, a write completes or changes nothing, the core never throws, and a full buffer is an error value (Handles).
This section lists what document gives that JavaScript does not, each with the smallest program that shows it and a link to the section that teaches it. The programs are C++; the same document is reached from JavaScript by the module of Using it from JavaScript, which gives a page or a Worker the points that need no C++ to write. Binary transmission comes first, because it is where document wins outright, and the last subsection says where it does not.
Binary transmission
A document is its own wire format. doc.image() is its bytes, and a receiver opens them where they
landed: wd::open(buffer), or Document.open(bytes) in JavaScript, reads a header of 64 bytes and,
for a sealed image, a CRC, and the document is there. Nothing is parsed and no tree is built, and
the receiver allocates nothing per value and nothing per message. A JSON text must be parsed on
arrival, and a parse allocates for every value it holds.
That is where document wins outright, in the places where moving the image costs nothing or a memory copy:
-
A page and its Worker. The image crosses as a transferred
ArrayBuffer, which moves in constant time, wherepostMessageof an object makes a structured clone of every node (An image sent to a Worker). -
Threads of one process. A document moves through a queue as a small handle, and its image stays where it is (Threads).
-
Processes of one machine. A pool of blocks in shared memory, and the offset of a block sent through a queue (Processes).
-
A socket, on one machine or a fast local link. A reader fills a block of a pool straight from the stream and opens the image there, so the receiver’s CPU, which is the bottleneck on such a link, spends none of its time parsing (Images on the wire).
This program copies an image into a buffer of the program’s own and opens it there, with no parse; Boost.JSON, for which a value points into its memory resource, can only be serialized and parsed again:
constexpr std::string_view text = R"({"user":"Ada","langs":["Analytical","Note G"]})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
return false;
}
const std::span<const std::byte> image = made->image();
// A buffer of the program's, 16-aligned, large enough for the image.
alignas(16) std::array<std::byte, 8192> buffer{};
if (image.size() > buffer.size()) {
return false;
}
std::ranges::copy(image, buffer.begin());
// Opened in place: the buffer becomes the document, with no parse.
wd::result<wd::basic_document<wd::default_traits, wd::external>> opened =
wd::open(std::span<std::byte>(buffer.data(), image.size()));
if (opened.has_error()) {
return false;
}
std::cout << "5. An image copied to another buffer and opened in place\n";
print_document(opened->stringify());
// A Boost.JSON value points into its memory resource, so its bytes cannot be copied: the
// only way to move it is to serialize it and parse it again.
print_boost(boost_text(made->stringify()));
5. An image copied to another buffer and opened in place
document: {"user":"Ada","langs":["Analytical","Note G"]}
Boost.JSON: {"user":"Ada","langs":["Analytical","Note G"]}
Over a network the text is the smaller payload. An image is several times the size of the JSON text it holds, since it keeps a node of 192 bytes for each value (Memory per value), and it is still larger once both are compressed (The sizes on the wire). On a link that carries the bytes, compressed JSON is smaller and arrives sooner, unless the receiver’s parse costs more than the extra bytes take to cross.
JSON and NDJSON streams
JavaScript reads one JSON text with JSON.parse, and reads a stream by cutting it into texts first.
wd::stream_parser takes the bytes of a stream as they arrive, in chunks of any size, split
anywhere, and delivers each document as soon as the bytes that end it are in: NDJSON, JSON text
sequences, and concatenated JSON with or without whitespace between the documents
(Streams of documents). A bad line can be dropped, with its line number reported, and the stream
goes on (Errors). Reading NDJSON by hand takes a loop over next():
/**
NDJSON: each document ends at its line's newline, so a line may end in CRLF, a line may be empty,
and the last line may lack its newline, which finish closes.
*/
bool read_ndjson() {
std::cout << "NDJSON, taken with next():\n";
wd::stream_options options;
options.framing = wd::framing::lines;
wd::stream_parser p(options);
constexpr std::string_view text =
"{\"id\":1,\"tags\":[\"a\",\"b\"]}\n"
"{\"id\":2,\"note\":\"first\\nsecond\"}\r\n"
"\n"
"[3,4.5]\n"
"null";
if (!write_in_pieces(p, text, options.on_error)) {
return false;
}
if (const wd::result<std::size_t> finished = p.finish(); finished.has_error()) {
report_error(p, finished.error());
return false;
}
take_ready(p, options.on_error);
return true;
}
NDJSON, taken with next():
{"id":1,"tags":["a","b"]}
{"id":2,"note":"first\nsecond"}
[3,4.5]
null
The same parser reads from Boost.Asio, Boost.Cobalt and an actor system, and a JavaScript sender can feed it chunk by chunk (With Boost.Asio, Streams).
Exact numbers
A JavaScript number is a double, and 0.1 + 0.2 is 0.30000000000000004. document stores a number
as the decimal that was written, in a decimal128, writes it as JavaScript writes a number, and
leaves the arithmetic to the program: in the traits’ own number type it is exact, and get<T>()
refuses to round. js_traits stores doubles, to read a number as JavaScript would
(Numbers):
// 0.1 + 0.2, computed by the program in the traits' own number type, then stored.
wd::result<wd::document> exact = wd::parse(R"({"a":0.1,"b":0.2})");
wd::result<wd::basic_document<wd::js_traits>> js =
wd::parse<wd::js_traits>(R"({"a":0.1,"b":0.2})");
if (exact.has_error() || js.has_error()) {
return 1;
}
using decimal = boost::decimal::decimal128_t;
const wd::result<decimal> a = (*exact)["a"].get<decimal>();
const wd::result<decimal> b = (*exact)["b"].get<decimal>();
const wd::result<double> x = (*js)["a"].get<double>();
const wd::result<double> y = (*js)["b"].get<double>();
if (!a || !b || !x || !y || !(*exact)["sum"].set(*a + *b) || !(*js)["sum"].set(*x + *y)) {
return 1;
}
std::cout << exact->stringify() << '\n' << js->stringify() << '\n';
{"a":0.1,"b":0.2,"sum":0.3}
{"a":0.1,"b":0.2,"sum":0.30000000000000004}
A JavaScript peer reads the exact digits of a number back with getDecimal, where get gives the
Number that rounds them (Exact numbers).
Sealed images
An image is a function of the operations that made it, the same bytes on every target. seal()
writes the CRC-32C of those bytes into its header, and opening checks it, so a bit flipped on a disk
or a wire, a torn write or a stray write is refused as errc::damaged_document before any of the
image is read (Sealed images):
// A sealed image with one byte changed is refused as damaged, by open and by from_image.
const auto found =
std::ranges::search(bytes, std::as_bytes(std::span(std::string_view("Ada"))));
if (found.empty()) {
return false;
}
found.front() = std::byte{'a'};
if (!refused("one byte changed", wd::open(bytes))) {
return false;
}
one byte changed: damaged_document, a sealed image whose bytes do not match its seal
A seal is a CRC against accidental damage and is not authentication: anyone can compute a valid seal for any bytes. Never open an image from a source you do not trust; data from an untrusted source enters as JSON text, which the parser checks entirely (What is refused).
Memory you manage
A document lives in one block of memory, a single relocatable image, and there is no garbage
collector in the way: the program decides when the memory grows, when it shrinks and when it goes.
reserve grows the image once for a batch (the way std::vector::reserve does) and capacity says
how large it is; shrink_to_fit gives back the free space at its end, and compact rewrites the
image without the holes that deleted values leave (Compacting); free() in
JavaScript releases a document at once, where the collector would. A pool of blocks, allocated
once, leases a block to each image that arrives and takes it back, zeroed, when the document goes
(Images on the wire):
wd::result<wd::document> made = wd::parse("{}");
if (made.has_error()) {
return false;
}
for (std::uint64_t i = 0; i < 1000; ++i) {
if (!(*made)[std::to_string(i)].set(std::string(100, 'x'))) {
return false;
}
}
const std::uint64_t grown = made->image().size();
if (!made->root().set(wd::object{{"kept", true}}) || !made->shrink_to_fit()) {
return false;
}
std::cout << "grown to " << grown << " bytes, shrunk to " << made->image().size() << ": "
<< made->stringify() << '\n';
// Blocks never move: a deep copy into a new document packs the content.
wd::result<wd::document> packed = wd::document::create();
if (packed.has_error() || !packed->root().set(*made)) {
return false;
}
std::cout << "packed: " << packed->image().size() << " bytes\n";
grown to 524288 bytes, shrunk to 352864: {"kept":true}
packed: 4096 bytes
/** A pool of two blocks: the most the client leases at once. */
wd::image_pool pool_of_two() {
return wd::image_pool::create(block_bytes, 2).value();
}
Shared documents with copy on write
wd::shared_document is std::shared_ptr for a document: copies share one image, a count in the
block’s header says how many hold it, and the last one gives the block back. A holder that wants to
write calls make_mutable, which copies the image into a block of its own when others hold it, so
the others keep the document as it was. The count lives in the pool, so it is the same in every
thread and every process that maps it (Sharing):
// make_mutable copies the image into a block of the writer's own when other holders exist, so the
// writer changes its copy and the others keep the document as it was. The library does not
// synchronise writers: a block with several writers, or a writer and readers of that same block,
// must wait for each other by the program's own means, such as a mutex. This writer changes only
// its copy, which no other holder reads.
void writer_task(wd::shared_document<> holder, std::string& out) {
if (holder.make_mutable().has_error() || holder["name"].set("Lovelace").has_error()) {
out = "error";
return;
}
out = describe(holder);
}
writer: Lovelace was born in 1815 after the writer: Ada was born in 1815, use_count 1, 6 of 7 blocks free last copy dropped: 7 of 7 blocks free
Ordered-index queries
An object is kept in two orders at once: JavaScript’s, which iteration follows, and key order, which
by_key() gives with lower_bound, upper_bound and equal_range. A prefix query is two bounds,
and nothing is sorted: a JavaScript program sorts every key to get the same answer, and gets it
again at each query (Keys in order):
// by_key is the same object in key order, by UTF-16 code units, with bounds for ranges of keys.
// A JavaScript program sorts every key to get any of these; here the tree keeps them sorted.
wd::result<wd::document> catalog_made = wd::parse("{}");
if (catalog_made.has_error()) {
return fail(catalog_made);
}
wd::document& catalog = *catalog_made;
wd::result<wd::object_ref> as_catalog = catalog.root().as_object();
if (as_catalog.has_error()) {
return fail(as_catalog);
}
const wd::object_ref catalog_obj = *as_catalog;
const std::array<std::string_view, 13> names{
"user_3", "admin", "user_1", "guest", "user_9", "user", "user_2",
"owner", "user_7", "user_5", "user_4", "user_8", "user_6",
};
for (const std::string_view key : names) {
const auto added = catalog_obj.insert(key, true);
if (added.has_error()) {
return fail(added);
}
}
std::cout << "JavaScript order:";
for (const auto& member : catalog_obj) {
std::cout << ' ' << member.first;
}
std::cout << '\n';
const auto keys = catalog_obj.by_key();
std::cout << "by_key order:";
for (const auto& member : keys) {
std::cout << ' ' << member.first;
}
std::cout << '\n';
// The keys with the prefix "user_" lie from its first key up to the first key at or after
// "user`", the character after '_'.
const std::ranges::subrange users(keys.lower_bound("user_"), keys.lower_bound("user`"));
std::cout << "prefix user_: " << std::ranges::distance(users) << " keys, from "
<< users.begin()->first << " to " << std::ranges::prev(users.end())->first << '\n';
const auto [exact, exact_end] = keys.equal_range("user_5");
std::cout << "equal_range user_5: " << std::ranges::distance(exact, exact_end) << " member, "
<< exact->first << '\n';
std::cout << "lower_bound p: " << keys.lower_bound("p")->first << '\n';
const std::ranges::subrange between(keys.lower_bound("g"), keys.lower_bound("v"));
std::cout << "keys from g up to v: " << std::ranges::distance(between) << '\n';
JavaScript order: user_3 admin user_1 guest user_9 user user_2 owner user_7 user_5 user_4 user_8 user_6 by_key order: admin guest owner user user_1 user_2 user_3 user_4 user_5 user_6 user_7 user_8 user_9 prefix user_: 9 keys, from user_1 to user_9 equal_range user_5: 1 member, user_5 lower_bound p: user keys from g up to v: 12
Where document does not win
document is a different trade from Boost.JSON and from JavaScript’s own objects, and some of it is a loss:
-
Parsing is slower than Boost.JSON’s and than
JSON.parse. Every value is a node of a tree of indices inside the image, which the parse must build; Boost.JSON appends to contiguous arrays. The benchmarks measure it, natively, in Node and in Chrome (Reading the results). -
An image is larger than its JSON text, many times larger, and still larger when both are compressed. On a narrow network, compressed JSON is the smaller payload and the better choice (The sizes on the wire).
-
A seal is not authentication. It is a CRC against accidental damage. An image is trusted input, and an image from a source that is not trusted must never be opened (What is refused).
-
A lookup walks a tree where JavaScript and Boost.JSON hash, and an array splice renumbers the positions after it (Reading the results).
What document asks for in return is what the other sections teach: a document that behaves as JavaScript does, that moves as bytes, that lives in memory its program manages, and that cannot make the program crash.
Using it from JavaScript
document is also a JavaScript module. js/webcpp-document.mjs is an ES module that wraps the
library, compiled to WebAssembly with Emscripten, in two classes, Document and StreamParser,
and runs in Node, in a page and in a Worker. A JavaScript program keeps JavaScript’s values and
JavaScript’s errors, and gets what JavaScript lacks: exact numbers, queries over ordered keys, a
binary image it can send to a Worker with no parse, and memory it frees when it chooses. This
section teaches the module; the sections before it teach the C++ library behind it.
The listings of this section are literal, and are not compiled by this page: each is copied from a
test or from the example of libs/document/js, where Node’s test runner runs the tests and a headless
Chromium runs the example, so that what they show is checked. The files are named above each
listing.
The package is built, and its tests run, with b2 and Emscripten’s toolset; the module is the
webcpp-document-core program, an ES module, which webcpp-document.mjs imports and awaits when it
loads:
b2 toolset=emscripten libs/document/js//package
b2 toolset=emscripten libs/document/test/js//js-tests
The tests are the own lane js of the library (libs/document/test/js), which runs on
Emscripten’s toolset: js-tests and shared-tests run Node’s test runner on the packages,
npm-package-test tests the npm package as a user receives it, and js-chrome and shared-chrome
run the examples in headless Chromium.
Documents
A Document is made by Document.parse(text), which is JSON.parse and throws the SyntaxError
that JSON.parse throws, or by Document.open(bytes) (Images). A place in the
document is a path: an array of string keys and non-negative integer indices from the root, []
being the root itself. A path that leads to nothing reads undefined, as property access does.
get, set, erase, push, splice, length and stringify do what their names do in
JavaScript:
const doc = Document.parse('{"user":{"name":"Ada","age":36,"langs":["C"]},"n":null,"t":false}')
assert.equal(doc.get(['user', 'name']), 'Ada')
assert.equal(doc.get(['user', 'age']), 36)
assert.deepEqual(doc.get(['user', 'langs']), ['C'])
assert.equal(doc.get(['n']), null)
assert.equal(doc.get(['missing', 'deeper']), undefined)
assert.equal(doc.get(['user', 'langs', 0]), 'C')
doc.free()
get returns a scalar as its JavaScript value and a container as a copy, and toJSON() is get([]),
so JSON.stringify(doc) works on a document. set writes what JSON.stringify writes for its
value, so undefined erases a member, and -0 is 0. erase says whether the key was there:
const doc = Document.parse('{"user":{"name":"Ada"},"langs":[]}')
doc.set(['user', 'age'], 30)
doc.set(['user', 'tags'], { a: [1, 'two', null] })
doc.set(['pi'], 3.14159)
doc.set(['zero'], -0)
assert.equal(doc.get(['user', 'age']), 30)
doc.set([], [true])
assert.equal(doc.stringify(), '[true]')
doc.free()
push returns the new length, as Array.prototype.push does, and splice takes the arguments
of Array.prototype.splice, negative positions and clamped counts included, and returns the elements it removed:
const doc = Document.parse('{"langs":["C","Rust","Go"]}')
assert.equal(doc.push(['langs'], 'Note G'), 4)
assert.equal(doc.length(['langs']), 4)
assert.deepEqual(doc.splice(['langs'], 0, 1), ['C'])
assert.deepEqual(doc.splice(['langs'], -1, 1, 'JS', 'Lisp'), ['Note G'])
assert.equal(doc.stringify(), JSON.stringify({ langs: ['Rust', 'Go', 'JS', 'Lisp'] }))
doc.free()
stringify(indent) is JSON.stringify(doc, null, indent), with the same bytes, and the key order is
JavaScript’s (Key order).
Errors
An operation that fails throws the error JavaScript would throw for it: SyntaxError for malformed
text, RangeError for a number out of range or for no room left in a buffer, and TypeError for a
value of another kind, a write through something that is not a container, a value that is not JSON (a
function, a symbol, a BigInt), a path that is not an array of keys and indices, bytes that are not an
image, a sealed image that is damaged, and a document that was freed. These are the errc values of
Errors, each under the name JavaScript gives it. An operation that throws changes
nothing:
const doc = Document.parse('{"obj":{"a":1},"arr":[1,2],"num":5,"str":"s"}')
assert.throws(() => doc.set(['missing', 'x'], 1), TypeError)
assert.throws(() => doc.push(['obj'], 1), TypeError)
assert.throws(() => doc.erase(['arr', 0]), TypeError)
assert.throws(() => doc.splice(['num'], 0, 1), TypeError)
assert.throws(() => doc.set(['f'], () => 1), TypeError)
assert.throws(() => doc.get(['arr', 1.5]), TypeError)
doc.free()
Exact numbers
get gives the Number of a JSON number, which rounds a number that a double cannot hold.
getDecimal gives the number exactly, as the text of its decimal, so a price or an identifier of
thirty digits survives a round trip through a JavaScript program (Numbers):
const text = '0.1234567890123456789012345678901234'
const doc = Document.parse(`{"price":${text},"small":1.50}`)
assert.equal(doc.getDecimal(['price']), text)
assert.equal(doc.get(['price']), Number(text))
assert.equal(doc.getDecimal(['small']), '1.5')
assert.equal(doc.get(['small']), 1.5)
doc.free()
Ordered keys
The keys of an object are also kept in key order, so a range of keys is found without sorting them.
keysInRange(path, low, high) gives the keys from low up to, and not including, high; a prefix
query is the range from the prefix to the prefix followed by (Keys in order):
const doc = Document.parse('{"users":{"user_2":{},"user_10":{},"admin":{},"user_1":{},' +
'"user_":{},"user`":{}}}')
assert.deepEqual(doc.keysInRange(['users'], 'user_', 'user_'),
['user_', 'user_1', 'user_10', 'user_2'])
assert.deepEqual(doc.keysInRange(['users'], 'x', 'y'), [])
doc.free()
Images
A document is its image, and the module hands the image to the program. doc.byteLength is its size,
and doc.image() is a Uint8Array that views the bytes where they are in WebAssembly memory, with
no copy: it stays valid until the memory grows or the document changes, so a program that keeps
the bytes copies them with slice(). Document.open(bytes) makes a document of an image. When the
bytes are a view of WebAssembly memory, 16-aligned, such as another document’s image(), it opens
them in place and shares them; otherwise it copies them into the module’s memory first, which is a
memory copy and no parse. A document opened from bytes is its own, and the bytes it was opened from
may change or go. seal() writes the CRC of the image into it, which open checks, and refuses a
sealed image whose bytes changed with a TypeError (Sealed images):
const doc = Document.parse('{"a":[1,2,3]}')
assert.equal(doc.byteLength, doc.image().byteLength)
assert.equal(doc.seal(), undefined)
const image = doc.image().slice()
assert.equal(Document.open(image).stringify(), '{"a":[1,2,3]}')
image[image.length - 1] ^= 0xff
assert.throws(() => Document.open(image), TypeError)
doc.free()
An image is trusted input. open refuses bytes that are not an image, and a seal catches bytes
that changed by accident; it does not authenticate them, and an image from a source that is not
trusted must never be opened (What is refused).
Memory
A document lives in WebAssembly memory, outside the heap that JavaScript’s collector manages, in one relocatable image. Memory is therefore a thing the program holds, not a thing the collector finds:
-
doc.free()releases the image at once, and a secondfree()does nothing. A freed document refuses every operation with aTypeError. A program that makes many documents, such as one per message of a stream, frees each when it is done. -
A document that is never freed is released when the collector collects its object, through a
FinalizationRegistry, which runs when the engine chooses and not when the program needs the memory. It is a safety net and not a plan. -
The module’s memory grows on demand, up to 4 GB, and WebAssembly never returns memory to the system; a freed image returns to the module’s allocator, for the next document to use.
-
image()is a view of that memory andbyteLengthits size, so a program knows what a document costs, and a document that is to travel as bytes is already the bytes.
A parsed document starts at 4096 bytes and grows as it needs. doc.capacity is the number of bytes
its image can hold before a change must move it, and doc.reserve(bytes) makes the image hold at
least that many, so a program that knows how large a document will grow pays for the room once
(Reserving). A size that is not a non-negative integer, or that is past the document’s
limit, is a RangeError, and nothing changes. shrink_to_fit and compact of the C++ library,
which pack an image, are not in the module.
const doc = Document.parse('{"a":1}')
const before = doc.capacity
assert.ok(before >= doc.byteLength)
assert.equal(doc.reserve(before * 4), undefined)
assert.ok(doc.capacity >= before * 4)
assert.equal(doc.stringify(), '{"a":1}')
doc.free()
An image sent to a Worker
A page parses a document, copies its image into an ArrayBuffer of its own and transfers the
buffer to a Worker. The transfer moves the buffer in constant time and detaches it from the sender,
where postMessage of an object would clone every value of it. The copy is made first because a
view of WebAssembly memory cannot be transferred. The Worker opens the image in its own memory, which
is one memory copy, and reads from it: nothing is parsed there. The page is js/example/page.mjs:
const doc = Document.parse(text)
doc.set(['user', 'age'], 36)
const bytes = doc.image().slice()
doc.free()
worker.postMessage({ image: bytes.buffer }, [bytes.buffer])
and the Worker, js/example/worker.mjs, which answers with what the document holds:
self.onmessage = ({ data }) => {
const doc = Document.open(new Uint8Array(data.image))
self.postMessage({
name: doc.get(['user', 'name']),
age: doc.get(['user', 'age']),
langs: doc.get(['user', 'langs']),
price: doc.getDecimal(['price']),
text: doc.stringify(),
})
doc.free()
}
A headless Chromium runs the example and fails unless the Worker answered what the document holds
and the transfer detached the buffer. How the transfer compares with postMessage of an object is
measured in Benchmarks.
Documents shared between workers
The transfer above gives each Worker a copy of the image, and one Worker’s copy is its own. A second
build of the module, js/shared/webcpp-document-shared.mjs, shares documents instead: its
WebAssembly memory is one SharedArrayBuffer, and every Worker the program creates can join it.
Several Workers then read the same document at once, in place: nothing is transferred and nothing
is copied, and the image a Worker reads is at the address, in that memory, where the page parsed it.
It is the shared_document of Sharing, taught from JavaScript.
Every document of this module is in the memory’s pool, whose blocks are the room of an image, and a
Document is one reference to a document, as a std::shared_ptr is: useCount counts the
references of every Worker, and the last one given up, by free() or by the collector, gives the
block back. A write is every holder’s, since they read the same bytes. makeMutable() first makes
the document the writer’s own: a copy in a block of its own when others hold it, the same block when
none does, while the others go on reading the original (Copy on write).
The page starts the module when it imports the file, and hands a Worker two things in a message:
connection(), which the Worker joins, and a token for the document. The Worker awaits
join(connection), which runs the module on the shared memory in that Worker, and then adopts the
token. The page, js/shared/example/page.mjs, parses a document and gives its token to four
Workers:
const free = pool.available
const doc = Document.parse('{"user":{"name":"Ada","langs":["C","Note G"]},"price":1.50}')
const address = doc.image().byteOffset
say(`page: parsed into one block of the pool, use count ${doc.useCount}`)
const workers = Array.from({ length: 4 }, start)
const adopted = await Promise.all(workers.map(worker =>
worker.send({ command: 'adopt', connection: connection(), token: doc.share() })))
and a Worker, js/shared/example/reader.mjs, joins, adopts, and writes on a copy of its own:
const commands = {
async adopt({ connection, token }) {
await join(connection)
doc = SharedDocument.adopt(token)
return describe()
},
// The document made this worker's own, a copy in a block of its own, then changed.
write({ name }) {
doc.makeMutable()
doc.set(['user', 'name'], name)
return describe()
},
free() {
doc.free()
return {}
},
}
A Worker that is done calls leave(): it drops every reference it holds, gives its slot back and may
join again. A Worker that calls free() on a document drops that reference alone.
A token is what share() returns: a small object, { offset, ticket, generation }, that
postMessage clones, and that keeps one reference to the document until a thread adopts it.
SharedDocument.adopt(token) takes the reference over, once; SharedDocument.drop(token) gives it
back without adopting it, as a receiver does that does not want the document. A token that was
adopted or dropped is spent, and adopt and drop refuse it with a TypeError, even when its
block holds another document by then, as does anything that is not a token:
const doc = Document.parse(text) const token = doc.share() SharedDocument.drop(token) assert.equal(doc.useCount, 1) assert.throws(() => SharedDocument.adopt(token), TypeError) assert.throws(() => SharedDocument.drop(token), TypeError)
A headless Chromium runs the example, and fails unless the page prints these lines, in which the four Workers read the page’s own address and the one that wrote does not:
cross-origin isolated: true page: parsed into one block of the pool, use count 1 worker 1: reads Ada and C, Note G, at the page's address: true, in a SharedArrayBuffer: true worker 2: reads Ada and C, Note G, at the page's address: true, in a SharedArrayBuffer: true worker 3: reads Ada and C, Note G, at the page's address: true, in a SharedArrayBuffer: true worker 4: reads Ada and C, Note G, at the page's address: true, in a SharedArrayBuffer: true page: use count 5, blocks in use 1 worker 4: makeMutable, then set: reads Grace, at the page's address: false page: reads Ada, use count 4, blocks in use 2 page: every worker freed its reference, use count 1 page: freed, every block back in the pool: true
A SharedArrayBuffer exists only in a page that is cross-origin isolated, which the server asks of
the browser with two headers on the page and on every file it loads: Cross-Origin-Opener-Policy:
same-origin and Cross-Origin-Embedder-Policy: require-corp. Without them the module does not
start. Node needs neither.
The memory is laid out once, by the thread that starts the module, and has limits:
-
The memory is fixed at 128 MiB and does not grow, as growing a shared memory would make every Worker refresh its views of it. A full heap is an error, a
RangeError, and not an abort. -
The pool has 64 blocks of 512 KiB. A document is one block, so an image can be no larger than the room of a block: a change past it is a
RangeError, and so ismakeMutable()orDocument.parsewhen no block is free.pool.available,pool.blocksandpool.blockBytestell what is left. -
At most 32 Workers are joined at once; a thirty-third
joinis aRangeErroruntil one leaves. At most 1024 tokens are in flight at once. -
A Worker that is terminated without calling
leave()keeps its references and its slot for the life of the memory: its documents are not given back. A Worker that is done callsleave()first. -
join.js, the Emscripten library that lets a Worker the program creates run the module, reads internals of Emscripten 6.0.11 (createWasm,wasmMemory,updateMemoryViews), so the module is tied to that version of Emscripten until it is checked against another.
Streams
StreamParser is the stream parser of Streams of documents, with the same framings, concatenated
(the default), lines for NDJSON and rfc7464, and the same policy on errors, onError stop
or skip, the latter for the framings that can go on. StreamParser.from(source, options) is an
async generator that takes a ReadableStream of bytes or strings, such as a response body, or any
async iterable of them, and yields a Document as soon as the bytes that end it arrive, whatever
the chunks:
for await (const doc of StreamParser.from(chunked(encoder.encode(lines), size), { framing: 'lines' })) {
assert.ok(doc instanceof Document)
out.push(doc.toJSON())
}
A bad line throws a SyntaxError once the documents before it are yielded, and under onError
skip it is dropped and counted in parser.dropped. The end of the source ends the stream, as
finish() does, so a top-level number or a last line without its newline is delivered
(Errors).
The parser can also be driven by hand, which is how a sender written in JavaScript feeds the parser
compiled from C++: write takes a chunk, read gives the next complete document or undefined,
and finish ends the stream:
const parser = new StreamParser({ framing: 'lines' })
parser.write(encoder.encode('{"a":'))
assert.equal(parser.read(), undefined)
parser.write(encoder.encode('1}\n'))
assert.deepEqual(parser.read().toJSON(), { a: 1 })
assert.equal(parser.read(), undefined)
parser.finish()
Each document read or from gives is a Document, in WebAssembly memory like any other, which
the program frees when it has used it.
From JavaScript
document’s interface is JavaScript’s ways with JSON, written in C++: a name of JavaScript becomes a member function of the same meaning, and an operation that JavaScript makes throw returns its error instead. A program that knows how JavaScript reads and writes a parsed object knows how document does. This section is for the C++ programmer who knows JavaScript; the JavaScript programmer who wants document in a page, a Worker or Node reads Using it from JavaScript, and what C++ gives that JavaScript does not is in What C++ gives JavaScript.
JavaScript, then C++
Each row is a JavaScript expression and what a program of document writes for it, v being a
handle (Handles) and doc a document:
| JavaScript | document |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
the same names (Arrays) |
|
|
The program below prints each JavaScript expression beside what document gives for it, with this
helper, which writes undefined where nothing is there, as JavaScript prints it:
/** Prints a JavaScript expression and the value it reads, `undefined` when nothing is there. */
void show(std::string_view expression, const wd::const_value& v) {
std::cout << expression << " -> " << (v.is_undefined() ? "undefined" : v.stringify()) << '\n';
}
Handles walks through the program; every line it prints is what Node prints for the same JavaScript.
Where document differs, on purpose
A few things JavaScript does would make a document either unsafe or unlike JSON. document does
otherwise there, and each difference keeps the text JSON.stringify writes, but where the table
says it does not:
| JavaScript | document | Why |
|---|---|---|
|
|
A document is a tree, as JSON is: no value is shared, and no cycle exists. |
|
it inserts a copy of the array as it was |
The same: a tree has no cycle. |
|
erases |
Nothing stores |
|
stores |
The same text. |
arrays may have holes |
|
Arrays are dense (Arrays); the text is the same. |
|
|
A handle names a member or an element, never both. |
numbers are doubles |
exact with the default traits; JavaScript’s doubles with |
What is stored is what was sent (Numbers). |
|
the default traits refuse 40 digits with |
The default traits keep what was sent; |
|
|
|
|
refused with |
Boost.JSON’s parser, which document uses, cannot deliver one (Lone surrogates). |
a Uint8Array is written |
bytes are written as base64 by default |
The traits’ bytes policy; |
prototypes, getters, symbols, |
none |
Out of scope: a document holds JSON’s values, and dates and bytes. |
The program shows four of them: an index written as a key, a key written as an index, -0, and
the lone surrogate the parser refuses:
wd::result<wd::document> made = wd::parse(R"({"list":[10,20],"o":{"3":"three"},"z":-0})");
if (made.has_error()) {
return false;
}
wd::document& doc = *made;
show("doc.list['0']", doc["list"]["0"]); // JavaScript: 10, an index written as a key
show("doc.o[3]", doc["o"][3]); // JavaScript: "three", a key written as an index
show("doc.z", doc["z"]); // -0 is 0, as JSON.stringify writes it
say(R"(JSON.parse('"\ud800"') refused)", wd::parse(R"("\ud800")").error() == wd::errc::syntax);
doc.list['0'] -> undefined
doc.o[3] -> undefined
doc.z -> 0
JSON.parse('"\ud800"') refused -> true
JSON.parse gives 10, "three" and 0 for the first three, and keeps the lone surrogate.
Handles
A document holds one value, and a program reaches the values inside it through handles:
wd::value, which reads and writes, and wd::const_value, which reads. doc.root(), doc["key"],
doc[3] and every navigation from a handle return one. A handle names one place in one document,
and is cheap to copy: it holds the document’s address and the ids of nodes, not a pointer into
the tree.
Navigation never fails
In JavaScript, doc.user.nick is undefined when user has no nick, and doc.n.x.y throws a
TypeError when doc.n is a number. A handle never fails: it names one of three things, and
reads undefined where nothing is there.
| A handle names | Made by | Reads, and set does |
|---|---|---|
a node |
navigation to a value that exists |
the value; |
a missing member or element of a container that exists |
|
|
nothing |
navigation through |
|
The program of From JavaScript parses a user and a number, and navigates:
constexpr std::string_view text = R"({"user":{"name":"Ada","tags":["math"]},"n":3})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
return false;
}
wd::document& doc = *made;
wd::value user = doc["user"];
show("user.name", user["name"]);
show("user.nick", user["nick"]); // a missing member: set creates it
show("doc.n.x.y", doc["n"]["x"]["y"]); // nothing: JavaScript throws a TypeError here
show("user.tags[0]", user["tags"][0]);
show("user.tags.at(-1)", user["tags"].at(-1));
user.name -> "Ada" user.nick -> undefined doc.n.x.y -> undefined user.tags[0] -> "math" user.tags.at(-1) -> "math"
A handle resolves its place again at every call, so it reads what is there now: user["nick"]
reads undefined until a set creates the member, and the same handle then reads the new value.
A handle stays valid while the document grows and its image moves to a larger buffer; it reads
undefined once its value is erased; and it is invalid once its document is moved from or
destroyed, as a container’s iterators are.
Reading
kind() is JavaScript’s typeof, with null, arrays, dates and bytes told apart: one of the
enumerators of wd::kind, undefined, null, boolean, number, string, date, bytes,
array and object, each with an is_ function. get<T>() reads the value as T and returns
a result: errc::type for another kind, and errc::range for a number that T cannot hold
exactly. T is bool, any integer type, a Boost.Decimal type, the traits’ number type,
std::string, std::string_view, std::u16string, std::u16string_view, wd::date_ms,
std::vector<std::byte> or std::span<const std::byte> (Strings, dates and bytes).
value_or(fallback) is JavaScript’s ??: the value get<T>() gives, T being the fallback’s type, or
the fallback when nothing is there, when the value is null, or when it cannot be read as T.
contains(key) is key in v for an object, size() an object’s key count or an array’s length,
and length() an array’s length or a string’s in UTF-16 code units, as JavaScript counts them:
say("typeof user.name === 'string'", user["name"].is_string());
say("'tags' in user", user.contains("tags"));
if (const wd::result<std::uint64_t> length = user["name"].length()) {
say("user.name.length", *length);
}
say("doc.n ?? 0", doc["n"].value_or(std::int64_t{0}));
say("user.nick ?? 'none'", user["nick"].value_or(std::string_view("none")));
say("Number.isInteger(doc.n)", doc["n"].is_integer());
typeof user.name === 'string' -> true 'tags' in user -> true user.name.length -> 3 doc.n ?? 0 -> 3 user.nick ?? 'none' -> none Number.isInteger(doc.n) -> true
A std::string_view that get returns, and a key that keys() gives, are views into the
document’s image: each is valid until the document’s next change, which may move the image.
Writing
set(x) is assignment. It takes bool, any integer type, the Boost.Decimal types, the traits’
number type, text (std::string_view, const char*, std::string, std::u16string_view), bytes
(std::span<const std::byte>), a wd::date_ms, nullptr or wd::null, the literals
wd::object and wd::array, which are JSON written in C++, a handle or a document of the same
traits, which it copies, and wd::undefined. A character is not a number: set('a') does not
compile, nor does set of a float, nor of a double unless the traits’ number type is one.
erase(key) is delete, and returns whether the key was there. set(wd::undefined) erases a
member too: JavaScript keeps a member whose value is undefined, and writes nothing for it, while a
document never stores undefined. In an array, it stores null, the text JavaScript writes for
it; at the root, the document holds undefined, and writes nothing. assign_from(source) is
Object.assign: each member of the source is set here, in its order, as one operation that
completes or changes nothing; an array’s elements are its members "0" to "n-1", and null or
undefined add nothing.
// user.nick = "Countess"; user.age = 36; user.tags.push("poetry"); user.home = {...};
if (!user["nick"].set("Countess") || !user["age"].set(36) || !user["tags"].push("poetry") ||
!user["home"].set(wd::object{{"city", "London"}, {"rooms", wd::array{1, 2}}})) {
return false;
}
show("user", user);
// delete user.age; user.nick = undefined, which erases it; Object.assign(user, {...}).
const wd::result<bool> erased = user.erase("age");
if (!erased || !user["nick"].set(wd::undefined)) {
return false;
}
say("delete user.age", *erased);
wd::result<wd::document> defaults = wd::parse(R"({"lang":"en","name":"Lovelace"})");
if (defaults.has_error() || !user.assign_from(defaults->root())) {
return false;
}
show("user", user);
user -> {"name":"Ada","tags":["math","poetry"],"nick":"Countess","age":36,"home":{"city":"London","rooms":[1,2]}}
delete user.age -> true
user -> {"name":"Lovelace","tags":["math","poetry"],"home":{"city":"London","rooms":[1,2]},"lang":"en"}
Every writing function returns a result and is [[nodiscard]]. It completes, or it changes
nothing: it finds the room it needs in the image before it allocates (Images), so a
full image, a value of the wrong kind or a number out of range leave the document as it was.
Copies, never aliases
JavaScript’s doc.copy = doc.user makes two names of one object, so that a change through one is
seen through the other. A document is a tree, as JSON is, so set of a handle copies the subtree:
// doc.copy = doc.user shares one object in JavaScript; here it is a copy of the subtree.
if (!doc["copy"].set(user) || !doc["copy"]["name"].set("Byron")) {
return false;
}
show("user.name", user["name"]);
show("copy.name", doc["copy"]["name"]);
user.name -> "Lovelace" copy.name -> "Byron"
A copy reads its source before it changes anything, so a value may be copied into its own
subtree. Between documents, copies need the same traits, and any storage. doc.clone() copies a
whole document into a new owned one, as structuredClone would.
A wd::value is not a regular value: with exceptions, x = y writes where x points, and
never makes it point elsewhere (The sugar that throws). It must not be swapped or sorted:
std::swap(x, y) assigns through both handles, leaving both places holding the value of y, and
std::sort over a range of handles writes the document’s values the same way. Read the values
out, sort them, and set them back.
Iteration and equality
entries(), keys() and values() are Object.entries, Object.keys and Object.values, in
the order of the traits’ object policy (Key order), and a for over an array’s handle
visits its elements. A key is a std::string_view with UTF-8 strings, and a
std::u16string_view with UTF-16 ones (Traits):
// for (const [key, value] of Object.entries(user))
for (auto [key, value] : user.entries()) {
std::cout << key << ": " << value.stringify() << '\n';
}
// for (const tag of user.tags)
for (const wd::const_value tag : user["tags"]) {
std::cout << "tag " << tag.stringify() << '\n';
}
name: "Lovelace"
tags: ["math","poetry"]
home: {"city":"London","rooms":[1,2]}
lang: "en"
tag "math"
tag "poetry"
A range reads the document as it is when each step is taken, so changing the container it walks is undefined, as it is for a standard container.
== compares two values structurally, which JavaScript does not: the same kind; numbers by value,
so 1, 1.0 and 1e0 are equal; strings, dates and bytes by value; arrays element by element;
objects with the same keys, each holding an equal value, in any order. Two documents compare
their roots.
Numbers
JavaScript’s numbers are doubles: JSON.parse rounds every number to the nearest one, so a peer
that sends 34 exact digits receives 17. With its default traits, document stores every number
exactly as it was sent, and writes it back as JavaScript would write that value; with js_traits,
it holds JavaScript’s own doubles, and agrees with JavaScript on every digit.
Stored by value
A number is stored in one of two forms, chosen by its value, never by its spelling: an integer that
fits the traits’ integer, std::int64_t, is one; any other number is the traits’ number,
Boost.Decimal’s decimal128_t by default, normalised, without trailing zeros. So 1, 1.0 and
1e0 are one stored value, as they are one number in JavaScript, and 18446744073709551615, past
std::int64_t, is an exact number. Parsing reads a number from its text, never through a double:
more than 34 significant digits, or an exponent past the range of decimal128_t, is errc::range.
Writing follows JavaScript’s Number::toString on the stored digits, with k significant digits
and the decimal point after position n:
| When | Written as | Example |
|---|---|---|
|
the digits, then |
|
|
the digits, with a point after |
|
|
|
|
otherwise |
one digit, a point and the others if any, |
|
So every number JSON.stringify writes, the shortest text of a double, is written back byte for
byte as JavaScript writes it, and a number a double cannot hold stays exact. The program parses one text
with each traits:
constexpr std::string_view text =
"[0.1000000000000000000000000000000001,18446744073709551615,4611686018427387904,"
"1e21,1.50,1E-7,1.0,-0]";
parsed("default_traits", wd::parse(text));
parsed("js_traits ", wd::parse<wd::js_traits>(text));
default_traits: [0.1000000000000000000000000000000001,18446744073709551615,4611686018427387904,1e+21,1.5,1e-7,1,0] js_traits : [0.1,18446744073709552000,4611686018427388000,1e+21,1.5,1e-7,1,0]
With js_traits, the numbers are what Node holds, written as Node writes them: the 34 digits
round to 0.1, and 262, 4611686018427387904, is written 4611686018427388000. A number under
js_traits is first rounded to a double, then stored as an integer when that double is integral
and fits std::int64_t, so it writes back as Node writes the double.
What each refuses
The default traits refuse a number they cannot store exactly: more than 34 significant digits, or
an exponent past the range of decimal128_t, beyond about 1e6144. Within it they store every
number exactly, 1e999 included, which a JavaScript peer reads as Infinity. js_traits refuse
no number: they round as JSON.parse does, and a number past a double’s range is Infinity, as
JSON.parse makes it, which stringify writes null, as JSON.stringify does:
constexpr std::string_view forty_digits = "1234567890123456789012345678901234567890";
parsed("40 digits, default_traits", wd::parse(forty_digits));
parsed("40 digits, js_traits ", wd::parse<wd::js_traits>(forty_digits));
parsed("1e999, default_traits ", wd::parse("1e999"));
parsed("1e999, js_traits ", wd::parse<wd::js_traits>("1e999"));
40 digits, default_traits: a number or index out of range 40 digits, js_traits : 1.2345678901234568e+39 1e999, default_traits : 1e+999 1e999, js_traits : null
set refuses a NaN or an infinity of a Boost.Decimal type with errc::range. A double’s NaN and
infinities, which arithmetic makes, and a parse past a double’s range, are stored under
js_traits, and written null, as JavaScript writes them.
Reading a number
get<T>() gives a number as any integer type, a Boost.Decimal type, or the traits’ number type,
when T holds it exactly, and errc::range otherwise: a number that is not an integer, or does
not fit, is never truncated or rounded into T. is_integer() is Number.isInteger, true for a
number such as 1e30 too:
wd::result<wd::document> made =
wd::parse(R"({"small":300,"big":18446744073709551615,"half":0.5,"huge":1e30})");
if (made.has_error()) {
return 1;
}
const wd::document& doc = *made;
read("small as int64_t", doc["small"].get<std::int64_t>());
read("small as int8_t", doc["small"].get<std::int8_t>());
read("big as uint64_t", doc["big"].get<std::uint64_t>());
read("big as int64_t", doc["big"].get<std::int64_t>());
read("half as int64_t", doc["half"].get<std::int64_t>());
std::cout << "Number.isInteger(huge): " << std::boolalpha << doc["huge"].is_integer() << '\n';
small as int64_t: 300 small as int8_t: a number or index out of range big as uint64_t: 18446744073709551615 big as int64_t: a number or index out of range half as int64_t: a number or index out of range Number.isInteger(huge): true
A double is read or set only when the traits’ number type is one: the default traits have no
conversion from or to double, nor to float ever, so a program cannot lose digits by accident.
Arithmetic
A document stores numbers; it computes nothing. A program reads a value in the traits’ own number type, computes, and stores the result, so the arithmetic is the number type’s: exact in decimal, binary in a double.
// 0.1 + 0.2, computed by the program in the traits' own number type, then stored.
wd::result<wd::document> exact = wd::parse(R"({"a":0.1,"b":0.2})");
wd::result<wd::basic_document<wd::js_traits>> js =
wd::parse<wd::js_traits>(R"({"a":0.1,"b":0.2})");
if (exact.has_error() || js.has_error()) {
return 1;
}
using decimal = boost::decimal::decimal128_t;
const wd::result<decimal> a = (*exact)["a"].get<decimal>();
const wd::result<decimal> b = (*exact)["b"].get<decimal>();
const wd::result<double> x = (*js)["a"].get<double>();
const wd::result<double> y = (*js)["b"].get<double>();
if (!a || !b || !x || !y || !(*exact)["sum"].set(*a + *b) || !(*js)["sum"].set(*x + *y)) {
return 1;
}
std::cout << exact->stringify() << '\n' << js->stringify() << '\n';
{"a":0.1,"b":0.2,"sum":0.3}
{"a":0.1,"b":0.2,"sum":0.30000000000000004}
Strings, dates and bytes
JSON has strings; JavaScript programs also send dates and bytes through it, as text. A document
stores strings in the encoding its traits choose, and holds dates and bytes as values of their own,
which it writes as JSON.stringify writes a Date and, by its traits, a Uint8Array.
Strings
The traits’ string is the stored encoding of strings and keys: wd::utf8, the default, or
wd::utf16, the code units JavaScript holds, which js_traits choose. Either way, length()
counts UTF-16 code units, as JavaScript does: UTF-8 is decoded to count them. Text is validated
where it enters: the parser refuses invalid UTF-8, and set refuses text invalid in its encoding
with errc::type.
A view reads a string as it is stored, and cannot transcode: get<std::string_view>() reads
UTF-8 strings only, and get<std::u16string_view>() UTF-16 ones; get<std::string>() and
get<std::u16string>() transcode from either. Keys come out as the encoding’s view. The sugar’s
as_string() reads UTF-8 strings only: under js_traits, a program uses as<std::u16string_view>()
or as<std::string>() (The sugar that throws).
constexpr std::string_view text = R"(["é😀","tab\there \"q\" \u0001"])";
wd::result<wd::document> utf8 = wd::parse(text);
wd::result<wd::basic_document<wd::js_traits>> utf16 = wd::parse<wd::js_traits>(text);
if (utf8.has_error() || utf16.has_error()) {
return false;
}
// "é😀".length is 3 in UTF-16 code units, whatever the stored encoding.
read("UTF-8 length ", (*utf8)[0].length());
read("UTF-16 length", (*utf16)[0].length());
read("UTF-8 as std::string_view", (*utf8)[0].get<std::string_view>());
read("UTF-8 as std::u16string_view", (*utf8)[0].get<std::u16string_view>());
read("UTF-16 as std::u16string_view", (*utf16)[0].get<std::u16string_view>());
read("UTF-16 as std::string", (*utf16)[0].get<std::string>());
std::cout << "both write: " << utf8->stringify() << ' ' << utf16->stringify() << '\n';
// A lone surrogate: kept by UTF-16 and written as JSON.stringify writes it; refused by UTF-8.
constexpr std::array<char16_t, 1> lone{0xD800};
const std::u16string_view surrogate(lone.data(), lone.size());
print("UTF-16 set", (*utf16)[2].set(surrogate));
print("UTF-8 set ", (*utf8)[2].set(surrogate));
std::cout << "UTF-16 writes: " << (*utf16)[2].stringify() << '\n';
UTF-8 length : 3 UTF-16 length: 3 UTF-8 as std::string_view: é😀 UTF-8 as std::u16string_view: a value of another kind UTF-16 as std::u16string_view: 3 code units UTF-16 as std::string: é😀 both write: ["é😀","tab\there \"q\" \u0001"] ["é😀","tab\there \"q\" \u0001"] UTF-16 set: ok UTF-8 set : a value of another kind UTF-16 writes: "\ud800"
Both encodings write the same text, escaped as JSON.stringify escapes it: a quotation mark and a backslash
with a backslash; \b, \f, \n, \r and \t; any other character below U+0020 as \u and four
lowercase hexadecimal digits; nothing else, so /, text beyond ASCII and U+2028 and U+2029 are
written as they are.
A lone surrogate is a UTF-16 code unit of a pair whose other half is missing, which JavaScript’s
strings may hold. UTF-8 cannot encode one, so set refuses it with wd::utf8; wd::utf16 keeps
it, and writes it as \u and its four digits, as JSON.stringify has since ES2019. The parser
refuses one with either encoding (Lone surrogates).
Dates
A date is a wd::date_ms, milliseconds since the epoch, 1970-01-01T00:00:00Z, as a JavaScript
Date holds it, and its range is JavaScript’s, 8.64e15 ms on either side: set past it is
errc::range. It is written as Date.prototype.toJSON writes it, "2026-10-12T03:04:05.678Z",
with a year outside 0 to 9999 written as six digits and a sign, "+275760-09-13T00:00:00.000Z".
JSON has no dates, so a parse keeps every string a string, as JSON.parse does, unless
parse_options::revive_dates, which turns a string in exactly that format into a date.
get<wd::date_ms>() reads such a string as a date too, revived or not:
constexpr std::string_view text = R"({"at":"2026-10-12T03:04:05.678Z","day":"2026-10-12"})";
wd::parse_options revive;
revive.revive_dates = true;
wd::result<wd::document> plain = wd::parse(text);
wd::result<wd::document> revived = wd::parse(text, revive);
if (plain.has_error() || revived.has_error()) {
return false;
}
std::cout << "is_date: " << std::boolalpha << (*plain)["at"].is_date() << ' '
<< (*revived)["at"].is_date() << ' ' << (*revived)["day"].is_date() << '\n';
// A string in Date.prototype.toJSON's format reads as a date, revived or not.
const wd::result<wd::date_ms> at = (*plain)["at"].get<wd::date_ms>();
std::cout << "ms since the epoch: " << (at ? at->time_since_epoch().count() : 0) << '\n';
const wd::date_ms last{std::chrono::milliseconds{8'640'000'000'000'000}};
print("set the last date", (*plain)["last"].set(last));
print("set one ms later ", (*plain)["later"].set(last + std::chrono::milliseconds{1}));
print("written", plain);
is_date: false true false
ms since the epoch: 1791774245678
set the last date: ok
set one ms later : a number or index out of range
written: {"at":"2026-10-12T03:04:05.678Z","day":"2026-10-12","last":"+275760-09-13T00:00:00.000Z"}
Bytes
Bytes are a std::span<const std::byte> that set stores raw in the image, and that an image
carries as they are. stringify writes them as the traits’ bytes policy says:
| Policy | Written as |
|---|---|
|
base64 of RFC 4648, with padding |
|
base64url of RFC 4648, section 5, without padding |
|
what |
parse_options::revive_bytes turns a string valid in the policy’s text back into bytes, and,
with javascript_bytes, an object whose keys are exactly "0" to "n-1" holding integers from 0
to 255. get<std::vector<std::byte>>() reads such a value as bytes too, and
get<std::span<const std::byte>>() reads bytes in place:
const std::array<std::byte, 4> raw{
std::byte{0xde},
std::byte{0xad},
std::byte{0xbe},
std::byte{0xef},
};
const std::span<const std::byte> view(raw);
wd::result<wd::document> base64 = wd::document::create();
wd::result<wd::basic_document<canonical_traits>> base64url =
wd::basic_document<canonical_traits>::create();
wd::result<wd::basic_document<wd::js_traits>> js = wd::basic_document<wd::js_traits>::create();
if (base64.has_error() || base64url.has_error() || js.has_error() ||
!base64->root().set(view) || !base64url->root().set(view) || !js->root().set(view)) {
return false;
}
print("base64 ", base64);
print("base64url ", base64url);
print("javascript_bytes", js);
// revive_bytes turns text of the bytes policy back into bytes.
wd::parse_options revive;
revive.revive_bytes = true;
wd::result<wd::document> revived = wd::parse(R"({"key":"3q2+7w=="})", revive);
if (revived.has_error()) {
return false;
}
const wd::result<std::vector<std::byte>> key = (*revived)["key"].get<std::vector<std::byte>>();
std::cout << "revived: " << std::boolalpha << (*revived)["key"].is_bytes() << ", "
<< (key ? key->size() : 0) << " bytes\n";
base64 : "3q2+7w=="
base64url : "3q2-7w"
javascript_bytes: {"0":222,"1":173,"2":190,"3":239}
revived: true, 4 bytes
The program’s canonical_traits choose base64url (Traits).
Key order
The order of an object’s keys is what makes two JSON.stringify texts equal or not, and
JavaScript’s is not the order of insertion. Its OrdinaryOwnPropertyKeys gives the keys that are
array indices first, ascending, then every other key in the order it was created. Measured in Node
26, JSON.parse('{"b":1,"2":2,"a":3,"1":4}') holds {"1":4,"2":2,"b":1,"a":3}, and a document
writes the same.
Array indices
An array index is a key that is the canonical decimal spelling of an integer from 0 to
4,294,967,294: "0", "7" and "10" are, and "01", "-1", "1.0", " 1" and
"4294967295" are not, so they keep their place among the other keys. Assigning an existing key
keeps its place. Erasing a key and creating it again puts it where a new key goes: last among the
other keys, or back among the indices, ascending, for an index.
The policies
The traits’ object chooses the order:
| Policy | The order of an object’s keys |
|---|---|
|
JavaScript’s: array indices ascending, then the other keys in the order they were created |
|
the order each key was created, the text’s order for a parsed object, whatever the key |
|
by the keys’ UTF-16 code units, with either encoding, as JavaScript’s |
A repeated key in parsed text is accepted as JSON.parse accepts it: the last value, in the first
one’s place. The program parses one text under each policy, the last two through traits of its
own (Traits), then erases and creates again a key and an index:
constexpr std::string_view text = R"({"b":1,"2":2,"a":3,"1":4,"01":5})";
print("javascript_order", wd::parse(text));
print("insertion_order ", wd::parse<insertion_traits>(text));
print("sorted ", wd::parse<canonical_traits>(text));
// An index key erased and created again goes back among the indices; any other key, last.
wd::result<wd::document> made = wd::parse(text);
if (made.has_error() || !made->root().erase("b") || !(*made)["b"].set(6) ||
!made->root().erase("1") || !(*made)["1"].set(7)) {
return false;
}
print("erased, created ", made);
javascript_order: {"1":4,"2":2,"b":1,"a":3,"01":5}
insertion_order : {"b":1,"2":2,"a":3,"1":4,"01":5}
sorted : {"01":5,"1":4,"2":2,"a":3,"b":1}
erased, created : {"1":7,"2":2,"a":3,"01":5,"b":6}
Every operation keeps the order: set, erase, assign_from, a copy and the parser. An object’s
members are kept in ordered indices, never in a hashed one, since keys come from untrusted text:
no input makes a lookup slower than its logarithm.
Arrays
A handle of an array has JavaScript’s array methods, under their JavaScript names, with
JavaScript’s index rules. Each is a core function: it returns a result, completes or changes
nothing, and fails with errc::type, JavaScript’s TypeError, on anything but an array.
| JavaScript | document | Returns |
|---|---|---|
|
|
|
|
|
|
|
|
the same, from the front |
|
|
|
|
|
|
|
|
the same, removing every element from |
|
|
|
|
|
|
|
|
|
|
|
a handle, which names nothing out of range |
|
|
|
|
|
|
The values a method takes are what set takes (Writing), each copied into the
array; a method that returns removed or copied elements returns them in a new owned document, so
that they outlive the array. The program logs each call, what it returned and the array after it:
/** The text of a value, `undefined` when nothing is there. */
std::string text(const wd::const_value& v) {
return v.is_undefined() ? "undefined" : v.stringify();
}
/** Prints a call, what it returned and the array after it. */
void log(std::string_view call, std::string_view returned, const wd::const_value& a) {
std::cout << call << " -> " << returned << ", a = " << text(a) << '\n';
}
/** Prints a call that returns a document: the removed values, or a new array. */
void log(std::string_view call, const wd::result<wd::document>& r, const wd::const_value& a) {
log(call, r.has_value() ? text(r->root()) : r.error().message(), a);
}
/** Prints a call that returns a new length. */
void log(std::string_view call, const wd::result<std::uint64_t>& r, const wd::const_value& a) {
log(call, r.has_value() ? std::to_string(*r) : r.error().message(), a);
}
/** Prints a call that returns nothing. */
void log(std::string_view call, const wd::result<void>& r, const wd::const_value& a) {
log(call, r.has_value() ? "undefined" : r.error().message(), a);
}
wd::value a = made->root();
log("a.push(6, 7)", a.push(6, 7), a);
log("a.pop()", a.pop(), a);
log("a.shift()", a.shift(), a);
log("a.unshift(0)", a.unshift(0), a);
log("a.splice(1, 2, 'x', 'y', 'z')", a.splice(1, 2, "x", "y", "z"), a);
log("a.reverse()", a.reverse(), a);
log("a.slice(-2)", a.slice(-2), a);
log("a.concat([8, 9], 10)", a.concat(wd::array{8, 9}, 10), a);
log("a.at(-1)", text(a.at(-1)), a);
a.push(6, 7) -> 7, a = [1,2,3,4,5,6,7] a.pop() -> 7, a = [1,2,3,4,5,6] a.shift() -> 1, a = [2,3,4,5,6] a.unshift(0) -> 6, a = [0,2,3,4,5,6] a.splice(1, 2, 'x', 'y', 'z') -> [2,3], a = [0,"x","y","z",4,5,6] a.reverse() -> undefined, a = [6,5,4,"z","y","x",0] a.slice(-2) -> ["x",0], a = [6,5,4,"z","y","x",0] a.concat([8, 9], 10) -> [6,5,4,"z","y","x",0,8,9,10], a = [6,5,4,"z","y","x",0] a.at(-1) -> 0, a = [6,5,4,"z","y","x",0]
Indices
Indices follow JavaScript exactly. A negative start counts from the end and is clamped at 0, and
a start past the length is the length; an omitted count removes to the end, and a negative one
is 0. The end of slice follows the same rules as start, and at counts a negative index from the
end:
if (!a.set(wd::array{1, 2, 3, 4, 5})) {
return 1;
}
log("a.splice(-2, 1)", a.splice(-2, 1), a);
log("a.splice(10, 0, 'end')", a.splice(10, 0, "end"), a);
log("a.splice(1, -5)", a.splice(1, -5), a);
log("a.splice(3)", a.splice(3), a);
a.splice(-2, 1) -> [4], a = [1,2,3,5] a.splice(10, 0, 'end') -> [], a = [1,2,3,5,"end"] a.splice(1, -5) -> [], a = [1,2,3,5,"end"] a.splice(3) -> [5,"end"], a = [1,2,3]
Dense arrays
JavaScript’s arrays may have holes, elements that were never set; JSON has none, and writes a hole
as null. A document’s arrays are dense, the traits’ only array policy, wd::dense: a longer
length, or a write past the end, fills the elements between with null, which writes the same
text. So pop or shift of an element that was a hole returns null, where JavaScript returns
undefined:
log("a.length = 1", a.set_length(1), a);
log("a.length = 3", a.set_length(3), a);
log("a[5] = 6", a[5].set(6), a);
log("a.pop()", a.pop(), a);
log("a.pop()", a.pop(), a); // JavaScript: undefined, the element of a hole
a.length = 1 -> undefined, a = [1] a.length = 3 -> undefined, a = [1,null,null] a[5] = 6 -> undefined, a = [1,null,null,null,null,6] a.pop() -> 6, a = [1,null,null,null,null] a.pop() -> null, a = [1,null,null,null]
Copies and empty arrays
pop and shift of an empty array return a document that holds undefined, as JavaScript
returns undefined. And since a document is a tree, a.splice(0, 1, a) inserts a copy of the array
as it was before the call, where JavaScript inserts the array itself, a cycle; the copy is a value
of its own:
if (!a.set(wd::array{})) {
return 1;
}
log("a.pop()", a.pop(), a); // a document that holds undefined
// JavaScript inserts the array itself, a cycle; a document inserts a copy of it.
if (!a.set(wd::array{1, 2})) {
return 1;
}
log("a.splice(0, 1, a)", a.splice(0, 1, a), a);
log("a[0].push({})", a[0].push(wd::object{}), a); // the copy, not a
a.pop() -> undefined, a = []
a.splice(0, 1, a) -> [1], a = [[1,2],2]
a[0].push({}) -> 3, a = [[1,2,{}],2]
A method that inserts many values, push, unshift, splice or concat, finds the room for all
of them first: in a full external buffer, it returns errc::capacity and the array is as it was
(Images).
Containers
The handles of Handles read and write a document as JavaScript does. A C++ program
that walks a document also wants the interface its containers have, and document gives it:
as_object() makes of an object a view that is a sorted map, as_array() makes of an array a view
that is a vector, and both are ranges that the standard algorithms and views read. They are views of
the document and own nothing, wd::object_ref and wd::array_ref, and every operation that can fail
returns its result as the rest of the library does. The program of this section is
example/document_containers.cpp, and prints the same lines on every target.
Its helpers print the message of a failed result and the document after an operation:
/** Prints the message of a failed result, and yields the exit code of main. */
template <class Result>
int fail(const Result& r) {
std::cout << "error: " << r.error().message() << '\n';
return 1;
}
/** Prints an operation and the document after it, as JSON. */
void step(std::string_view what, const wd::document& doc) {
std::cout << what << " -> " << doc.stringify() << '\n';
}
An object is a sorted map
insert and try_emplace answer the member and whether it was inserted, and a key that is there
keeps its value; insert_or_assign assigns to it, as JavaScript’s o[key] = v does. find answers
the member’s iterator, or end(), contains whether the key is there, and erase how many members
it removed. Iteration follows JavaScript’s order, so a new key comes last, and so does a key that is
erased and inserted again (Key order):
// An object is a sorted map: insert and try_emplace answer the member and whether it was
// inserted, and insert_or_assign assigns to it as JavaScript's o[key] = v does.
wd::result<wd::document> made = wd::parse("{}");
if (made.has_error()) {
return fail(made);
}
wd::document& doc = *made;
wd::result<wd::object_ref> as_object = doc.root().as_object();
if (as_object.has_error()) {
return fail(as_object);
}
const wd::object_ref obj = *as_object;
const auto name = obj.insert("name", "Ada");
if (name.has_error()) {
return fail(name);
}
std::cout << "insert name: " << name->second << '\n';
// A key that is there keeps its value.
const auto again = obj.insert("name", "Grace");
if (again.has_error()) {
return fail(again);
}
std::cout << "insert name again: " << again->second
<< ", name = " << doc["name"].value_or(std::string_view("nobody")) << '\n';
const auto assigned = obj.insert_or_assign("name", "Augusta Ada");
if (assigned.has_error()) {
return fail(assigned);
}
std::cout << "insert_or_assign name: " << assigned->second
<< ", name = " << doc["name"].value_or(std::string_view("nobody")) << '\n';
const auto born = obj.try_emplace("born", 1815);
if (born.has_error()) {
return fail(born);
}
std::cout << "try_emplace born: " << born->second << '\n';
// find answers the member's iterator, or the end; contains answers whether the key is there.
const wd::object_ref::iterator found = obj.find("born");
std::cout << "find born: " << found->second.value_or(std::int64_t{0}) << '\n';
const wd::object_ref::iterator missing = obj.find("nope");
std::cout << "contains born: " << obj.contains("born")
<< ", find nope is end: " << (missing == obj.end()) << '\n';
const auto erased = obj.erase("born");
if (erased.has_error()) {
return fail(erased);
}
std::cout << "erase born: " << *erased << ", contains born: " << obj.contains("born") << '\n';
// A new key comes last, and so does a key erased and inserted again: the members iterate in
// JavaScript's order, which is the order they were added in.
const auto langs = obj.insert("langs", wd::array{"Analytical", "Note G"});
if (langs.has_error()) {
return fail(langs);
}
const auto known = obj.insert("known", true);
if (known.has_error()) {
return fail(known);
}
const auto born_again = obj.insert("born", 1815);
if (born_again.has_error()) {
return fail(born_again);
}
for (const auto& [key, handle] : obj) {
std::cout << key << " = " << handle.stringify() << '\n';
}
insert name: true insert name again: false, name = Ada insert_or_assign name: false, name = Augusta Ada try_emplace born: true find born: 1815 contains born: true, find nope is end: true erase born: 1, contains born: false name = "Augusta Ada" langs = ["Analytical","Note G"] known = true born = 1815
An array is a vector
push_back, emplace_back, insert, erase and resize change an array as they change a
std::vector, and at checks the index: an index past the end is an error value and not an
undefined element. erase answers the iterator of the element after the one it removed, and
resize to a larger size fills with null (Dense arrays):
// An array is a vector: push_back, emplace_back, insert, erase and resize change it as
// std::vector does, and at checks the index.
wd::result<wd::document> list_made = wd::parse("[1,2,3]");
if (list_made.has_error()) {
return fail(list_made);
}
wd::document& list_doc = *list_made;
wd::result<wd::array_ref> as_array = list_doc.root().as_array();
if (as_array.has_error()) {
return fail(as_array);
}
const wd::array_ref list = *as_array;
const auto pushed = list.push_back(4);
if (pushed.has_error()) {
return fail(pushed);
}
step("push_back(4)", list_doc);
const auto emplaced = list.emplace_back(5);
if (emplaced.has_error()) {
return fail(emplaced);
}
step("emplace_back(5)", list_doc);
const auto placed = list.insert(std::ranges::next(list.begin(), 1), 9);
if (placed.has_error()) {
return fail(placed);
}
step("insert(begin + 1, 9)", list_doc);
// erase answers the iterator of the element after the one it removed.
const wd::array_ref::iterator after = list.erase(std::ranges::next(list.begin(), 1));
step("erase(begin + 1)", list_doc);
const wd::value next_element = *after;
std::cout << "erase returns the element after: " << next_element.value_or(std::int64_t{0})
<< '\n';
const auto shrunk = list.resize(3);
if (shrunk.has_error()) {
return fail(shrunk);
}
step("resize(3)", list_doc);
std::cout << "front " << list.front().value_or(std::int64_t{0}) << ", back "
<< list.back().value_or(std::int64_t{0}) << '\n';
const wd::result<wd::value> third = list.at(2);
if (third.has_error()) {
return fail(third);
}
std::cout << "at(2) = " << third->value_or(std::int64_t{0}) << '\n';
// An index past the end is an error value, not an undefined element.
const wd::result<wd::value> beyond = list.at(10);
std::cout << "at(10) is errc::range: "
<< (beyond.has_error() && beyond.error() == wd::errc::range) << '\n';
const auto grown = list.resize(5);
if (grown.has_error()) {
return fail(grown);
}
step("resize(5)", list_doc);
push_back(4) -> [1,2,3,4] emplace_back(5) -> [1,2,3,4,5] insert(begin + 1, 9) -> [1,9,2,3,4,5] erase(begin + 1) -> [1,2,3,4,5] erase returns the element after: 2 resize(3) -> [1,2,3] front 1, back 3 at(2) = 3 at(10) is errc::range: true resize(5) -> [1,2,3,null,null]
Ranges
An object and an array are each a std::ranges::bidirectional_range, so the standard algorithms and the
views over them read their members as they read a vector or a map:
// An object and an array are ranges: the standard algorithms and the views over them read
// their members, as they read a vector.
wd::result<wd::document> shop_made = wd::document::create();
if (shop_made.has_error()) {
return fail(shop_made);
}
wd::document& shop = *shop_made;
const auto shop_set = shop.root().set(wd::object{
{"prices", wd::array{3, 8, 12, 5, 20}},
{"stock", wd::object{{"apple", 4}, {"pear", 0}, {"plum", 7}}},
});
if (shop_set.has_error()) {
return fail(shop_set);
}
wd::result<wd::array_ref> as_prices = shop["prices"].as_array();
if (as_prices.has_error()) {
return fail(as_prices);
}
const wd::array_ref prices = *as_prices;
wd::result<wd::object_ref> as_stock = shop["stock"].as_object();
if (as_stock.has_error()) {
return fail(as_stock);
}
const wd::object_ref stock = *as_stock;
const auto above_five = [](const wd::value& v) { return v.value_or(std::int64_t{0}) > 5; };
const auto doubled_value = [](const wd::value& v) { return 2 * v.value_or(std::int64_t{0}); };
std::cout << "prices above 5: " << std::ranges::count_if(prices, above_five) << '\n';
std::cout << "prices above 5, doubled:";
// The view is built and iterated in one statement: a filter view has no const iteration.
for (const std::int64_t twice :
prices | std::views::filter(above_five) | std::views::transform(doubled_value)) {
std::cout << ' ' << twice;
}
std::cout << '\n';
const auto empty = std::ranges::find_if(
stock, [](const auto& member) { return member.second.value_or(std::int64_t{0}) == 0; });
std::cout << "first out of stock: " << empty->first << '\n';
prices above 5: 3 prices above 5, doubled: 16 24 40 first out of stock: pear
What a change leaves valid is more than the standard containers leave: an insertion, and any growth
of the image, keeps every iterator and handle valid and invalidates only the views (std::string_view) read
from a value; an erase invalidates the iterators to the erased member and its subtree. A range reads
the document as it is when each step is taken, so changing the container it walks is undefined, as it
is for a standard container.
Keys in order
An object is kept in JavaScript’s order, and also in key order: by_key() is the same object ordered
by key, by UTF-16 code units whatever the encoding, with find, lower_bound, upper_bound and
equal_range. Each step and each bound is a search of a tree, so a query costs O(log n), and nothing
is sorted: a JavaScript program sorts every key to get any of these answers, and gets it again each
time. A prefix query is a range from the first key at or after the prefix up to the first key at or
after the prefix with its last character raised by one:
// by_key is the same object in key order, by UTF-16 code units, with bounds for ranges of keys.
// A JavaScript program sorts every key to get any of these; here the tree keeps them sorted.
wd::result<wd::document> catalog_made = wd::parse("{}");
if (catalog_made.has_error()) {
return fail(catalog_made);
}
wd::document& catalog = *catalog_made;
wd::result<wd::object_ref> as_catalog = catalog.root().as_object();
if (as_catalog.has_error()) {
return fail(as_catalog);
}
const wd::object_ref catalog_obj = *as_catalog;
const std::array<std::string_view, 13> names{
"user_3", "admin", "user_1", "guest", "user_9", "user", "user_2",
"owner", "user_7", "user_5", "user_4", "user_8", "user_6",
};
for (const std::string_view key : names) {
const auto added = catalog_obj.insert(key, true);
if (added.has_error()) {
return fail(added);
}
}
std::cout << "JavaScript order:";
for (const auto& member : catalog_obj) {
std::cout << ' ' << member.first;
}
std::cout << '\n';
const auto keys = catalog_obj.by_key();
std::cout << "by_key order:";
for (const auto& member : keys) {
std::cout << ' ' << member.first;
}
std::cout << '\n';
// The keys with the prefix "user_" lie from its first key up to the first key at or after
// "user`", the character after '_'.
const std::ranges::subrange users(keys.lower_bound("user_"), keys.lower_bound("user`"));
std::cout << "prefix user_: " << std::ranges::distance(users) << " keys, from "
<< users.begin()->first << " to " << std::ranges::prev(users.end())->first << '\n';
const auto [exact, exact_end] = keys.equal_range("user_5");
std::cout << "equal_range user_5: " << std::ranges::distance(exact, exact_end) << " member, "
<< exact->first << '\n';
std::cout << "lower_bound p: " << keys.lower_bound("p")->first << '\n';
const std::ranges::subrange between(keys.lower_bound("g"), keys.lower_bound("v"));
std::cout << "keys from g up to v: " << std::ranges::distance(between) << '\n';
JavaScript order: user_3 admin user_1 guest user_9 user user_2 owner user_7 user_5 user_4 user_8 user_6 by_key order: admin guest owner user user_1 user_2 user_3 user_4 user_5 user_6 user_7 user_8 user_9 prefix user_: 9 keys, from user_1 to user_9 equal_range user_5: 1 member, user_5 lower_bound p: user keys from g up to v: 12
The module of Using it from JavaScript gives the same query to JavaScript as keysInRange.
Reserving room
A batch of insertions of a known size is made with the image reserved once, as a vector is
reserved: the insertions then move nothing, where without the reservation the same insertions grow
the image and move it. The example of this section measures it with reserve and capacity
(Reserving).
Writing JSON
wd::stringify(v, indent) is JSON.stringify(v, null, indent), and writes its exact bytes: the
keys in the policy’s order (Key order), strings escaped as JSON.stringify escapes them
(Strings, dates and bytes), numbers as JavaScript writes them (Numbers), a date as
Date.prototype.toJSON writes it, and bytes by the traits’ policy. It is a function of a handle
or of a document, and a member of both, doc.stringify(2) and doc["user"].stringify():
// JSON.stringify(doc, null, 2).
std::cout << doc.stringify(2) << '\n';
The indent is JavaScript’s: 0 writes compactly, a larger one is clamped to 10 spaces, and an empty
array or object is written [] or {} at any indent. A value that is undefined, a handle of
nothing or a document that holds undefined, writes no text, as JSON.stringify(undefined)
returns none. stringify returns a std::string, so its only failure is the standard library’s
std::bad_alloc; a program that must not meet even that writes to a sink of its own.
In pieces, to a sink
wd::stringify_to(v, sink, indent, chunk) writes the same bytes as stringify, in pieces of at
most chunk bytes, 4096 unless told otherwise, to a sink: any callable that takes a
std::string_view and returns void or wd::result<void>. A failure the sink returns stops the
writing and is returned; an exception it throws propagates. The document is unchanged either way,
and a writer holds no recursion, so a document nested as deep as memory allows is written:
// The same bytes as stringify, in pieces of at most 8 bytes, to a sink that prints each.
const wd::result<void> written =
wd::stringify_to(*made, [](std::string_view piece) { std::cout << piece << '|'; }, 0, 8);
if (written.has_error()) {
return 1;
}
std::cout << '\n';
// A sink that returns a failure stops the writing, and stringify_to returns that failure.
std::string kept;
const wd::result<void> stopped = wd::stringify_to(
*made,
[&kept](std::string_view piece) -> wd::result<void> {
if (!kept.empty()) {
return wd::errc::capacity;
}
kept = piece;
return {};
},
0, 8);
std::cout << kept << " then " << stopped.error().message() << '\n';
{"name":|"Ada","t|ags":["m|ath"],"n|":1500}|
{"name": then no room left
The first sink writes a bar after each piece of 8 bytes; the second keeps the first piece, then fails, which stops the writing there.
Parsing
wd::parse(text, options) is JSON.parse(text): it reads JSON text into a new owned document and
returns it in a result. It accepts what RFC 8259 and JSON.parse accept, and nothing else: no
comments, no trailing commas, and whitespace of space, tab, line feed and carriage return only. A
key written twice keeps the last value in the first one’s place, as JavaScript does. A
document’s parser never throws: its input is the network’s, and every failure is a value.
wd::parse<Traits> parses into a document of other traits, as wd::parse<wd::js_traits>(text)
does in Numbers; wd::parse_into(buffer, text) parses into a buffer the program owns
(Images); and wd::parse_or_throw(text), with exceptions only, throws as
JSON.parse does (The sugar that throws).
Limits and revivers
wd::parse_options holds the limits of a parse, each of which is errc::capacity when it is
passed, and the revivers:
| Member | What it is |
|---|---|
|
The deepest nesting of arrays and objects, 256 unless told otherwise. |
|
The most bytes of text, every chunk counted; no limit unless told otherwise. |
|
The largest image an owned document grows to, 64 MiB unless told otherwise. |
|
Whether a string in exactly the format of |
|
Whether text of the traits’ bytes policy becomes bytes (Bytes). |
Memory per value
Every value of a document is a node of the image’s tree, and a node takes 192 bytes of the image
whatever it holds: a number, true, null, an empty string, an empty array or object alike. A
string’s bytes, and a key’s, take a block of their own besides: 48 bytes for a string of 8 bytes,
112 for one of 100. So "abcdefgh" takes 240 bytes, and {"k":0}, an object and a member with
its key, 432. These are the image’s sizes on every target, measured, as an image is the same
bytes everywhere (Images).
A short value costs far more in the image than in its text. Measured, 2 MB of text, [0,0,...]
with 1,048,576 zeros, makes an image of 268 MB, 128 times the text, which shrink_to_fit()
brings to 201 MB; and while an owned image grows, its old and new buffers are held at once. So
the default max_document_bytes is 64 MiB, which refuses input from the network early: it is
reached by 700 KB of such text, which a native release build reads for 0.4 seconds with 137 MB
resident before it fails, where a limit of 1 GiB would take 11 MB of text, 9 seconds and 2.2 GB.
A program that parses larger documents raises max_document_bytes to what it can afford, and
sets max_bytes with it: a parse past either fails with errc::capacity. A document the program
builds itself, with create, keeps a limit of 1 GiB (Images).
In chunks
wd::parser is the parser itself, wd::basic_parser<Traits, Storage> for other traits or a
buffer of the program’s: write(chunk) takes the text in chunks of any size, split anywhere,
inside a string, a number, an escape or a character’s bytes, down to one byte at a time, and
finish() ends the text and returns the document. The document a parser builds is the one a
whole parse builds, however the text is split:
wd::parser p;
// The text {"name":"Ada","tags":["math"],"n":1.5e3}, split inside a string and a number.
for (const std::string_view chunk :
{R"({"name":"Ad)", R"(a","tags":["ma)", R"(th"],"n":1.)", R"(5e3})"}) {
if (const wd::result<void> written = p.write(chunk); written.has_error()) {
std::cout << "error: " << written.error().message() << '\n';
return 1;
}
}
wd::result<wd::document> made = p.finish();
if (made.has_error()) {
std::cout << "error: " << made.error().message() << '\n';
return 1;
}
std::cout << made->stringify() << '\n';
{"name":"Ada","tags":["math"],"n":1500}
The parser builds the document node by node as each chunk arrives, so the document may grow and
its image move between two chunks; finish returns it. reset() makes the parser ready for
another text, its selectors kept (Selectors).
Errors and offsets
Malformed text is errc::syntax, JavaScript’s SyntaxError. offset() is the count of bytes
consumed, which after a syntax error is the offset where it was found, the same however the text
was split, and cause() is the code of Boost.JSON’s parser behind it. A number the traits refuse
is errc::range (What each refuses), and a limit passed errc::capacity.
A parser reads one text: anything but whitespace after it is errc::syntax, whose cause() is
Boost.JSON’s extra_data, at the offset where the second text begins, whether it comes in the same
chunk or a later one, so "1 2" and "[1] " then "[2]" are refused, and no selector sees the
second text. A parser that failed stays failed: every later write and finish returns the same
error, until reset. A finished parser takes no more text, which is errc::syntax, until reset.
And max_bytes counts every chunk written, not each one alone:
p.reset();
report("write [1,2", p.write("[1,2"), p);
report("write ,]", p.write(",]"), p); // the error, at the byte where it was found
std::cout << "cause: " << p.cause().message() << '\n';
report("write 3]", p.write("3]"), p); // a failed parser stays failed
p.reset();
report("write [1]", p.write("[1]"), p);
const wd::result<wd::document> one = p.finish();
std::cout << "finish: " << (one.has_value() ? one->stringify() : one.error().message()) << '\n';
report("write [2]", p.write("[2]"), p); // a finished parser takes no more text
wd::parse_options limits;
limits.max_depth = 2;
limits.max_bytes = 8;
wd::parser limited(limits);
report("write [[1],", limited.write("[[1],"), limited);
report("write [2]]", limited.write("[2]]"), limited); // 9 bytes in all
std::cout << "[[[1]]]: " << wd::parse("[[[1]]]", limits).error().message() << '\n';
write [1,2: ok write ,]: malformed JSON text at 5 cause: syntax error write 3]: malformed JSON text at 5 write [1]: ok finish: [1] write [2]: malformed JSON text at 3 write [[1],: ok write [2]]: no room left at 5 [[[1]]]: no room left
Lone surrogates
JSON.parse('"\ud800"') gives a string holding a lone surrogate, half of a UTF-16 pair. document’s
parser refuses it with errc::syntax, with either encoding, utf16 included, which could store
it. That is a limit of Boost.JSON’s parser, which document reads JSON with, and a deliberate
choice: the parser cannot deliver a lone surrogate, but only refuse it, or, with
allow_invalid_utf16, replace it with U+FFFD, which would change the text without a word. So it
is documented, not worked around. A program that must hold one, with wd::utf16, stores it with
set of a std::u16string_view, and stringify writes it as JSON.stringify does
(Strings).
Streams of documents
A stream of documents is a stream of bytes that holds JSON texts one after another: a socket that
a server writes NDJSON to, a log file of JSON lines, the output of a program that prints indented
documents. wd::stream_parser, wd::basic_stream_parser<Traits> for other traits, in
<webcpp/document/stream.hpp>, takes the bytes in chunks of any size, split anywhere, down to one
byte at a time, and delivers each document as soon as the bytes that end it are consumed. It reads
one document at a time: at a document’s end it delivers it and begins the next on the bytes that
follow, in the same chunk, so a stream of any length needs the memory of the documents not yet
taken and of the one being read.
Each document is parsed as wd::parse parses one text (Parsing), with the same key
order, numbers and revivers, into a new owned document, and is held to the same limits on its own:
max_depth, max_document_bytes, and max_bytes, which counts the bytes of one document from its
first, not those of the stream (Limits and revivers). wd::stream_options holds those
options, and two of its own: the framing, which says how the documents are told apart, and the
policy on errors.
Framings
wd::framing |
The stream |
|---|---|
|
Documents one after another, with any JSON whitespace between them, or none: indented JSON,
|
|
NDJSON, also called JSON Lines: one document per line. |
|
JSON text sequences, RFC 7464: each document in a record that the byte 0x1E opens. |
NDJSON
A line of NDJSON holds one document, and a document ends at its line’s \n. JSON never holds a raw
\n inside a string, where it is always the escape \n, so a raw \n in the stream always ends a
line. With framing::lines:
-
a document ends at its line’s
\n, or at\r\n: the\r, and any other JSON whitespace before or after the document on its line, is ignored; -
an empty line, or one of whitespace only, is ignored;
-
the last line may end without its
\n:finish()closes it; -
two documents on one line are an error for that line, and so is a document not complete at its
\n. Indented JSON is therefore not NDJSON:{alone on a line is an error, and so is each line after it, until a line holds a whole document again.
A document of a line is delivered at the line’s \n, once the line is known to hold it alone.
JSON text sequences
With framing::rfc7464, a record starts at 0x1E, the ASCII record separator, and ends before the
next 0x1E or at the stream’s end. Its document may span lines, indented, and is delivered as soon
as its value closes; a top-level number closes at the \n that RFC 7464 puts after each text, at
the next 0x1E, or at finish(). A record holds nothing but whitespace after its document; an empty
record is ignored. Whitespace before the first record is ignored, and any other byte before it is
an error.
Errors
A bad document is errc::syntax for malformed text, errc::range for a number the traits refuse,
and errc::capacity past a limit, as in a parse (Errors and offsets). What follows it is
the policy’s, wd::on_error:
wd::on_error |
What a bad document does |
|---|---|
|
It ends the stream: the documents before it are delivered, then every call returns its error,
until |
|
It is dropped, and its error is delivered in its place, in the stream’s order; the stream goes on
at the next line with |
A parser asked to skip with framing::concatenated is refused: every call of it returns
boost::system::errc::invalid_argument. After an error is delivered, error_line() and
error_offset() say where it was found, the line counted from 1 and the offset in bytes from the
stream’s start, and error_cause() gives the code of Boost.JSON’s parser behind it, which is
extra_data for a second document on a line or in a record.
finish() ends the stream: it closes a top-level number or a last line without its \n, and a
document the end cuts short is errc::syntax. After it the parser takes no more bytes, which is
errc::syntax, until reset(); an empty write and another finish() complete nothing.
Without I/O
The parser is sans-IO: it reads no socket and no file, so it serves any source, a socket read by hand, a file read in blocks, an actor’s messages, or JavaScript that hands chunks to a C++ function compiled with Emscripten. Its documents come out in one of two ways:
-
write(bytes)queues each document it completes, andnext()takes the oldest: aresult<std::optional<document>>that holds the document, nothing when none is complete, or an error, delivered as the policy says; -
write(bytes, f)andfinish(f), the push form, hand each document tofas it completes, after any thatnext()has not taken:ftakes aresult<document>, the document or, withon_error::skip, the error of one dropped, and returnsvoidorresult<void>, whose failure ends the stream with that error.
write and finish return how many documents, and dropped documents’ errors, they completed.
line() is the line of the last byte consumed, offset() the bytes consumed, and documents()
the documents delivered so far. The core never throws, as the rest of document: an exception that
f throws propagates, with the bytes up to the document it was given consumed.
By hand
The program below reads each stream from a string, with no I/O at all. Its helpers print the error a call returned with the line it was found on, take every document the parser holds, and write the text in pieces of one, five, two and seven bytes in turn, as a socket might deliver it:
/** Prints the error a write, a finish or a take returned, with the line it was found on. */
void report_error(const wd::stream_parser& p, const boost::system::error_code& error) {
std::cout << "error at line " << p.error_line() << ": " << error.message() << '\n';
}
/**
Takes every document the parser holds, printing each as stringify writes it. Under
on_error::skip a dropped document's error is printed and taking goes on; under on_error::stop the
error comes back on every take, so taking ends at it.
*/
void take_ready(wd::stream_parser& p, wd::on_error policy) {
for (;;) {
wd::result<std::optional<wd::document>> taken = p.next();
if (taken.has_error()) {
report_error(p, taken.error());
if (policy == wd::on_error::stop) {
return;
}
continue;
}
const std::optional<wd::document>& held = *taken;
if (!held.has_value()) {
return;
}
std::cout << held->stringify() << '\n';
}
}
/**
Writes `text` to the parser in pieces of 1, 5, 2 and 7 bytes in turn, as a socket might deliver
it, and takes the documents each piece completes. Returns false when the stream stopped.
*/
bool write_in_pieces(wd::stream_parser& p, std::string_view text, wd::on_error policy) {
constexpr std::array<std::size_t, 4> sizes{1, 5, 2, 7};
for (std::size_t turn = 0; !text.empty(); ++turn) {
const std::string_view piece = text.substr(0, sizes[turn % sizes.size()]);
text.remove_prefix(piece.size());
if (const wd::result<std::size_t> written = p.write(piece); written.has_error()) {
report_error(p, written.error());
return false;
}
take_ready(p, policy);
}
return true;
}
NDJSON ends each document at its line’s newline, so a line may end in CRLF, a line may be empty, and
the last line may lack its newline, which finish() closes. The documents are taken with next():
/**
NDJSON: each document ends at its line's newline, so a line may end in CRLF, a line may be empty,
and the last line may lack its newline, which finish closes.
*/
bool read_ndjson() {
std::cout << "NDJSON, taken with next():\n";
wd::stream_options options;
options.framing = wd::framing::lines;
wd::stream_parser p(options);
constexpr std::string_view text =
"{\"id\":1,\"tags\":[\"a\",\"b\"]}\n"
"{\"id\":2,\"note\":\"first\\nsecond\"}\r\n"
"\n"
"[3,4.5]\n"
"null";
if (!write_in_pieces(p, text, options.on_error)) {
return false;
}
if (const wd::result<std::size_t> finished = p.finish(); finished.has_error()) {
report_error(p, finished.error());
return false;
}
take_ready(p, options.on_error);
return true;
}
NDJSON, taken with next():
{"id":1,"tags":["a","b"]}
{"id":2,"note":"first\nsecond"}
[3,4.5]
null
Concatenated JSON, with no separator or with indentation between the documents, is pushed to a callback, which sees each document as soon as its last byte is consumed:
/**
Concatenated JSON, with no separator or with indentation between documents, pushed to a callback:
each document is handed over as soon as its last byte is consumed, so a callback sees it before
the rest of the piece is read.
*/
bool push_concatenated() {
std::cout << "concatenated, pushed to a callback:\n";
wd::stream_parser p;
constexpr std::string_view text =
"{\"a\":1} {\"b\":2}\n"
"[1][2]\n"
"{\n"
" \"c\": [\n"
" 3\n"
" ]\n"
"}";
const auto print = [](wd::result<wd::document> made) {
std::cout << (made.has_value() ? made->stringify() : made.error().message()) << '\n';
};
for (std::size_t at = 0; at < text.size(); at += 3) {
if (const wd::result<std::size_t> written = p.write(text.substr(at, 3), print);
written.has_error()) {
report_error(p, written.error());
return false;
}
}
if (const wd::result<std::size_t> finished = p.finish(print); finished.has_error()) {
report_error(p, finished.error());
return false;
}
std::cout << p.documents() << " documents\n";
return true;
}
concatenated, pushed to a callback:
{"a":1}
{"b":2}
[1]
[2]
{"c":[3]}
5 documents
With on_error::skip, a bad line is reported with its line number and the stream goes on at the next
line:
/**
NDJSON with a bad line under on_error::skip: the bad line is reported with its line number, and the
stream goes on from the next line.
*/
bool skip_bad_line() {
std::cout << "NDJSON with a bad line, skipped:\n";
wd::stream_options options;
options.framing = wd::framing::lines;
options.on_error = wd::on_error::skip;
wd::stream_parser p(options);
constexpr std::string_view text = "{\"ok\":1}\n{\"bad\":}\n[2]\n\"three\"";
if (!write_in_pieces(p, text, options.on_error)) {
return false;
}
if (const wd::result<std::size_t> finished = p.finish(); finished.has_error()) {
report_error(p, finished.error());
return false;
}
take_ready(p, options.on_error);
return true;
}
NDJSON with a bad line, skipped:
{"ok":1}
error at line 2: malformed JSON text
[2]
"three"
And a stream of JSON text sequences, whose records open with the byte 0x1E and may span lines:
/**
JSON text sequences, RFC 7464: each record opens with the byte 0x1E, and its document may span
several lines.
*/
bool read_rfc7464() {
std::cout << "rfc7464 records:\n";
wd::stream_options options;
options.framing = wd::framing::rfc7464;
wd::stream_parser p(options);
constexpr std::string_view text =
"\x1e{\"a\":1}\n"
"\x1e{\n \"b\": [\n 2\n ]\n}\n"
"\x1e\"three\"\n";
if (!write_in_pieces(p, text, options.on_error)) {
return false;
}
if (const wd::result<std::size_t> finished = p.finish(); finished.has_error()) {
report_error(p, finished.error());
return false;
}
take_ready(p, options.on_error);
return true;
}
rfc7464 records:
{"a":1}
{"b":[2]}
"three"
A bare number or other scalar that ends a concatenated stream is closed only by the end of the
input. A document that is an object or an array ends where its value closes, but 42 has no byte
that ends it: the number could go on in the next chunk. It is delivered when finish() is called,
which the Asio and Cobalt readers of this section call when the stream reaches its end.
With Boost.Asio
<webcpp/document/asio.hpp> reads a stream of documents from Boost.Asio’s streams. It is the one
header of document that includes Boost.Asio, which builds natively only, so <webcpp/document.hpp>
leaves it out, and a program that uses it includes it and is built natively.
wd::async_read_document(stream, buffer, parser, token) reads the next document of stream, an
AsyncReadStream such as a socket or a pipe. It takes the parser’s next document when the parser
holds one, and otherwise reads from the stream into buffer and writes what it read into the
parser, piece by piece, until a document is complete. The buffer is either of Boost.Asio’s two
models: a DynamicBuffer_v2, such as what asio::dynamic_buffer makes of a string, is taken by
value and only what it names must outlive the operation; a DynamicBuffer_v1, such as Beast’s
multi_buffer, is taken by reference and must outlive it. Bytes the buffer already holds are read
first, and a buffer made of several pieces is never joined into one. It is a composed operation that takes any completion token of Boost.Asio’s, a callback,
asio::use_awaitable, asio::deferred or boost::cobalt::use_op among them, and completes with
(boost::system::error_code, document):
| Error | The completion |
|---|---|
none |
The next document. |
the error of a document dropped |
With |
the error that stopped the stream |
With |
|
The stream ended, and the parser’s |
another |
The error of a read. |
On an error the document is empty. A completion is never called inside async_read_document: one
that needs no read is dispatched to the handler’s immediate executor, or posted to the stream’s.
One operation at a time reads a stream, its buffer and its parser, which outlive it.
The program below runs a server and a client on the loopback address, in one thread. The server writes NDJSON, then concatenated JSON, in pieces of one, three, seven, two and thirteen bytes, so the client’s parser meets documents cut in the middle. The connection is a pair of sockets:
/**
A TCP connection on the loopback address: the server's end, written to, and the client's end,
read from.
*/
struct link {
link(asio::io_context& io, tcp::acceptor& acceptor) : server(io), client(io) {
client.connect(acceptor.local_endpoint());
acceptor.accept(server);
}
tcp::socket server;
tcp::socket client;
};
The server’s end is written in pieces, and after each piece the context is drained until the client has consumed it, so that the output does not depend on timing; the write side is then shut down, which the client reads as the end of the stream:
/**
Writes `text` to the server's end in pieces of 1, 3, 7, 2 and 13 bytes in turn. After each piece
the context is drained until the client's parser has consumed it, so the pieces arrive one at a
time; then the write side is shut down, which the client reads as eof.
*/
bool send_in_pieces(asio::io_context& io, tcp::socket& server, std::string_view text,
const wd::stream_parser& parser) {
constexpr std::array<std::size_t, 5> sizes{1, 3, 7, 2, 13};
std::uint64_t sent = 0;
for (std::size_t turn = 0; !text.empty(); ++turn) {
const std::string_view piece = text.substr(0, sizes[turn % sizes.size()]);
text.remove_prefix(piece.size());
asio::write(server, asio::buffer(piece.data(), piece.size()));
sent += piece.size();
if (!drain_until(io, [&parser, sent] { return parser.offset() == sent; })) {
return false;
}
}
server.shutdown(tcp::socket::shutdown_send);
return true;
}
The client reads the NDJSON in a coroutine, awaiting async_read_document in a loop until the
completion is eof:
/**
Reads the client's documents until eof, awaiting each read in turn. The parser's finish delivers
its last document before the eof.
*/
asio::awaitable<void> read_in_coroutine(tcp::socket& client, wd::stream_parser& parser) {
std::string storage;
const auto buffer = asio::dynamic_buffer(storage);
for (;;) {
auto [ec, made] = co_await wd::async_read_document(client, buffer, parser,
asio::as_tuple(asio::use_awaitable));
if (ec == asio::error::eof) {
std::cout << "eof\n";
co_return;
}
if (ec) {
std::cout << "error: " << ec.message() << '\n';
co_return;
}
std::cout << made.stringify() << '\n';
}
}
/** Sends NDJSON over a link and reads it in a coroutine, one document printed per line. */
bool ndjson_in_coroutine(asio::io_context& io, tcp::acceptor& acceptor) {
std::cout << "NDJSON, read in a coroutine:\n";
link c(io, acceptor);
wd::stream_options options;
options.framing = wd::framing::lines;
wd::stream_parser parser(options);
constexpr std::string_view text =
"{\"id\":1,\"tags\":[\"a\",\"b\"]}\n"
"{\"id\":2,\"ok\":false}\r\n"
"\n"
"[3,null]\n"
"\"last\"";
bool done = false;
asio::co_spawn(io, read_in_coroutine(c.client, parser),
[&done](const std::exception_ptr&) { done = true; });
return send_in_pieces(io, c.server, text, parser) && drain_until(io, [&done] { return done; });
}
NDJSON, read in a coroutine:
{"id":1,"tags":["a","b"]}
{"id":2,"ok":false}
[3,null]
"last"
eof
and the concatenated JSON with a callback, which starts the next read by posting it, so that no read
begins inside its own callback. The last document of this stream is the bare number 42, which
arrives only when eof makes the reader finish the parser:
/**
Reads documents with a callback, each read started after the one before it completes. The next
read is posted rather than called, so no read is started from inside its own callback.
*/
class callback_reader {
public:
callback_reader(tcp::socket& client, wd::stream_parser& parser, bool& done)
: client_(client), parser_(parser), done_(done), buffer_(asio::dynamic_buffer(storage_)) {}
/** Starts the read of the next document. */
void start() {
wd::async_read_document(client_, buffer_, parser_,
[this](boost::system::error_code ec, const wd::document& made) {
if (ec == asio::error::eof) {
std::cout << "eof\n";
done_ = true;
return;
}
if (ec) {
std::cout << "error: " << ec.message() << '\n';
done_ = true;
return;
}
std::cout << made.stringify() << '\n';
asio::post(client_.get_executor(), [this] { start(); });
});
}
private:
tcp::socket& client_;
wd::stream_parser& parser_;
bool& done_;
std::string storage_;
asio::dynamic_string_buffer<char, std::char_traits<char>, std::allocator<char>> buffer_;
};
/** Sends concatenated JSON over a link and reads it with a callback. */
bool concatenated_with_callback(asio::io_context& io, tcp::acceptor& acceptor) {
std::cout << "concatenated JSON, read with a callback:\n";
link c(io, acceptor);
wd::stream_parser parser;
constexpr std::string_view text =
"{\"a\":1} {\"b\":[true,null]}\n"
"[1][2]\n"
"{\n \"c\": \"x\"\n}\n"
"42";
bool done = false;
callback_reader reader(c.client, parser, done);
reader.start();
return send_in_pieces(io, c.server, text, parser) && drain_until(io, [&done] { return done; });
}
concatenated JSON, read with a callback:
{"a":1}
{"b":[true,null]}
[1]
[2]
{"c":"x"}
42
eof
After an HTTP header, with Boost.Beast
A response’s header is read with Boost.Beast, and the body that follows it is read as a stream of
documents from the same buffer. Beast reads the socket in blocks, so the bytes of the body that
arrived with the header are in the buffer when async_read_header returns. They stay there, past
the header, and async_read_document takes them before it reads the socket again: nothing is copied
out of the buffer, and nothing is lost.
The program below runs a server and a client on the loopback address, in one thread. The client sends a GET, and the server answers with a header that Beast writes and an NDJSON body in pieces. The header and the body’s first piece leave in one write, so the client’s first read holds both, which is the case this section is about:
/**
Writes `bytes` to the socket; false when the write failed, after printing the error.
*/
asio::awaitable<bool> send_bytes(tcp::socket& socket, std::string_view bytes) {
auto [ec, sent] = co_await asio::async_write(socket, asio::buffer(bytes.data(), bytes.size()),
asio::as_tuple(asio::use_awaitable));
if (ec) {
std::cout << "server: " << ec.message() << '\n';
co_return false;
}
co_return true;
}
/**
Answers the client's GET: the response header, written by Beast, and the NDJSON body in pieces of
2, 5, 3 and 11 bytes; the write side is then shut down, which ends the body for the client, since
the response has no Content-Length.
*/
asio::awaitable<void> serve(tcp::socket& socket) {
constexpr std::string_view body =
"{\"id\":1,\"name\":\"ada\"}\n"
"{\"id\":2,\"tags\":[\"a\",\"b\"]}\n"
"[3,null]\n"
"\"last\"";
beast::multi_buffer buffer;
http::request<http::empty_body> request;
auto [read_ec, read_bytes] =
co_await http::async_read(socket, buffer, request, asio::as_tuple(asio::use_awaitable));
if (read_ec) {
std::cout << "server: " << read_ec.message() << '\n';
co_return;
}
// Without a Content-Length the body ends with the connection, so the connection is not kept.
http::response<http::empty_body> response{http::status::ok, request.version()};
response.set(http::field::content_type, "application/x-ndjson");
response.keep_alive(false);
std::ostringstream header;
header << response;
// The header and the body's first piece leave in one write, so the client's first read can take
// both: the bytes past the header are then in the client's buffer when Beast returns.
constexpr std::array<std::size_t, 4> sizes{2, 5, 3, 11};
std::string_view rest = body;
std::string first = header.str();
const std::string_view head_piece = rest.substr(0, sizes[0]);
rest.remove_prefix(head_piece.size());
first.append(head_piece);
if (!co_await send_bytes(socket, first)) {
co_return;
}
for (std::size_t turn = 1; !rest.empty(); ++turn) {
const std::string_view piece = rest.substr(0, sizes[turn % sizes.size()]);
rest.remove_prefix(piece.size());
if (!co_await send_bytes(socket, piece)) {
co_return;
}
}
socket.shutdown(tcp::socket::shutdown_send);
}
The client reads the header into a beast::multi_buffer with a response_parser<empty_body>, and
prints the status and the content type. Beast does not read the body: the same buffer goes on to
async_read_document, with the document parser of the stream:
beast::multi_buffer buffer;
http::response_parser<http::empty_body> header;
auto [header_ec, header_bytes] = co_await http::async_read_header(
socket, buffer, header, asio::as_tuple(asio::use_awaitable));
if (header_ec) {
std::cout << "client: " << header_ec.message() << '\n';
co_return;
}
const http::response<http::empty_body>& response = header.get();
std::cout << "status " << response.result_int() << ' ' << response.reason() << '\n';
std::cout << "content type " << response[http::field::content_type] << '\n';
// The buffer may already hold the first bytes of the body: whatever Beast read past the header
// stays in it. The reader writes those bytes into the parser before it reads the socket again,
// so the first document is parsed from them, and nothing is copied out of the buffer.
wd::stream_options options;
options.framing = wd::framing::lines;
wd::stream_parser documents(options);
for (;;) {
auto [ec, made] = co_await wd::async_read_document(socket, buffer, documents,
asio::as_tuple(asio::use_awaitable));
if (ec == asio::error::eof) {
std::cout << "eof\n";
co_return;
}
if (ec) {
std::cout << "error: " << ec.message() << '\n';
co_return;
}
std::cout << made.stringify() << '\n';
}
status 200 OK
content type application/x-ndjson
{"id":1,"name":"ada"}
{"id":2,"tags":["a","b"]}
[3,null]
"last"
eof
With Boost.Cobalt
async_read_document takes boost::cobalt::use_op as it takes any token, so a Cobalt task reads a
stream of documents in the style of Cobalt, and cobalt::as_result turns the completion into a
result whose error is the stream’s, eof included. One task writes the lines in pieces, another
reads each document and prints it with its number, until the end of the stream, which the last line
ends without a newline:
// Sends the text in pieces of 1, 3, 7, 2 and 13 bytes in turn, then shuts the sending side of the
// connection, which is the end of the stream for the reader.
cobalt::task<void> send(tcp::socket& socket, std::string_view text) {
constexpr std::array<std::size_t, 5> sizes{1, 3, 7, 2, 13};
for (std::size_t turn = 0; !text.empty(); ++turn) {
const std::size_t size = std::min(sizes[turn % sizes.size()], text.size());
co_await asio::async_write(socket, asio::buffer(text.data(), size), cobalt::use_op);
text.remove_prefix(size);
}
socket.shutdown(tcp::socket::shutdown_send);
}
// Reads the documents one at a time and prints each with its number; the end of the stream is
// eof, which is not an error, and a failure of the stream is printed and ends the reader.
cobalt::task<void> read(tcp::socket& socket) {
wd::stream_options options;
options.framing = wd::framing::lines;
wd::stream_parser parser(options);
std::string storage;
const auto buffer = asio::dynamic_buffer(storage);
std::size_t count = 0;
for (;;) {
auto next = co_await cobalt::as_result(
wd::async_read_document(socket, buffer, parser, cobalt::use_op));
if (next.has_error()) {
if (next.error() == asio::error::eof) {
break;
}
std::cout << "error: " << next.error().message() << '\n';
co_return;
}
++count;
std::cout << count << ' ' << next->stringify() << '\n';
}
std::cout << "eof after " << count << " documents\n";
}
// The connection is made before the coroutines start: the listening socket holds it until accept.
cobalt::main co_main(int /*argc*/, char** /*argv*/) {
const auto executor = cobalt::this_thread::get_executor();
tcp::acceptor acceptor(executor, tcp::endpoint(asio::ip::address_v4::loopback(), 0));
tcp::socket writer(executor);
tcp::socket reader(executor);
reader.connect(acceptor.local_endpoint());
acceptor.accept(writer);
co_await cobalt::join(send(writer, ndjson), read(reader));
co_return 0;
}
1 {"name":"Ada","tags":["math"],"n":1500}
2 [1,2.5,null,true,"x"]
3 "plain text"
4 {"z":1,"a":2}
5 {"last":true}
eof after 5 documents
In an actor system
A parser can live inside an actor, because it is sans-IO: the actor gives it bytes and takes its documents. The program below runs two actors on xactor’s FIFO driver, which makes its output the same on every run. The messages are the bytes of the stream, its end, a document the parser completed, and a line it dropped:
/** The bytes of the stream, as the host sends them. */
struct chunk {
std::string text;
};
/** The end of the stream, which the parser closes. */
struct end_of_stream {};
/** A document the parser completed, moved to the consumer: its image stays where it is. */
struct parsed {
wd::document value;
};
/** A line the parser dropped, and the line it was on. */
struct rejected {
std::string reason;
std::uint64_t line = 0;
};
using message = std::variant<chunk, end_of_stream, parsed, rejected>;
The parser actor writes each chunk it receives to a stream_parser, whose push form hands each
document to a callback that sends it to the consumer. The document is moved in the message, and its
image stays where it is. A line that is not JSON is dropped, and the actor reports it and goes on:
/** Parses the stream, and sends each document, or each line it drops, to the consumer. */
class parser final : public xactor::actor_logic<message> {
public:
explicit parser(xactor::actor_ref consumer) : consumer_(consumer), stream_(lines_skipping()) {}
/** The callback the parser hands each document to, and each dropped line's error. */
auto hand(xactor::turn<message>& turn) {
return
[this, &turn](wd::result<wd::document>&& made) { return relay(turn, std::move(made)); };
}
xactor::result<void> handle(xactor::turn<message>& turn,
const xactor::envelope<message>& cause) override {
if (const auto* bytes = std::get_if<chunk>(&cause.payload)) {
const xactor::result<std::size_t> written = stream_.write(bytes->text, hand(turn));
return as_void(written);
}
if (std::holds_alternative<end_of_stream>(cause.payload)) {
const xactor::result<std::size_t> finished = stream_.finish(hand(turn));
return as_void(finished);
}
return {};
}
private:
/** Sends a completed document, or the reason a line was dropped, to the consumer. */
xactor::result<void> relay(xactor::turn<message>& turn, wd::result<wd::document>&& made) {
if (made.has_value()) {
return turn.send(consumer_, parsed{.value = std::move(*made)});
}
return turn.send(consumer_,
rejected{.reason = made.error().message(), .line = stream_.error_line()});
}
/** Keeps the failure of a call of the parser; the documents it completed went on already. */
static xactor::result<void> as_void(const xactor::result<std::size_t>& counted) {
if (!counted.has_value()) {
return {boost::system::in_place_error, counted.error()};
}
return {};
}
xactor::actor_ref consumer_;
wd::stream_parser stream_;
};
The consumer prints what it receives:
/** Prints each document with stringify, and each line the parser dropped. */
class consumer final : public xactor::actor_logic<message> {
public:
xactor::result<void> handle([[maybe_unused]] xactor::turn<message>& turn,
const xactor::envelope<message>& cause) override {
if (const auto* got = std::get_if<parsed>(&cause.payload)) {
std::cout << got->value.stringify() << '\n';
}
if (const auto* dropped = std::get_if<rejected>(&cause.payload)) {
std::cout << "line " << dropped->line << " dropped: " << dropped->reason << '\n';
}
return {};
}
};
and the host delivers the chunks one at a time, cut inside a string, a number and a line, and runs the actors until they are idle after each:
xactor::scheduler<message> system(xactor::budgets{.fuel = 100});
const xactor::result<xactor::actor_ref> printing = xactor::create_actor<consumer>(system);
if (!printing.has_value()) {
return 1;
}
const xactor::result<xactor::actor_ref> reading =
xactor::create_actor<parser>(system, *printing);
if (!reading.has_value()) {
return 1;
}
// Two documents and a line that is not JSON, in chunks cut inside a string, a number and a
// line; a third document with an exponent, and a last one with no newline after it.
const std::array<std::string_view, 7> pieces{
R"({"name":"Ad)",
R"(a","born":181)",
"5}\n{\"name\":\"Gr",
R"(ace","langs":["COB)",
"OL\"]}\nnot j",
"son\n{\"name\":\"Alan\",\"n\":1.5",
"e3}\n{\"name\":\"Edsger\"}",
};
xactor::fifo_driver<message> driver(system);
std::uint64_t delivery = 0;
for (const std::string_view piece : pieces) {
const xactor::correlation_id execution{.value = ++delivery};
if (!system.deliver(*reading, chunk{.text = std::string(piece)}, execution).has_value()) {
return 1;
}
if (!driver.run_until_idle(nullptr).has_value()) {
return 1;
}
}
const xactor::correlation_id execution{.value = ++delivery};
if (!system.deliver(*reading, end_of_stream{}, execution).has_value()) {
return 1;
}
if (!driver.run_until_idle(nullptr).has_value()) {
return 1;
}
{"name":"Ada","born":1815}
{"name":"Grace","langs":["COBOL"]}
line 3 dropped: malformed JSON text
{"name":"Alan","n":1500}
{"name":"Edsger"}
Fed from JavaScript
A sender written in JavaScript can feed the parser compiled from C++ chunk by chunk, and a
ReadableStream such as a response body can be turned into an async iterable of documents. The
module that does this is taught in Streams; the program below shows the same
parser in a program of your own, compiled with Emscripten, which JavaScript drives through
Embind.
The class wraps a stream_parser and holds two JavaScript functions. JavaScript writes the bytes
of the stream to it, and its callback runs once for each document that a chunk completes, with the
text stringify writes, or for each line it drops, with the line and the message. Embind turns a
Uint8Array into the std::string of write, so a chunk may end inside a character:
/**
A parser of a stream of documents that JavaScript writes to and that calls JavaScript back: the
callback `on_document` is given each document as the text `stringify` writes, and `on_error` the
line and the message of an error.
The framing `lines` is NDJSON, whose bad line is dropped and reported while the stream goes
on; any other name is a concatenated stream, whose first error ends it.
*/
class json_stream {
public:
json_stream(const std::string& framing, emscripten::val on_document, emscripten::val on_error)
: on_document_(std::move(on_document)),
on_error_(std::move(on_error)),
parser_(options_of(framing)) {}
/** Takes a chunk of the stream, and calls back for each document it completes. */
void write(const std::string& chunk) {
const wd::result<std::size_t> written =
parser_.write(chunk, [this](auto made) { deliver(std::move(made)); });
if (written.has_error()) {
report(written.error());
}
}
/** Ends the stream, which closes a last line with no newline and a number at the end. */
void finish() {
const wd::result<std::size_t> finished =
parser_.finish([this](auto made) { deliver(std::move(made)); });
if (finished.has_error()) {
report(finished.error());
}
}
private:
static wd::stream_options options_of(std::string_view framing) {
wd::stream_options options;
if (framing == "lines") {
options.framing = wd::framing::lines;
options.on_error = wd::on_error::skip;
}
return options;
}
/** What the parser gives for each document it completes, or for each line it drops. */
void deliver(wd::result<wd::document> made) {
if (made.has_error()) {
report(made.error());
return;
}
on_document_(made->stringify());
}
void report(const boost::system::error_code& error) {
on_error_(static_cast<unsigned>(parser_.error_line()), error.message());
}
emscripten::val on_document_;
emscripten::val on_error_;
wd::stream_parser parser_;
};
EMSCRIPTEN_BINDINGS(document_stream_js) {
emscripten::class_<json_stream>("JsonStream")
.constructor<const std::string&, emscripten::val, emscripten::val>()
.function("write", &json_stream::write)
.function("finish", &json_stream::finish);
}
Node loads the module and sends two streams in chunks, one of NDJSON, whose bad line is dropped
and reported while the stream goes on, and one concatenated, whose last number only finish
closes. The fifth chunk of the NDJSON stream ends with the byte 0xC3, the first of the two bytes
of the é of Zoé:
// Sends a text to a C++ stream in chunks of the given size and prints what each chunk completes.
// The text is encoded first, so that a chunk may end inside a character, as a socket's may.
function send(framing, text, size) {
let chunk = 0
const stream = new module.JsonStream(framing,
document => lines.push(` chunk ${chunk}: ${document}`),
(line, message) => lines.push(` chunk ${chunk}: line ${line} dropped, ${message}`))
const bytes = encoder.encode(text)
lines.push(`${framing}, in chunks of ${size} bytes:`)
for (let at = 0; at < bytes.length; at += size) {
chunk += 1
stream.write(bytes.subarray(at, at + size))
}
chunk += 1
stream.finish()
stream.delete()
}
send('lines', '{"id":1,"tags":["a","b"]}\n{"id":2,"name":"Zoé"}\n{"bad":}\n[3,4.5]\n"last"', 9)
send('concatenated', '{"a":1} {"b":[2,3]}[4]"five" 6', 4)
Each line it prints says the chunk after which a document arrived. The last line of the NDJSON
stream, and the last number of the other, arrive when finish ends the stream, as nothing before
ends them:
lines, in chunks of 9 bytes:
chunk 3: {"id":1,"tags":["a","b"]}
chunk 6: {"id":2,"name":"Zoé"}
chunk 7: line 3 dropped, malformed JSON text
chunk 8: [3,4.5]
chunk 9: "last"
concatenated, in chunks of 4 bytes:
chunk 2: {"a":1}
chunk 6: {"b":[2,3]}
chunk 7: [4]
chunk 8: "five"
chunk 10: 6
The program is a driven test: webcpp.drive builds it with Emscripten and runs Node over it, in the
own lane js (Using it from JavaScript), where Node compares what it prints with these lines.
Selectors
A stream of a million records should not need the memory of a million records. A parser’s
selectors name places in the text, and deliver each value there as soon as it is complete, while
the rest of the text is still to come: on delivers a value and drops it from the document being
built, and observe delivers it and keeps it.
The language
A selector is a subset of JSONPath: $, the root, then any sequence of these steps.
| Step | What it matches |
|---|---|
|
The member |
|
The member |
|
The element |
|
Every element of an array. |
|
Every member of an object. |
So $.items[*].id matches the id of each element of items. A malformed selector is
errc::type, returned by on or observe.
Delivery
A callback takes a wd::const_value, the value, which it reads but cannot change, and a
wd::path_view, where the value is in the text, valid during the call; to_string() writes it as
a selector of that place, $['items'][0], which on parses back. A key’s ' and \ are escaped
and its other bytes written as they are, control characters included, so the text is not RFC
9535’s normalized path. A callback returns void or wd::result<void>. The program sums the
quantities of items as they stream, and keeps meta:
/** A callback that prints what it receives and where. */
constexpr auto print = [](const wd::const_value& v, wd::path_view where) {
std::cout << " " << where.to_string() << ": " << v.stringify() << '\n';
};
constexpr std::string_view text =
R"({"meta":{"page":1},"items":[{"id":"a","qty":2},{"id":"b","qty":3}],"done":true})";
wd::parser p;
std::int64_t total = 0;
const wd::result<void> taken =
p.on("$.items[*]", [&total](const wd::const_value& item, wd::path_view where) {
print(item, where);
total += item["qty"].value_or(std::int64_t{0});
});
const wd::result<void> kept = p.observe("$.meta", print);
if (taken.has_error() || kept.has_error() || p.write(text).has_error()) {
return 1;
}
std::cout << "total " << total << '\n';
finished(p);
$['meta']: {"page":1}
$['items'][0]: {"id":"a","qty":2}
$['items'][1]: {"id":"b","qty":3}
total 5
finish: {"meta":{"page":1},"items":[],"done":true}
A scalar is delivered when its token ends, and an array or an object when it closes. A value
inside another that also matches is delivered first, since it completes first; if on took it,
the outer value no longer holds it. A path’s indices are those of the text, which on does not
renumber:
wd::parser rows;
if (rows.on("$.rows[*][1]", print).has_error() ||
rows.observe("$.rows[*]", print).has_error() ||
rows.write(R"({"rows":[[1,2,3],[4,5,6]]})").has_error()) {
return 1;
}
finished(rows);
$['rows'][0][1]: 2
$['rows'][0]: [1,3]
$['rows'][1][1]: 5
$['rows'][1]: [4,6]
finish: {"rows":[[1,3],[4,6]]}
on("$") takes the root, and finish then returns a document that holds undefined. A number at
the top of the text completes only when the text ends, since another digit could follow, so it is
delivered by finish:
wd::parser whole;
if (whole.on("$", print).has_error() || whole.write("[1,2]").has_error()) {
return 1;
}
finished(whole); // the root was taken
whole.reset(); // the selectors stay
if (whole.write("12").has_error()) {
return 1;
}
std::cout << "12 written\n"; // a number at the top ends only with the text
finished(whole);
$: [1,2] finish: undefined 12 written $: 12 finish: undefined
A failing callback
A failure a callback returns fails the parser: write returns it, and so does every later call,
until reset. In a build with exceptions, an exception a callback throws fails the parser the same
way: write or finish rethrows it, and every later call throws it again, until reset.
wd::parser strict;
const wd::result<void> subscribed =
strict.on("$[*]", [](const wd::const_value& v, wd::path_view) -> wd::result<void> {
if (!v.is_number()) {
return wd::errc::type;
}
return {};
});
if (subscribed.has_error()) {
return 1;
}
std::cout << "write: " << strict.write(R"([1,"two",3])").error().message() << '\n';
finished(strict); // a failed parser stays failed until reset
write: a value of another kind finish: a value of another kind
In bounded memory
A value on takes is erased from the document once delivered, its blocks zeroed and freed, so
the next one takes the same room: a stream of any length needs the memory of one element at a
time. A callback that must keep a value copies it, into a document of its own, with set or
clone.
wd::parser stream;
std::uint64_t count = 0;
if (stream.on("$[*]", [&count](const wd::const_value&, wd::path_view) { ++count; })
.has_error() ||
stream.write("[").has_error()) {
return 1;
}
for (std::uint64_t i = 0; i < 100'000; ++i) {
const std::string element =
std::string(i == 0 ? "" : ",") + R"({"id":)" + std::to_string(i) + "}";
if (stream.write(element).has_error()) {
return 1;
}
}
if (stream.write("]").has_error()) {
return 1;
}
const wd::result<wd::document> streamed = stream.finish();
if (streamed.has_error()) {
return 1;
}
std::cout << count << " delivered, " << streamed->stringify()
<< " left, image under 64 KiB: " << std::boolalpha
<< (streamed->image().size() < 65'536) << '\n';
100000 delivered, [] left, image under 64 KiB: true
Traits
A document’s traits choose what it stores and how: the C++ types of its scalars, the encoding of
its strings, how it writes bytes, and the order of an object’s keys. wd::basic_document<Traits,
Storage> takes them first; wd::document is wd::basic_document<wd::default_traits, wd::owned>,
and its handles wd::value and wd::const_value are those of the default traits too.
| Member | wd::default_traits |
wd::js_traits |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js_traits derive from the default traits, and a program’s traits do the same, changing what they
need:
/** The default traits, but every key in the order it was created, whatever it is. */
struct insertion_traits : wd::default_traits {
using object = wd::insertion_order;
};
/** The default traits, but keys sorted by their code units, and bytes written as base64url. */
struct canonical_traits : wd::default_traits {
using object = wd::sorted;
using bytes = wd::base64url;
};
Key order parses a text with both, and Bytes writes bytes with the
second. The concept wd::document_traits says what traits must give, and a document of traits
that do not satisfy it does not compile. This version fixes boolean, integer and date to
bool, std::int64_t and wd::date_ms; number is the traits’ choice, a type that satisfies
wd::stored_scalar, trivially copyable, of 1, 2, 4, 8 or 16 bytes, with a wd::scalar_codec that
stores it the same way on every target and a wd::number_codec that reads it from JSON text and
writes it as JavaScript writes numbers. string, bytes and object are one of the encodings and
policies Strings, dates and bytes and Key order list, and array is wd::dense, the one
array policy.
Traits are part of the image
A document’s image records its traits: its header holds a tag that hashes the size and the codec
of each scalar, the encoding, the bytes policy and the object policy, with the sizes of the tree’s
nodes. An image written with other traits is refused with errc::incompatible_document, never read
wrongly (What is refused). Copies between documents need the same traits, and a
document’s handles carry them in their type, so a handle of one traits cannot be given to a
document of another.
Images
A document lives in one block of memory, its image: a header of 64 bytes, then a heap that holds the tree, its nodes, its indices and its strings. Every pointer inside is an offset from where it is stored, and every size a 64-bit integer, so the image works at any 16-aligned address and has the same bytes on a 64-bit native target and on wasm32. A document can therefore live in memory it does not own, such as a browser’s, be copied as bytes, be written to a file and read back, and travel as binary between a program’s processes. This section teaches the image itself: its bytes, its header, what opening refuses, where it lives and how its size is managed. Images on the wire sends images over streams and Sharing shares them between threads and processes.
The bytes
doc.image() is the image, a std::span<const std::byte> valid until the document’s next change.
wd::from_image(bytes) copies an image into a new owned document, from any address;
doc.clone() does the same with a document’s own:
wd::result<wd::document> made = wd::parse(R"({"user":"Ada","langs":["Analytical","Note G"]})");
if (made.has_error()) {
return false;
}
const std::span<const std::byte> image = made->image();
std::cout << image.size() << " bytes, sha-256 " << digest_of(image) << "...\n";
// An owned copy, at another address, which grows as any owned document does.
wd::result<wd::document> copy = wd::from_image(image);
if (copy.has_error() || !(*copy)["user"].set("Lovelace")) {
return false;
}
std::cout << copy->stringify() << '\n' << made->stringify() << '\n';
4096 bytes, sha-256 ed85084394e7bf7f...
{"user":"Lovelace","langs":["Analytical","Note G"]}
{"user":"Ada","langs":["Analytical","Note G"]}
The same operations give the same image, byte for byte, at any address and on every target: the digest above is the one every target of webcpp’s continuous integration prints, natively with Clang and GCC, on emscripten, and on wasm32-wasip2 and wasm32-wasip3. Free memory is zero: a buffer is zeroed when it is made or grown, a block is zeroed before it is freed, and nodes are built in place. So an erased value, a secret among them, leaves none of its bytes in the image, and the image is a function of the operations alone.
After an operation that failed for want of room, the document’s content is as it was, but bytes
of the heap’s free space may differ, where the heap keeps its own bookkeeping: compare documents
with ==, not their raw images, after a failure.
The header
The header says what the bytes after it are, in the byte order of the machine that wrote it:
| Offset | Size | Field |
|---|---|---|
0 |
8 |
the magic, |
8 |
4 |
the format version, 1 |
12 |
4 |
the byte-order mark, |
16 |
4 |
reserved, zero; an image whose reserved bytes are not zero is refused |
20 |
4 |
the layout tag: a hash of the sizes and alignments of the tree’s node, its container and the heap, and of the traits (Traits are part of the image) |
24 |
8 |
the size of the image |
32 |
8 |
the offset of the tree’s container, the heap’s first block, the same in every image |
40 |
8 |
the id the next node takes; ids are never reused |
48 |
8 |
the seal: zero for an image that is not sealed, else the high bit and the CRC-32C of the image, with these eight bytes taken as zero (Sealed images) |
56 |
8 |
zero |
The format version is the library’s: webcpp::document::format_version, which the library raises when
the layout of an image changes, and then provides a migration from the previous version. The Boost
release an image was written with is not in the header, and a Boost upgrade does not change the
version, since the structures the image holds, Boost.Interprocess’s rbtree_best_fit blocks and
Boost.MultiIndex’s ordered index nodes, rarely change. The library watches them: its format test
compares the compact image of a fixed document, under each traits, with golden images committed with
the tests, and fails when a Boost release changes one byte of them. The bytes at offset 16 are kept
for a future version.
What is refused
wd::open and wd::from_image refuse bytes shorter than the header or than the size it states,
and another magic, with errc::not_a_document; and an image of another format version, byte order,
layout tag or nonzero reserved bytes, with errc::incompatible_document. A mismatch is refused,
never read or converted. Then the heap’s own check of its structure runs, and a container offset other than
the heap’s first block, or a pointer to the root that leaves the image, is errc::not_a_document
too, refused before anything is read through it. A sealed image whose bytes do not match its seal is
errc::damaged_document, and is checked first of all (Sealed images).
std::cout << "16 zero bytes: " << wd::from_image(std::array<std::byte, 16>{}).error().message()
<< '\n';
std::cout << "other traits: " << wd::from_image<wd::js_traits>(image).error().message() << '\n';
16 zero bytes: not a document's image other traits: an image of another format, layout, byte order or traits
Each refusal has its own cause, and a program tells them apart by the errc of the error. This
program makes one of each and prints the code and the message: an image of other traits, an image of
another format version, text that is longer than a header and has no magic, zeroed memory, a buffer
that is not 16-aligned (since open reads in place), an image cut short, a header that states a size
larger than the bytes it is given, and a header that names a container outside the heap. The last
line is the same image cut short after it was sealed (Sealed images):
// An image written with default_traits is no image of js_traits: the layout tag differs.
if (!refused("js_traits", wd::from_image<wd::js_traits>(image))) {
return false;
}
// The format version is the header's field at offset 8; another version is not read.
std::span<std::byte> bytes = copy_into(storage, image);
set_field(bytes, offsetof(wd::image_header, version), std::uint32_t{2});
if (!refused("format version 2", wd::open(bytes))) {
return false;
}
// Bytes that are not an image, and are longer than a header, have no magic.
constexpr std::string_view text =
"this text is not an image, and it is longer than the 64 bytes of a header";
if (!refused("garbage", wd::from_image(std::as_bytes(std::span(text))))) {
return false;
}
// Memory that was never written reads as zeros, which are no magic either.
alignas(16) std::array<std::byte, 256> zeros{};
if (!refused("zeroed memory", wd::open(zeros))) {
return false;
}
// open reads the image in place, so its buffer must be 16-aligned: this one starts 8 bytes in.
bytes = copy_into(std::span<std::byte>(storage).subspan(8), image);
if (!refused("misaligned buffer", wd::open(bytes))) {
return false;
}
// Cut short, an image that is not sealed is no image at all.
bytes = copy_into(storage, image);
if (!refused("truncated, unsealed", wd::open(bytes.first(bytes.size() - 16)))) {
return false;
}
// A header that states a size larger than the bytes it is given is refused before it is read.
bytes = copy_into(storage, image);
set_field(bytes, offsetof(wd::image_header, size), std::uint64_t{image.size() + 4096});
if (!refused("size", wd::open(bytes.first(image.size())))) {
return false;
}
// A header that names a container outside the heap is refused before anything is read there.
bytes = copy_into(storage, image);
set_field(bytes, offsetof(wd::image_header, container), std::uint64_t{image.size()});
return refused("container offset", wd::open(bytes));
// Cut short, a sealed image is a torn write: damaged, not merely no image.
std::span<std::byte> bytes = copy_into(storage, made->image());
if (!refused("truncated, sealed", wd::open(bytes.first(bytes.size() - 16)))) {
return false;
}
js_traits: incompatible_document, an image of another format, layout, byte order or traits format version 2: incompatible_document, an image of another format, layout, byte order or traits garbage: not_a_document, not a document's image zeroed memory: not_a_document, not a document's image misaligned buffer: not_a_document, not a document's image truncated, unsealed: not_a_document, not a document's image size: not_a_document, not a document's image container offset: not_a_document, not a document's image truncated, sealed: damaged_document, a sealed image whose bytes do not match its seal
An image is trusted input. Those checks cannot prove sound every offset inside the tree, so an image travels only between a program’s own processes and peers it trusts; untrusted data enters as JSON text, which the parser checks entirely. A seal (Sealed images) catches bytes that changed by accident and is not authentication: never open an image from a source you do not trust.
Owned and external storage
A document’s second template parameter is its storage:
| Storage | The buffer | When it is full |
|---|---|---|
|
the document’s own, 16-aligned |
it grows: a buffer twice the size, or what the operation needs if more, the image copied and the
heap grown, up to the |
|
a |
the operation returns |
wd::format(buffer) makes a new, empty document in a buffer, holding null; wd::open(buffer)
opens the image a buffer holds, in place; wd::parse_into(buffer, text) parses into one. Every
operation finds its room before it allocates, so a full buffer is an error value, never a
half-made change. A buffer that is not 16-aligned is errc::capacity to format, which finds no
room for an image there, and errc::not_a_document to open.
// A buffer the program owns, such as a browser's memory, 16-aligned.
alignas(16) std::array<std::byte, 8192> buffer{};
using external_document = wd::basic_document<wd::default_traits, wd::external>;
wd::result<external_document> formatted = wd::format(buffer);
if (formatted.has_error() || !formatted->root().set(wd::array{})) {
return false;
}
wd::basic_value<wd::default_traits, wd::external> list = formatted->root();
std::uint64_t pushed = 0;
wd::result<std::uint64_t> length = list.push("an element of the list");
for (; length.has_value(); length = list.push("an element of the list")) {
++pushed;
}
const std::string before = formatted->stringify();
std::cout << pushed << " pushed, then: " << length.error().message() << '\n';
// A failed operation changes nothing of the content.
std::cout << "unchanged: " << std::boolalpha
<< (list.splice(0, 1, wd::array{1, 2, 3}).error() == wd::errc::capacity) << ' '
<< (formatted->stringify() == before) << '\n';
// The buffer holds the image, which another document opens in place.
wd::result<external_document> opened = wd::open(buffer);
if (opened.has_error()) {
return false;
}
std::cout << "opened in place: " << opened->root().size() << " elements\n";
31 pushed, then: no room left unchanged: true true opened in place: 31 elements
Reserving
A document that is to be built large grows by doubling: each time the image is full, a buffer twice
the size is made, the image is copied into it and the old one is freed. reserve(bytes) is
std::vector::reserve for an image: it makes the image hold at least bytes in one step, so that a
batch of insertions of a known size moves nothing, and capacity() says how large the image is. A
reservation moves the image once, which invalidates every std::string_view read from a value but no
handle or iterator, and clears a seal; a reservation within the capacity changes nothing. A request
past the document’s max_bytes, or past an external image’s size, is errc::capacity and changes
nothing.
This program inserts ten thousand members into two objects, one without a reservation and one with a reservation of 256 bytes a member, and asks whether the image grew:
// The batch's size is known, so the image is reserved once, before the insertions: they then
// move nothing. Without the reservation, the same insertions grow the image.
constexpr std::int64_t batch = 10'000;
constexpr std::uint64_t bytes_per_member = 256;
wd::result<wd::document> plain_made = wd::parse("{}");
if (plain_made.has_error()) {
return fail(plain_made);
}
wd::document& plain = *plain_made;
wd::result<wd::object_ref> as_plain = plain.root().as_object();
if (as_plain.has_error()) {
return fail(as_plain);
}
const std::uint64_t plain_before = plain.capacity();
for (std::int64_t i = 0; i < batch; ++i) {
const auto added = as_plain->insert("k" + std::to_string(i), i);
if (added.has_error()) {
return fail(added);
}
}
std::cout << "without reserve, " << batch
<< " insertions grew the image: " << (plain.capacity() != plain_before) << '\n';
wd::result<wd::document> reserved_made = wd::parse("{}");
if (reserved_made.has_error()) {
return fail(reserved_made);
}
wd::document& reserved = *reserved_made;
wd::result<wd::object_ref> as_reserved = reserved.root().as_object();
if (as_reserved.has_error()) {
return fail(as_reserved);
}
const std::uint64_t reserved_before = reserved.capacity();
const auto reservation = reserved.reserve(static_cast<std::uint64_t>(batch) * bytes_per_member);
if (reservation.has_error()) {
return fail(reservation);
}
const std::uint64_t reserved_capacity = reserved.capacity();
std::cout << "reserve grew the image: " << (reserved_capacity != reserved_before) << '\n';
for (std::int64_t i = 0; i < batch; ++i) {
const auto added = as_reserved->insert("k" + std::to_string(i), i);
if (added.has_error()) {
return fail(added);
}
}
std::cout << "after the reserve, " << batch
<< " insertions grew it: " << (reserved.capacity() != reserved_capacity) << '\n';
without reserve, 10000 insertions grew the image: true reserve grew the image: true after the reserve, 10000 insertions grew it: false
Shrinking
A document that is parsed starts at 4096 bytes, and a document that is built grows from there as it
needs. doc.shrink_to_fit() gives back the free space at the end of an owned image, and only that:
blocks are never moved, so a value that sits at the end of the heap keeps the image as large as its
end. For a small document it is the whole story, a few values shrinking to little more than a
kilobyte. For a document that grew and then let its values go, it may leave an image nearly as large
as it was, and a deep copy into a new document packs the content, as compact does in place
(Compacting):
wd::result<wd::document> made = wd::parse("{}");
if (made.has_error()) {
return false;
}
for (std::uint64_t i = 0; i < 1000; ++i) {
if (!(*made)[std::to_string(i)].set(std::string(100, 'x'))) {
return false;
}
}
const std::uint64_t grown = made->image().size();
if (!made->root().set(wd::object{{"kept", true}}) || !made->shrink_to_fit()) {
return false;
}
std::cout << "grown to " << grown << " bytes, shrunk to " << made->image().size() << ": "
<< made->stringify() << '\n';
// Blocks never move: a deep copy into a new document packs the content.
wd::result<wd::document> packed = wd::document::create();
if (packed.has_error() || !packed->root().set(*made)) {
return false;
}
std::cout << "packed: " << packed->image().size() << " bytes\n";
grown to 524288 bytes, shrunk to 352864: {"kept":true}
packed: 4096 bytes
Compacting
doc.compact() rebuilds an owned image as the compact image of its content: the nodes in document
order, their ids numbered from the first, each block right after the last with no hole between, and
the image exactly as large as they are. Where shrink_to_fit can only trim the free tail,
compact takes out every hole, so a document that lost most of its values is smaller compacted
than shrunk. Measured, the same on a 64-bit target and on wasm32: a small document of 60 members
after 54 of them were erased is 15,312 bytes after shrink_to_fit and 2,352 after compact; a
large one, with 90% of its members erased, is 2,080,304 bytes and 209,424.
Compacting also makes the image a function of the content alone. Two documents of equal content
have byte-identical images after it, whatever edits made them, whether they were parsed, or built by
set, erase and splice, or had values replaced; and a compact image compacts to itself. Equal
documents can then be compared, hashed or stored by their bytes, and a seal over a compact image is
the same everywhere.
compact_size() is the exact size of the compact image, which it reads from the nodes without
allocating anything, so a program sizes a block with it before it builds the image.
compact_into(buffer) writes the compact image into a 16-aligned buffer of the program’s own and
opens it there, as an external document, zeroing the rest of the buffer and leaving the document as it was: it is the way to compact a
document that cannot reallocate, such as one leased from a pool or held in a shared block, or to send
an image without waste. It is errc::capacity for a buffer that is smaller than compact_size() or
not aligned, and writes nothing then.
Compacting removes holes, not the 192 bytes of each node (Memory per value), so it does not change how an image compares in size with the JSON text of the same document (The sizes on the wire).
compact() builds the new image before it replaces the old one, so a failure changes nothing. As a
vector’s reallocation does, it invalidates every handle, every std::string_view read from a value
and every iterator, and, the bytes having changed, it clears a seal: sealing again is the caller’s.
The image’s size afterwards is compact_size(), which is also its capacity(), so the next change
that needs room grows it.
A moved document takes the image of the one it is moved from, which then holds nothing: its handles, and those of the document moved into, are invalid.
Images on the wire
A document is its image, and its image can be sent as it is: the sender writes the bytes, and the receiver reads them into a block of memory and opens the document there. Nothing is parsed, no tree is built, and nothing is allocated for the values or for the message; the receiver’s work is the copy of the bytes off the stream and, for a sealed image, one pass to check the seal. JSON text needs a parse on arrival and allocates for each value it holds.
That is the use document’s image is built for, between programs that trust each other: a page and its Worker, the threads of a process, the processes of a machine, two machines on a fast local link whose receiver’s CPU is the bottleneck (Binary transmission). It is not the use for a narrow network: an image is many times the size of its JSON text, and larger than the compressed text (The sizes on the wire), so there the text is the smaller payload. The benchmarks measure what the receiver saves on each side of the trade (Reading the results).
This section sends images over a stream with Boost.Asio and receives them into a pool of blocks; sealing them, what a reader refuses and how big a block must be follow. Sharing shares images between threads and processes without any stream.
An image frames itself
An image states its size in its header (The header), so a stream of images needs no
separator and no length prefix: the images follow one another, and a reader that has the first 64
bytes of one knows where it ends. wd::async_write_image(stream, doc, token) seals the document and
writes its image as one buffer. wd::async_read_image(stream, pool, token) reads the header of the
next image into a block of a pool, checks it before it reads another byte, reads the rest of the
image into the same block and opens the document there. It reads exactly the bytes the image needs,
so the stream is left at the end of the image it completed.
Both are composed operations of Boost.Asio, in <webcpp/document/asio.hpp>, which take any completion
token, and are built natively only, as the rest of that header is (With Boost.Asio).
async_write_image completes with an error_code; async_read_image completes with an
error_code and a wd::leased_document<>, which holds the document and the block it lives in.
Pools of blocks
wd::image_pool::create(block_bytes, blocks) allocates a pool once: blocks blocks of block_bytes
bytes each, 16-aligned and zeroed, in one allocation. A block is leased to each image a reader
receives, and goes back when the leased_document that holds it goes, its bytes zeroed. The pool
therefore allocates nothing per message; a receiver that can hold two images at once makes a pool of
two blocks:
/** A pool of two blocks: the most the client leases at once. */
wd::image_pool pool_of_two() {
return wd::image_pool::create(block_bytes, 2).value();
}
When every block is leased, async_read_image completes with errc::capacity and reads nothing:
this is the backpressure of a stream of images, and the stream is read again when a block is given
back. A pool outlives its leases and its readers, and is not thread-safe: a block is leased and
given back, a lease destroyed included, on one thread at a time. For blocks that threads and
processes share, see Shared documents.
A leased_document is moved, never copied, and reads and writes as any document does within its
image; a change that needs more room than its block has returns errc::capacity. The sans-IO
wd::image_reader under async_read_image is the same reader without Asio: write(bytes) takes
the bytes of a stream and opens each image they complete, and prepare() and commit() let a
socket read straight into the block, with no copy at all.
Sizing a block
A block holds the largest image the pool will receive, a multiple of 16 and at least 64: an image
larger than a block is refused with errc::capacity. What a document costs on the wire is its image,
whatever it holds.
A document that is parsed starts at 4096 bytes, as the parser gives it room, so a document of a few
values sent as it is travels as 4096 bytes. shrink_to_fit() gives back the free space at the end of
the image, and only that: for a small document it brings the image down to little more than a
kilobyte (Shrinking). compact() rebuilds the image with no hole in it, and
compact_size() says how large that image is before it is built (Compacting), so a
program sizes its blocks, and what it sends, to what a document holds. This function makes the
documents of the next example, parsing each and shrinking it before it is sent:
/**
A document parsed from JSON text and shrunk to its size, so that its image holds what the document
needs and no more.
*/
wd::document document_of(std::string_view text) {
wd::document made = wd::parse_or_throw(text);
// A shrink that fails leaves the document as valid as it was, only larger.
static_cast<void>(made.shrink_to_fit());
return made;
}
/** The documents the server sends, in order. */
std::vector<wd::document> documents() {
std::vector<wd::document> sent;
sent.push_back(document_of(ada));
sent.push_back(document_of(grace));
sent.push_back(document_of(alan));
return sent;
}
Sending and receiving
The example sends three documents over a connection on the loopback address, in one program, and
receives them twice, once in a coroutine and once with a callback. The receiver prints each document
with stringify while its image is held in its block, whether the image is sealed, and the blocks the
pool has free then:
/**
Prints a document as the client holds it: its text, whether its image is sealed, and the blocks the
pool has free while the image's block is leased.
*/
void print_held(const wd::leased_document<>& image, const wd::image_pool& pool) {
std::cout << image.get().stringify() << ": " << (image.sealed() ? "sealed" : "not sealed")
<< ", blocks available: " << pool.available() << '\n';
}
The coroutine sends each image, awaiting each write, and shuts the write side down at the end, which
the reader sees as eof. The receiver awaits async_read_image in a loop. Each image is printed
while its block is leased, and the block goes back when the iteration ends:
/** Sends each document's image, awaiting each write, then shuts the write side down. */
asio::awaitable<void> send_in_coroutine(tcp::socket& server, std::vector<wd::document>& sent) {
for (wd::document& document : sent) {
auto [ec] =
co_await wd::async_write_image(server, document, asio::as_tuple(asio::use_awaitable));
if (ec) {
std::cout << "error: " << ec.message() << '\n';
co_return;
}
}
server.shutdown(tcp::socket::shutdown_send);
}
/**
Receives images until eof, awaiting each read in turn. Each image is printed while its block is
leased, and the block goes back when the loop's iteration ends.
*/
asio::awaitable<void> receive_in_coroutine(tcp::socket& client, wd::image_pool& pool, bool& done) {
for (;;) {
auto [ec, image] =
co_await wd::async_read_image(client, pool, asio::as_tuple(asio::use_awaitable));
if (ec == asio::error::eof) {
std::cout << "eof, blocks available: " << pool.available() << '\n';
done = true;
co_return;
}
if (ec) {
std::cout << "error: " << ec.message() << '\n';
done = true;
co_return;
}
print_held(image, pool);
}
}
/** Sends the documents over a link and receives them in a coroutine. */
bool images_in_coroutine(asio::io_context& io, tcp::acceptor& acceptor,
std::vector<wd::document>& sent) {
std::cout << "images, received in a coroutine:\n";
link c(io, acceptor);
wd::image_pool pool = pool_of_two();
bool done = false;
asio::co_spawn(io, send_in_coroutine(c.server, sent), asio::detached);
asio::co_spawn(io, receive_in_coroutine(c.client, pool, done), asio::detached);
return drain_until(io, [&done] { return done; });
}
images, received in a coroutine:
{"name":"Ada","born":1815,"langs":["Analytical"]}: sealed, blocks available: 1
{"name":"Grace","langs":["COBOL"]}: sealed, blocks available: 1
{"name":"Alan","n":1500}: sealed, blocks available: 1
eof, blocks available: 2
With a pool of two blocks and one document held, one block is free, and at eof both are. With
callbacks, the next write and the next read are
posted rather than called, so that none starts inside the callback of the one before it:
/**
Sends each document's image with async_write_image, the next one started after the last one
completed, then shuts the write side down. The next write is posted, not called, so that no write
starts inside the callback of the one before it.
*/
class callback_sender {
public:
callback_sender(tcp::socket& server, std::vector<wd::document>& sent)
: server_(server), sent_(sent) {}
/** Starts the first write. */
void start() { send_next(); }
private:
void send_next() {
if (next_ == sent_.size()) {
server_.shutdown(tcp::socket::shutdown_send);
return;
}
wd::async_write_image(server_, sent_[next_], [this](boost::system::error_code ec) {
if (ec) {
std::cout << "error: " << ec.message() << '\n';
return;
}
++next_;
asio::post(server_.get_executor(), [this] { send_next(); });
});
}
tcp::socket& server_;
std::vector<wd::document>& sent_;
std::size_t next_ = 0;
};
/**
Receives images with async_read_image, the next read started after the last one completed, until
eof. The image is printed inside its callback, where its block is leased; the block goes back when
the callback returns.
*/
class callback_receiver {
public:
callback_receiver(tcp::socket& client, wd::image_pool& pool, bool& done)
: client_(client), pool_(pool), done_(done) {}
/** Starts the first read. */
void start() { read_next(); }
private:
void read_next() {
wd::async_read_image(
client_, pool_, [this](boost::system::error_code ec, wd::leased_document<> image) {
if (ec == asio::error::eof) {
std::cout << "eof, blocks available: " << pool_.available() << '\n';
done_ = true;
return;
}
if (ec) {
std::cout << "error: " << ec.message() << '\n';
done_ = true;
return;
}
print_held(image, pool_);
asio::post(client_.get_executor(), [this] { read_next(); });
});
}
tcp::socket& client_;
wd::image_pool& pool_;
bool& done_;
};
/** Sends the documents over a link and receives them with a callback. */
bool images_with_callback(asio::io_context& io, tcp::acceptor& acceptor,
std::vector<wd::document>& sent) {
std::cout << "images, received with a callback:\n";
link c(io, acceptor);
wd::image_pool pool = pool_of_two();
bool done = false;
callback_sender sender(c.server, sent);
callback_receiver receiver(c.client, pool, done);
sender.start();
receiver.start();
return drain_until(io, [&done] { return done; });
}
images, received with a callback:
{"name":"Ada","born":1815,"langs":["Analytical"]}: sealed, blocks available: 1
{"name":"Grace","langs":["COBOL"]}: sealed, blocks available: 1
{"name":"Alan","n":1500}: sealed, blocks available: 1
eof, blocks available: 2
Several images in one write
wd::async_write_images(stream, documents, token) writes the images of a range of documents in one
gather write. Every document is sealed first, and when one fails to seal the operation completes
with its error and writes nothing. The images are not copied: Boost.Asio sends them with writev
and carries on over partial writes. The operation completes with the bytes written, which are the
sum of the images’ sizes; on an error, they are the bytes that reached the stream, and the whole
documents among them are the leading ones whose images fit in that count. The range is taken by
lvalue reference, and its documents stay unchanged until the write completes. The example sends its
three documents this way:
/**
Sends the three images in one gather write with async_write_images, then receives them in a
coroutine, as the first exchange does.
Tip: the write reads the images in place and completes with the bytes it wrote, so the documents
stay unchanged until it completes.
*/
bool images_in_one_write(asio::io_context& io, tcp::acceptor& acceptor,
std::vector<wd::document>& sent) {
std::cout << "images, sent in one write:\n";
link c(io, acceptor);
wd::image_pool pool = pool_of_two();
bool done = false;
wd::async_write_images(c.server, sent, [&c](boost::system::error_code ec, std::size_t bytes) {
if (ec) {
std::cout << "error: " << ec.message() << '\n';
} else {
std::cout << "written: " << bytes << " bytes\n";
}
c.server.shutdown(tcp::socket::shutdown_send);
});
asio::co_spawn(io, receive_in_coroutine(c.client, pool, done), asio::detached);
return drain_until(io, [&done] { return done; });
}
images, sent in one write:
written: 4176 bytes
{"name":"Ada","born":1815,"langs":["Analytical"]}: sealed, blocks available: 1
{"name":"Grace","langs":["COBOL"]}: sealed, blocks available: 1
{"name":"Alan","n":1500}: sealed, blocks available: 1
eof, blocks available: 2
Sealed images
doc.seal() writes a seal into the image’s header: the CRC-32C (Castagnoli) of the image’s bytes, with
the seal’s own eight bytes taken as zero, and a marker in the high bit. async_write_image seals the
document it sends. Opening a sealed image, with wd::open, wd::from_image or the image reader,
checks the seal first of all, before anything else its header states is trusted, and refuses an
image whose bytes do not match with errc::damaged_document. A flipped bit on a disk or a wire, a
torn write, a stray write over a buffer and an image cut short are all damage, and none of the image
is read:
// A sealed image with one byte changed is refused as damaged, by open and by from_image.
const auto found =
std::ranges::search(bytes, std::as_bytes(std::span(std::string_view("Ada"))));
if (found.empty()) {
return false;
}
found.front() = std::byte{'a'};
if (!refused("one byte changed", wd::open(bytes))) {
return false;
}
one byte changed: damaged_document, a sealed image whose bytes do not match its seal
The seal holds until the document’s next change, which clears it: an image is then simply not
sealed, never wrongly sealed, and sealed() says which. It costs a pass over the image, about 600
MB/s, and checking it when the image is opened costs the same. A program that trusts its memory, such
as one that opens an image it has just written, skips the check with wd::verify::none. The changed
byte of the example then reads as it is:
// verify::none skips the seal: the caller trusts the source, and the changed byte reads as is.
wd::result<wd::document> trusted =
wd::from_image(bytes, wd::default_max_bytes, wd::verify::none);
if (trusted.has_error()) {
return false;
}
std::cout << "verify::none, accepted: " << trusted->stringify() << '\n';
verify::none, accepted: {"user":"ada","langs":["Analytical","Note G"]}
A sealed image left in reused memory, which a later use wrote over in part, is refused too, since the seal shows that the bytes changed since the image was written. An image that is not damaged opens in place, and is still sealed:
// An old sealed image left in memory, which a later use wrote over in part, is refused as
// damaged: the seal shows that the bytes changed since the image was written.
bytes = copy_into(storage, made->image());
std::ranges::fill(bytes.subspan(2048, 64), std::byte{0xAA});
if (!refused("reused memory", wd::open(bytes))) {
return false;
}
// An intact sealed image opens in place, and is still sealed.
bytes = copy_into(storage, made->image());
wd::result<wd::basic_document<wd::default_traits, wd::external>> opened = wd::open(bytes);
if (opened.has_error()) {
return false;
}
std::cout << "opened, sealed " << std::boolalpha << opened->sealed() << ": "
<< opened->stringify() << '\n';
reused memory: damaged_document, a sealed image whose bytes do not match its seal
opened, sealed true: {"user":"Ada","langs":["Analytical","Note G"]}
A seal is a checksum against accidental damage, and is not authentication. Anyone can compute a valid seal for any bytes, so a sealed image from a source that is not trusted is no safer than an unsealed one. Never open an image from an untrusted source: the checks of What is refused cannot prove sound every offset inside a tree, and an image travels only between programs that trust each other. Untrusted data enters as JSON text, which the parser checks entirely.
What a reader refuses
The completion of async_read_image says what happened, and on every error the lease it carries is
empty:
| Error | When |
|---|---|
none |
An image was read and opened; the lease holds it. |
|
The stream ended between two images. |
|
Every block of the pool is leased, and the stream is not read; or an image is larger than a block. The end of the stream is reported this way too while the pool is full, since the reader needs a block to learn that the stream ended. |
|
Bytes that are not an image, or an image whose heap is not sound. |
|
An image of another format version, byte order, reserved bytes or traits. |
|
A sealed image whose bytes do not match its seal. |
another |
The error of a read. |
The refusal of an image ends the reader, as an error ends a stream of documents (Errors), because a stream of images that has lost its place cannot find the next header. The refusals are the ones of What is refused, which that section shows one by one.
Sharing
JavaScript shares an object by sharing a reference to it, and its collector decides when the object goes. A C++ program that shares a document between threads or processes needs the same two things, a reference that several holders have and a moment when the last one lets go, and document gives them without a collector and without copying the document. Its image is relocatable and holds offsets, not addresses, so it is the same bytes in every thread and in every process that maps it, at whatever address.
Threads
A document that moves from one thread to another moves its handle and never the bytes of its image. A producer parses documents and pushes each into a queue; the consumer receives them and keeps them. The queue is the program’s, of any kind; this one wakes the consumer on a push and tells it when nothing more comes:
// A queue for one producer and one consumer: a push wakes the consumer, and close tells it that
// nothing more comes.
template <class T>
class queue {
public:
void push(T value) {
{
const std::scoped_lock lock(mutex_);
items_.push_back(std::move(value));
}
ready_.notify_one();
}
void close() {
{
const std::scoped_lock lock(mutex_);
closed_ = true;
}
ready_.notify_all();
}
// The oldest value, waiting while the queue is open; nothing once it is closed and empty.
std::optional<T> pop() {
std::unique_lock lock(mutex_);
ready_.wait(lock, [this] { return closed_ || !items_.empty(); });
if (items_.empty()) {
return std::nullopt;
}
T value = std::move(items_.front());
items_.pop_front();
return value;
}
private:
std::mutex mutex_;
std::condition_variable ready_;
std::deque<T> items_;
bool closed_ = false;
};
What the producer sends is the document and the address of its image before the push, so that the consumer can show the image did not move:
// What the producer sends: a document, and the address of its image before the push. Moving a
// document moves its handle and never the bytes of its image, so the consumer finds them there.
struct parcel {
wd::document doc;
const std::byte* image = nullptr;
};
// What the consumer keeps: the documents in arrival order, how many of them still have their image
// where the producer left it, and how many texts failed to parse.
struct exchanged {
std::vector<wd::document> documents;
std::size_t same_image = 0;
std::size_t failed = 0;
};
// The producer parses each text and pushes the document; the consumer keeps each one it receives.
// The two threads write different members of the result, and main reads it after both are joined.
exchanged through_a_queue(std::array<std::string_view, 3> texts) {
queue<parcel> arrivals;
exchanged result;
std::thread consumer([&arrivals, &result] {
while (std::optional<parcel> next = arrivals.pop()) {
if (next->doc.image().data() == next->image) {
++result.same_image;
}
result.documents.push_back(std::move(next->doc));
}
});
std::thread producer([&arrivals, &result, texts] {
for (const std::string_view text : texts) {
wd::result<wd::document> parsed = wd::parse(text);
if (parsed.has_error()) {
++result.failed;
continue;
}
const std::byte* image = parsed->image().data();
arrivals.push(parcel{.doc = std::move(*parsed), .image = image});
}
arrivals.close();
});
producer.join();
consumer.join();
return result;
}
queue: 3 received, 3 with the image the producer sent, 0 failed to parse
{"id":1,"name":"Ada"}
[1,2,3]
{"id":3,"tags":["queue","thread"]}
Every document arrived with the image the producer sent: no byte was copied and nothing was parsed on the consumer’s side.
Shared documents
Several threads that read one document share it with wd::shared_document<>, which is
std::shared_ptr for a document: copies share one image, a count says how many hold it, and the last
holder to let go gives the image back. The images live in the blocks of a wd::shared_pool, over a
region of memory the program gives, the process heap or a segment of shared memory. A pool is
formatted once, wd::shared_pool::format(region, block_bytes), with a 16-aligned region; its
region_bytes(block_bytes, blocks) sizes the region for a number of blocks, blocks(),
block_bytes() and available() describe it, and wd::shared_pool::attach(region) is how another
view of the same region, in this thread or another process, joins it.
A block is a header of 64 bytes, which holds the count of references to its image, and the room of one image. The pool works through offsets from the region’s start and never addresses, its counts are lock-free atomics and its free blocks a lock-free stack: no lock is held at any moment, so no thread or process waits on another, and one that dies leaves no lock behind. A block that is free is all zero, and a block given back is zeroed.
shared_document<>::from_image(pool, bytes) copies an image into a free block, create(pool) makes a
new empty document in one, and use_count() says how many hold the document, in every process.
Copying a shared_document adds a reference, destroying one drops it, and a reader takes a
const reference and never writes. A reader holds its copy until it returns:
// The name and birth year of a shared document, read through a const reference: a reader never
// writes.
std::string describe(const wd::shared_document<>& doc) {
return std::string(doc["name"].value_or(std::string_view("nobody"))) + " was born in " +
std::to_string(doc["born"].value_or(std::int64_t{0}));
}
// A reader holds its copy until it returns, then drops it in its own thread.
void reader_task(const wd::shared_document<>& holder, std::string& out) {
out = describe(holder);
}
The image of a parsed document keeps the room the parser gave it, 4096 bytes at first, and a block of 2048 would refuse it, so the example shrinks the image to its content before it copies it into the pool (Shrinking). Four readers then share the document, and the count is the number of holders:
// The image is copied into a block of the pool; the document it makes holds the one reference.
wd::result<wd::shared_document<>> stored =
wd::shared_document<>::from_image(pool, parsed->image());
if (stored.has_error()) {
std::cout << "error: " << stored.error().message() << '\n';
return 1;
}
wd::shared_document<> shared = std::move(*stored);
std::cout << "stored: use_count " << shared.use_count() << ", " << pool.available() << " of "
<< pool.blocks() << " blocks free\n";
// Each reader gets its copy here, before any thread starts, so the count printed is exact.
constexpr std::size_t reader_count = 4;
std::vector<wd::shared_document<>> copies(reader_count, shared);
std::cout << "with " << reader_count << " readers: use_count " << shared.use_count() << '\n';
std::array<std::string, reader_count> read{};
std::vector<std::thread> readers;
readers.reserve(reader_count);
for (std::size_t i = 0; i < reader_count; ++i) {
readers.emplace_back(reader_task, std::move(copies[i]), std::ref(read[i]));
}
for (std::thread& reader : readers) {
reader.join();
}
std::cout << "after the readers: use_count " << shared.use_count() << ", " << pool.available()
<< " of " << pool.blocks() << " blocks free\n";
for (std::size_t i = 0; i < reader_count; ++i) {
std::cout << "reader " << i << ": " << read[i] << '\n';
}
pool: 7 blocks of 2048 bytes stored: use_count 1, 6 of 7 blocks free with 4 readers: use_count 5 after the readers: use_count 1, 6 of 7 blocks free reader 0: Ada was born in 1815 reader 1: Ada was born in 1815 reader 2: Ada was born in 1815 reader 3: Ada was born in 1815
Copy on write
A holder that wants to write calls make_mutable(). When it is the only holder, the document is its
own and nothing happens. When others hold the image, make_mutable copies it into a free block of
its own, the copy including the seal, and the holder changes the copy while the others keep the
document as it was. It is errc::capacity when every block is leased.
The library does not synchronise writers. A block that has several writers, or a writer and readers
of that same block, must wait for each other by the program’s own means, such as a mutex; the
writer below changes only the copy that make_mutable made, which no other holder reads:
// make_mutable copies the image into a block of the writer's own when other holders exist, so the
// writer changes its copy and the others keep the document as it was. The library does not
// synchronise writers: a block with several writers, or a writer and readers of that same block,
// must wait for each other by the program's own means, such as a mutex. This writer changes only
// its copy, which no other holder reads.
void writer_task(wd::shared_document<> holder, std::string& out) {
if (holder.make_mutable().has_error() || holder["name"].set("Lovelace").has_error()) {
out = "error";
return;
}
out = describe(holder);
}
// The writer takes its reference before its thread starts; its change leaves the shared
// document, which main still reads, as it was.
wd::shared_document<> writer_copy = shared;
std::string written;
std::thread writer(writer_task, std::move(writer_copy), std::ref(written));
writer.join();
std::cout << "writer: " << written << '\n';
std::cout << "after the writer: " << describe(shared) << ", use_count " << shared.use_count()
<< ", " << pool.available() << " of " << pool.blocks() << " blocks free\n";
// The last copy gives its block back to the pool, zeroed.
shared = wd::shared_document<>();
std::cout << "last copy dropped: " << pool.available() << " of " << pool.blocks()
<< " blocks free\n";
writer: Lovelace was born in 1815 after the writer: Ada was born in 1815, use_count 1, 6 of 7 blocks free last copy dropped: 7 of 7 blocks free
The change was made to the writer’s copy, so the document that main holds still reads Ada, and the last holder
to let go returned its block to the pool.
Processes
A pool in a segment of shared memory is shared by processes, each of which maps the segment at an address of its own, and a document is passed between them by the offset of its image. The program below is its own consumer: the producer formats a pool of eight blocks of 4096 bytes in Boost.Interprocess’s shared memory, parses or builds four documents in blocks of it, seals each, and sends the offsets through a message queue, and then runs itself again as the consumer, which attaches to the same segment. The constants say how many blocks and how large, and the end of the stream is an offset no image has:
// The room of each block's image, and the blocks of the pool: 8 blocks of 4096 bytes, the size of
// a parsed document's image.
constexpr std::size_t block_bytes = 4096;
constexpr std::size_t blocks = 8;
// The queue holds every offset at once, the end of the stream included.
constexpr std::size_t queue_messages = 8;
// The end of the stream: no document's image is at offset 0.
constexpr std::uint64_t end_of_stream = 0;
The producer makes each document in a block of the pool. release() gives up the producer’s
reference without dropping it, and returns the offset of the image, which is the document now as far
as the consumer is concerned:
// The document a JSON text makes: parsed, its image copied into a block of the pool, then sealed.
wd::result<wd::shared_document<>> sealed_from_text(wd::shared_pool pool, std::string_view text) {
wd::result<wd::document> parsed = wd::parse(text);
if (parsed.has_error()) {
return parsed.error();
}
wd::result<wd::shared_document<>> made =
wd::shared_document<>::from_image(pool, parsed->image());
if (made.has_error()) {
return made.error();
}
if (made->seal().has_error()) {
return wd::errc::type;
}
return made;
}
// Releases a document's reference and sends its offset, the document now the consumer's.
void hand_over(bip::message_queue& queue, wd::shared_document<>& doc) {
const std::uint64_t offset = doc.release();
queue.send(&offset, sizeof offset, 0);
}
// The producer: it makes the documents, hands each over and runs the consumer, which prints them.
int produce(const char* program) {
const names run = names_for(std::to_string(bip::ipcdetail::get_current_process_id()));
remove_names(run);
const remover cleanup(run);
const wd::result<std::size_t> size = wd::shared_pool::region_bytes(block_bytes, blocks);
if (size.has_error()) {
std::cout << "error: " << size.error().message() << '\n';
return 1;
}
bip::shared_memory_object memory(bip::create_only, run.segment.c_str(), bip::read_write);
memory.truncate(static_cast<bip::offset_t>(*size));
const bip::mapped_region mapped(memory, bip::read_write);
const std::span<std::byte> bytes(static_cast<std::byte*>(mapped.get_address()),
mapped.get_size());
const wd::result<wd::shared_pool> formatted = wd::shared_pool::format(bytes, block_bytes);
if (formatted.has_error()) {
std::cout << "error: " << formatted.error().message() << '\n';
return 1;
}
const wd::shared_pool pool = *formatted;
bip::message_queue queue(bip::create_only, run.queue.c_str(), queue_messages,
sizeof(std::uint64_t));
// Three documents parsed or built in place, and a fourth whose sealed image is then damaged.
wd::result<wd::shared_document<>> ada =
sealed_from_text(pool, R"({"name":"Ada","born":1815,"langs":["Analytical"]})");
wd::result<wd::shared_document<>> grace =
sealed_from_text(pool, R"({"name":"Grace","langs":["COBOL"]})");
wd::result<wd::shared_document<>> alan = wd::shared_document<>::create(pool);
wd::result<wd::shared_document<>> edsger = sealed_from_text(pool, R"({"name":"Edsger"})");
if (ada.has_error() || grace.has_error() || alan.has_error() || edsger.has_error()) {
std::cout << "error: a document could not be made\n";
return 1;
}
if (alan->root().set(wd::object{{"name", "Alan"}, {"n", 1500}}).has_error() ||
alan->seal().has_error()) {
std::cout << "error: a document could not be made\n";
return 1;
}
// One byte of the sealed image changes after the seal; any change breaks a seal.
constexpr std::size_t damaged_byte = 100;
pool.region()[edsger->offset() + damaged_byte] ^= std::byte{0xff};
hand_over(queue, *ada);
hand_over(queue, *grace);
hand_over(queue, *alan);
hand_over(queue, *edsger);
queue.send(&end_of_stream, sizeof end_of_stream, 0);
std::cout << "available before the consumer: " << pool.available() << '\n' << std::flush;
// The consumer is this program run again, as Boost.Interprocess's own tests run theirs; the
// parent waits for it, so its output comes before the parent's last line.
const std::string command =
"\"" + std::string(program) + "\" consumer " + run.segment + " " + run.queue;
// NOLINTNEXTLINE(bugprone-command-processor,cert-env33-c)
if (std::system(command.c_str()) != 0) {
std::cout << "error: the consumer failed\n";
return 1;
}
std::cout << "available after the consumer: " << pool.available() << '\n';
return 0;
}
The consumer attaches to the segment, and for each offset it receives, adopt takes over the
reference release gave up, opens the image in place, and checks its seal. The document is read
where it is and dropped, which returns its block to the pool. One image of the example was damaged
after it was sealed: adopt refuses it with errc::damaged_document, and the reference it
was handed is still counted, so the consumer gives it back by offset with pool.drop(offset), which
reads nothing of the image and returns the block:
// The consumer: it adopts each offset the producer sends, prints the document and drops it, so
// that its block returns to the pool; an offset the seal refuses is dropped by the pool, unread.
int consume(const char* segment, const char* queue_name) {
const bip::shared_memory_object memory(bip::open_only, segment, bip::read_write);
const bip::mapped_region mapped(memory, bip::read_write);
const std::span<std::byte> bytes(static_cast<std::byte*>(mapped.get_address()),
mapped.get_size());
const wd::result<wd::shared_pool> attached = wd::shared_pool::attach(bytes);
if (attached.has_error()) {
std::cout << "error: " << attached.error().message() << '\n';
return 1;
}
const wd::shared_pool pool = *attached;
bip::message_queue queue(bip::open_only, queue_name);
for (;;) {
std::uint64_t offset = 0;
std::size_t received = 0;
unsigned int priority = 0;
queue.receive(&offset, sizeof offset, received, priority);
if (offset == end_of_stream) {
return 0;
}
wd::result<wd::shared_document<>> adopted = wd::shared_document<>::adopt(pool, offset);
if (adopted.has_value()) {
std::cout << adopted->get().stringify() << ": use count " << adopted->use_count()
<< '\n';
} else {
std::cout << "refused: " << adopted.error().message() << '\n';
// A refused reference stays counted until it is given back: the pool drops it by its
// offset, without reading the image, and the block returns to the pool.
if (const wd::result<void> dropped = pool.drop(offset); dropped.has_error()) {
std::cout << "error: " << dropped.error().message() << '\n';
return 1;
}
}
}
}
available before the consumer: 4
{"name":"Ada","born":1815,"langs":["Analytical"]}: use count 1
{"name":"Grace","langs":["COBOL"]}: use count 1
{"name":"Alan","n":1500}: use count 1
refused: a sealed image whose bytes do not match its seal
available after the consumer: 8
Before the consumer ran, the pool had four free blocks of the eight, the four documents’ blocks being leased; after it, all eight are free. The reference count of each document read was one, the consumer’s.
A process that dies holding references leaves their blocks counted for good, as
Boost.Interprocess’s own shared_ptr does; recovering them is the program’s. Images in shared memory
are trusted input, as every image is (Sealed images): the processes that map a pool trust
one another.
Compared with Boost.JSON
Boost.JSON is the JSON library of Boost, and document reads JSON with its parser. Its document,
boost::json::value, is built for speed, and document is built to behave as JavaScript does and to
move as bytes: the two make different choices, and this section puts them side by side. Each part of
the program below runs the same JSON through both libraries and prints both outputs. The line of
document is what JavaScript gives where the part says nothing else, and the line of Boost.JSON is
what that library gives. It uses Boost.JSON by <boost/json.hpp>, beside document’s header:
#include <webcpp/document.hpp>
#include <boost/json.hpp>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <span>
#include <string>
#include <string_view>
namespace wd = webcpp::document;
namespace json = boost::json;
/** Prints the document's line of a section, under the label that names it. */
void print_document(std::string_view text) {
std::cout << " document: " << text << '\n';
}
/** Prints the Boost.JSON line of a section, under the label that names it. */
void print_boost(std::string_view text) {
std::cout << " Boost.JSON: " << text << '\n';
}
/** The text Boost.JSON writes for `text` once parsed, or the error it reports without throwing. */
std::string boost_text(std::string_view text) {
boost::system::error_code ec;
const json::value v = json::parse(text, ec);
return ec ? ec.message() : json::serialize(v);
}
Key order
boost::json::object keeps the keys in the order they came. JavaScript puts the keys that are array
indices first, ascending, then the others in the order they were created, and document does the
same (Key order):
constexpr std::string_view text = R"({"b":1,"2":2,"a":3,"1":4})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
return false;
}
std::cout << "1. Key order\n";
print_document(made->stringify());
print_boost(boost_text(text));
1. Key order
document: {"1":4,"2":2,"b":1,"a":3}
Boost.JSON: {"b":1,"2":2,"a":3,"1":4}
A key that is repeated keeps the last value, in the place of the first, as JavaScript does:
constexpr std::string_view text = R"({"b":1,"2":2,"b":3,"1":4})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
return false;
}
std::cout << "6. Repeated keys\n";
print_document(made->stringify());
print_boost(boost_text(text));
6. Repeated keys
document: {"1":4,"2":2,"b":3}
Boost.JSON: {"b":3,"2":2,"1":4}
Erasing keeps the order
boost::json::object::erase copies the last member into the gap, so d comes before b and c.
JavaScript’s delete keeps the order of the others, and so does document’s erase:
constexpr std::string_view text = R"({"a":1,"b":2,"c":3,"d":4})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error() || !made->root().erase("a")) {
return false;
}
std::cout << "2. Erase keeps the order of the other members\n";
print_document(made->stringify());
// Boost.JSON's erase copies the last member into the gap, so d comes before b and c.
boost::system::error_code ec;
json::value v = json::parse(text, ec);
json::object* object = v.if_object();
if (ec || object == nullptr) {
return false;
}
object->erase("a");
print_boost(json::serialize(v));
2. Erase keeps the order of the other members
document: {"b":2,"c":3,"d":4}
Boost.JSON: {"d":4,"b":2,"c":3}
Exact numbers
Boost.JSON stores a number as a double when it is not an integer, and writes it back as 1E-1.
document keeps the digits that were sent, and writes them as JavaScript writes a number
(Numbers):
constexpr std::string_view text =
R"([0.1000000000000000000000000000000001,12345678901234567890123])";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
return false;
}
// The digits are the ones read; 23 of them are written in JavaScript's exponent form.
std::cout << "3. Exact numbers\n";
print_document(made->stringify());
print_boost(boost_text(text));
3. Exact numbers document: [0.1000000000000000000000000000000001,1.2345678901234567890123e+22] Boost.JSON: [1E-1,1.2345678901234568E22]
Array methods
boost::json::array has insert and erase, which make the same edit as splice and unshift but
return no removed values. document has the array methods of JavaScript (Arrays):
wd::result<wd::document> made = wd::parse("[1,2,3,4]");
if (made.has_error()) {
return false;
}
wd::value a = made->root();
const wd::result<std::uint64_t> length = a.unshift(0);
// splice removes two elements from index 1, returns them, and inserts "x" in their place.
const wd::result<wd::document> removed = a.splice(1, 2, "x");
if (length.has_error() || removed.has_error()) {
return false;
}
std::cout << "4. Array methods\n";
print_document("unshift(0) -> " + std::to_string(*length) + "; splice(1, 2, \"x\") removes " +
removed->stringify() + "; a = " + a.stringify());
// Boost.JSON's array has insert and erase, which make the same edit but return no removed
// values.
json::array arr{1, 2, 3, 4};
arr.insert(arr.begin(), 0);
arr.erase(arr.begin() + 1, arr.begin() + 3);
arr.insert(arr.begin() + 1, json::value("x"));
print_boost(
"no splice or unshift; insert and erase make the edit, without the removed "
"values: a = " +
json::serialize(arr));
4. Array methods document: unshift(0) -> 5; splice(1, 2, "x") removes [1,2]; a = [0,"x",3,4] Boost.JSON: no splice or unshift; insert and erase make the edit, without the removed values: a = [0,"x",3,4]
An image copied and opened in place
A boost::json::value points into its memory resource, so its bytes cannot be copied to another
buffer: the only way to move it is to serialize it and parse it again. document’s image is the
document, and a copy of its bytes in a buffer of the program’s own, opened there, is the same
document with no parse (Images):
constexpr std::string_view text = R"({"user":"Ada","langs":["Analytical","Note G"]})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
return false;
}
const std::span<const std::byte> image = made->image();
// A buffer of the program's, 16-aligned, large enough for the image.
alignas(16) std::array<std::byte, 8192> buffer{};
if (image.size() > buffer.size()) {
return false;
}
std::ranges::copy(image, buffer.begin());
// Opened in place: the buffer becomes the document, with no parse.
wd::result<wd::basic_document<wd::default_traits, wd::external>> opened =
wd::open(std::span<std::byte>(buffer.data(), image.size()));
if (opened.has_error()) {
return false;
}
std::cout << "5. An image copied to another buffer and opened in place\n";
print_document(opened->stringify());
// A Boost.JSON value points into its memory resource, so its bytes cannot be copied: the
// only way to move it is to serialize it and parse it again.
print_boost(boost_text(made->stringify()));
5. An image copied to another buffer and opened in place
document: {"user":"Ada","langs":["Analytical","Note G"]}
Boost.JSON: {"user":"Ada","langs":["Analytical","Note G"]}
Errors are values
document’s operations return a result, neither a bad parse nor a bad write throws, and the core
never throws at all. Boost.JSON’s parse reports through an error_code when it is given one, but its
accessors, at and as_string among them, throw (Errors):
// The document's operations return result; neither a bad parse nor a bad write throws.
const wd::result<wd::document> broken = wd::parse("[1,");
wd::result<wd::document> number = wd::parse("5");
if (broken.has_value() || number.has_error()) {
return false;
}
const wd::result<std::uint64_t> pushed = number->root().push(1);
std::cout << "7. Errors are values\n";
print_document("parse(\"[1,\") -> " + broken.error().message() + "; push on a number -> " +
pushed.error().message());
// Boost.JSON's parse reports through error_code, but its value accessors (at, as_string)
// throw, so this section stays with the error_code overload.
boost::system::error_code ec;
static_cast<void>(json::parse("[1,", ec));
print_boost("parse(\"[1,\", ec) -> " + ec.message());
7. Errors are values
document: parse("[1,") -> malformed JSON text; push on a number -> a value of another kind
Boost.JSON: parse("[1,", ec) -> incomplete JSON
What Boost.JSON does better
document is not a fast JSON tree. Boost.JSON parses faster, stringifies faster, finds a key faster (it hashes, where document walks an ordered tree) and splices an array faster, and its values are smaller than document’s images: a node of an image takes 192 bytes whatever it holds (Memory per value). The benchmarks measure each of these against Boost.JSON and against JavaScript (Benchmarks, and Reading the results for where document is slower and why). The reasons to choose document are the ones above, and the ones that no parser has: an image that is sent as it is, opened in place and shared between threads and processes, which Images on the wire and Sharing teach.
The sugar that throws
document has two layers. The core, every function this page has shown so far, never throws and
exists in every build: each operation that can fail returns a result. The sugar reads as
JavaScript reads, and throws as JavaScript throws, so it exists only in a build with exceptions:
| Sugar | Its core function |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Each member is its core function, then, on failure, the exception of the core’s error: errc::syntax
throws wd::syntax_error, which also holds the byte offset; errc::type throws wd::type_error,
errc::range wd::range_error, errc::capacity wd::capacity_error, and
errc::not_a_document and errc::incompatible_document wd::image_error. Each derives from
wd::error, which derives from std::runtime_error and holds the code (Errors). A
member that throws has changed nothing, since its core function completes or changes nothing.
wd::document doc = wd::parse_or_throw(R"({"n":1,"user":{"name":"Ada"}})");
doc["n"] = 2; // doc.n = 2
doc["user"]["nick"] = "Countess"; // creates the member
const auto n = doc["n"].as<std::int64_t>();
const std::string_view name = doc["user"]["name"].as_string();
std::cout << n << ' ' << name << '\n';
// Assignment writes at the handle's place, a deep copy, and never rebinds the handle.
wd::value place = doc["n"];
place = doc["user"]["name"];
show("doc.n", doc["n"]);
doc["user"]["nick"] = wd::undefined; // erases the member
show("doc.user", doc["user"]);
try {
doc["n"]["x"]["y"] = 1; // a write through nothing
} catch (const wd::type_error& e) {
std::cout << "TypeError: " << e.what() << '\n';
}
try {
static_cast<void>(doc["user"].as<bool>());
} catch (const wd::type_error& e) {
std::cout << "TypeError: " << e.what() << '\n';
}
try {
static_cast<void>(wd::parse_or_throw("[1,]"));
} catch (const wd::syntax_error& e) {
std::cout << "SyntaxError: " << e.what() << " at " << e.offset() << '\n';
}
const wd::document empty; // holds null
wd::document twin = doc; // an equal and independent copy
twin["n"] = 3;
std::cout << empty.stringify() << ' ' << doc.stringify() << ' ' << twin.stringify() << '\n';
2 Ada
doc.n -> "Ada"
doc.user -> {"name":"Ada"}
TypeError: a value of another kind
TypeError: a value of another kind
SyntaxError: malformed JSON text at 3
null {"n":"Ada","user":{"name":"Ada"}} {"n":3,"user":{"name":"Ada"}}
Its rules
-
Assignment writes; it never rebinds.
h = xstoresxat the placehnames, andhstill names that place afterward.h = other, withothera handle, stores a deep copy of the valueothernames, asdoc["a"] = doc["b"]does: never an alias (Copies, never aliases). -
= wd::undefinederases a member, asset(wd::undefined)does. -
as_string()reads UTF-8 strings only: it returns astd::string_view, which a UTF-16 string cannot give. Underjs_traits, a program usesas<std::u16string_view>()oras<std::string>(). -
wd::document d;holdsnull, and the copy constructor gives an equal and independent copy. Both are for owned documents only: an external document has no buffer to be made or copied into. -
An external document throws
wd::capacity_errorwhen it is full, wheresetwould returnerrc::capacity.
Without exceptions
In a build without exceptions, the sugar is not there: each member carries the constraint
detail::exceptions_enabled, which no such build satisfies, so a call does not compile, and the
compiler’s note names the constraint. For as<std::int32_t>(), Clang writes "no matching member
function for call to 'as'", with the note "because 'int' does not satisfy 'exceptions_enabled'",
and the test no_exceptions holds what it writes for each member (Testing). A
generic program can ask whether the sugar is there:
/** Whether a handle of type `H` has the sugar `as<T>()`, which throws. */
template <class H>
concept has_as = requires(const H& h) { h.template as<std::int64_t>(); };
/** Whether a handle of type `H` can be assigned a value, `h = 1`, which throws on failure. */
template <class H>
concept assignable = requires(H h) { h = 1; };
static_assert(!has_as<wd::const_value>, "as<T>() needs exceptions");
static_assert(!assignable<wd::value>, "h = x needs exceptions");
static_assert(!std::is_default_constructible_v<wd::document>, "wd::document d; needs exceptions");
static_assert(!std::is_copy_constructible_v<wd::document>, "copying a document needs exceptions");
The core is the same in both builds, and an example of it is in Errors. A build has
no exceptions when webcpp has none, or when the program defines WEBCPP_DOCUMENT_NO_EXCEPTIONS
for document alone (Without exceptions).
Errors
Every operation of the core that can fail returns a wd::result<T>, Boost.System’s result: the
value, or a boost::system::error_code. document’s own codes are the enumerators of wd::errc, of
the category webcpp.document, whose values are fixed and never reused, so that a stored code
keeps its meaning across versions. The error a sink or a selector’s callback returns is passed on
unchanged, in its own category.
errc |
When | JavaScript | Exception, with exceptions only |
|---|---|---|---|
|
malformed JSON text, with Boost.JSON’s code as the parser’s |
|
|
|
a value of another kind; a write through nothing; text invalid in the encoding; a malformed selector; a method of arrays on anything but an array |
|
|
|
a number that does not fit or is not exact; a date out of JavaScript’s range |
|
|
|
no room: a full external buffer, an owned document at its |
none; |
|
|
bytes that are not an image |
none; |
|
|
an image of another format version, reserved bytes, layout, byte order or traits |
none; |
|
|
a sealed image whose bytes do not match its seal: changed, torn or cut short (Sealed images) |
none; |
|
The JavaScript column is the error JavaScript itself throws for the same fault; the module of
Errors throws a SyntaxError, a RangeError or a TypeError for every code.
An errc converts to an error_code and compares with one, r.error() == wd::errc::type, and
wd::error_category() is the category. The program below is built without exceptions, which
example/Jamfile asks for it whatever the build asks, and makes each error once:
/** Prints what failed, its code's value and category, and its message. */
void print(std::string_view what, boost::system::error_code e) {
std::cout << what << ": " << e.category().name() << ':' << e.value() << ", " << e.message()
<< '\n';
}
// errc::syntax: malformed text, and the parser's offset where it was found.
wd::parser p;
const wd::result<void> written = p.write("[1,]");
print("[1,]", written.error());
std::cout << " at byte " << p.offset() << ", Boost.JSON's cause: " << p.cause().message()
<< '\n';
wd::result<wd::document> made = wd::parse(R"({"n":300})");
if (made.has_error()) {
return 1;
}
wd::document& doc = *made;
// errc::type: a write through nothing, or a read of another kind.
print("doc.n.x.y = 1", doc["n"]["x"]["y"].set(1).error());
print("doc.n as a string", doc["n"].get<std::string>().error());
// errc::range: a number that does not fit, or a date out of JavaScript's range.
print("doc.n as int8_t", doc["n"].get<std::int8_t>().error());
// errc::capacity: a limit, here of depth.
wd::parse_options shallow;
shallow.max_depth = 1;
print("[[1]], max_depth 1", wd::parse("[[1]]", shallow).error());
// errc::not_a_document and errc::incompatible_document: bytes that are no image of these
// traits.
print("64 zero bytes", wd::from_image(std::array<std::byte, 64>{}).error());
print("another traits' image", wd::from_image<wd::js_traits>(doc.image()).error());
[1,]: webcpp.document:1, malformed JSON text at byte 3, Boost.JSON's cause: syntax error doc.n.x.y = 1: webcpp.document:2, a value of another kind doc.n as a string: webcpp.document:2, a value of another kind doc.n as int8_t: webcpp.document:3, a number or index out of range [[1]], max_depth 1: webcpp.document:4, no room left 64 zero bytes: webcpp.document:5, not a document's image another traits' image: webcpp.document:6, an image of another format, layout, byte order or traits
An operation that fails changes nothing, so the program goes on with the document as it was:
// An error compares with an errc, and the document is as it was before the call that failed.
if (const wd::result<void> set = doc["n"]["x"].set(1); set.error() == wd::errc::type) {
std::cout << "still " << doc.stringify() << '\n';
}
still {"n":300}
A failure of the process heap is not an error value. The image’s heap is never exhausted by
surprise, since every operation finds its room first, but a few things live on the process heap:
the text stringify returns, the key a handle of a missing member holds, and the parser’s frames.
The standard library’s std::bad_alloc there is thrown with exceptions and ends the program
without them, as it does for any C++ container.
Testing
A test of a program that uses document compares values: a document with ==, which compares
structurally, or the text stringify writes, which is what JSON.stringify writes, byte for byte, so that an
expected text can be measured with Node. Every failure is a value, which a test compares with an
errc; nothing depends on a clock, a thread or global state; and the same operations give the
same image, so that two runs, or two targets, can be compared byte for byte.
How document is tested
document’s tests are lightweight_test programs, one per subject, under test/, each declared in
test/Jamfile with webcpp’s webcpp.run. The JavaScript facts they hold are Node 26’s:
| Test | What it checks |
|---|---|
|
The values of |
|
Numbers read from JSON text exactly, at the edges of their text and of |
|
Array-index keys; dates as |
|
The image’s heap: strings of exact size zeroed when freed, an allocator that hands out and frees zeros, probes that leave the heap as it was, regions owned and external, grown, copied and shrunk, the same bytes at any address, and the header’s check. |
|
The selectors’ grammar and matcher, apart from any parser: which texts parse into which steps, which are refused, and which paths each selector matches. |
|
The tree under each object policy, keys that look like array indices, elements renumbered, deep copies and literals, the header it keeps, and every operation, which completes or changes nothing, in buffers of every size and in owned images that grow. |
|
|
|
Every settable type stored by value, and every node read back as every readable type, with their type and range failures; text validated and transcoded; dates, bytes, and structural equality. |
|
The handles: navigation that never fails, |
|
The key order through the handles, with keys that look like array indices and one that holds a zero byte, under each object policy and both encodings, sorted keys by UTF-16 code units with either: assignment keeps a key’s place, and a key erased and created again takes a new key’s. |
|
Images copied to another address and opened there, formatted, refused, the same for the same operations, no byte left of an erased secret, shrunk, a full external buffer, a buffer that is not 16-aligned, and a header whose container offset lies. |
|
|
|
What |
|
Every text of the corpus split at every byte, and written one byte at a time, gives the document
a whole parse gives; a syntax error’s offset is the same however the text is split; a failed
parser stays failed and a finished one takes no more text, nor a second text after its first;
|
|
Deliveries by |
|
Streams of documents: each framing on its own rows, NDJSON’s rules one by one, indented records
and bad ones in JSON text sequences, errors skipped with their lines or stopping the stream until
a reset, the limits held per document, and every stream split at every byte and written one byte
at a time giving what one write gives, through |
|
Natively only: |
|
Each array method against Node’s results, with JavaScript’s index rules, on empty arrays and on anything but an array; a handle kept while pushes grow the image; and a splice into external buffers of every size, which completes or changes nothing. |
|
Built with exceptions: assignment stores as |
|
Built without exceptions, a test of the compiler’s diagnostics: each member of the sugar called
once does not compile, and the note names |
|
Node as the oracle, from C++: every case of the corpus parsed, changed by its operations
through the core and written, under |
Each case is a function named after the rule it checks, such as handle_survives_growth, and
main calls them all and returns boost::report_errors(). Every test is built and passes without
exceptions too, but those that declare <exception-handling>on. Beside them, test/Jamfile
compiles each public header alone, as the only include of its translation unit, so that a header
that leans on an include it does not make fails, naming it. And example/Jamfile builds every
program this page shows, runs it, and compares what it printed with the .expected beside it; a
program that returns anything but 0 fails too.
Node as the oracle
test/oracle/ runs the same cases in Node and in C++. A case, test/fixtures/cases/<name>.json,
is a JSON text, an indent, and a list of operations: set, erase, push, pop, shift,
unshift, splice, reverse, the length set and Object.assign, each value written as JSON
text, so that neither side reads a number through a double. oracle.mjs runs each case with
JSON.parse, the operations in JavaScript and JSON.stringify, and writes what it gets to
test/fixtures/expected/<name>.json, which is committed; the test oracle_cases runs each case
through the document and must give those bytes. The cases written by hand follow the rules of this
page, key order, numbers, arrays, strings, dates, bytes and indentation; the others are random
documents and operations, which generate.mjs writes from a fixed seed. A case of numbers a double
does not keep runs with js_traits alone; every other runs with both traits.
No case holds what the two sides do differently by design (Where document differs, on purpose):
a lone surrogate, a number past a double’s range or past 34 digits, an operation JavaScript would
throw on, or the key __proto__, which sets a prototype in JavaScript.
Without exceptions
That document works without exceptions is checked by the lint, which compiles every public header,
and every test and example, which instantiate its templates, with exception-handling=off.
<webcpp/document/config.hpp>, which every public header of document includes first, defines
WEBCPP_DOCUMENT_NO_EXCEPTIONS when webcpp has no exceptions (WEBCPP_HAS_EXCEPTIONS is not
defined: the compiler has none, or the program defined WEBCPP_NO_EXCEPTIONS), and a program may
define it for document alone, as Boost.Asio’s BOOST_ASIO_NO_EXCEPTIONS does. The core never
throws, so the macro removes only the sugar that throws and the exception types
(The sugar that throws). The tests config, config_developer_defined and config_all_off
check the three ways it is defined, and the lint refuses a throw in a public header: an
exception goes through boost::throw_exception.
Targets
test/Jamfile and example/Jamfile declare the targets they are built for, webcpp.targets
native emscripten wasip2 wasip3, and b2 builds each program for those alone. From the root of a
webcpp checkout, each of these builds and runs:
b2 libs/document/test-
The 63 tests natively: the lightweight_test programs, and each of the 28 public headers compiled alone.
stream_asio, andasio.hppcompiled alone, are built natively only, as Boost.Asio is (With Boost.Asio). b2 libs/document/example-
The 20 examples, each built, run, and its output compared with the one beside it.
b2 libs/document/test libs/document/example toolset=emscripten-
The same on emscripten: 65 tests and 13 examples, each run by node, b2’s toolset itself, except the 6 driven programs, the tests of the JavaScript API and the example of Fed from JavaScript, which the own lane
jsruns. b2 libs/document/test/js//js toolset=emscripten-
The own lane
js(Using it from JavaScript): Node’s test runner on the module and on the module of shared documents, the npm package as a user receives it, and the two examples in headless Chromium, and Node over the example that binds a stream parser (example/document_stream_js.cpp). Each test builds the module it checks, so the lint and the CI compile it. b2 libs/document/test libs/document/example toolset=clang-wasip2 testing.launcher=wasmtime-
The same for wasm32-wasip2: 59 tests and 14 examples, each run by wasmtime.
toolset=clang-wasip3does the same for wasm32-wasip3: 59 tests and 14 examples. b2 libs/document/test/oracle//oracle-
The oracle lane: Node runs every case, and what it writes must be what
test/fixtures/expectedholds. It needs Node, and no package. b2 libs/document/test/oracle//update-expected-
Not a check: it writes Node’s results again, over the committed ones, after a case is added or changed.
b2 compares timestamps, so a result that counts is built from scratch, with b2 -a.
b2 libs/document/test//value builds and runs one test alone, while working on it.
The test matrix
The continuous integration runs document’s tests and examples in lanes, one toolset each: the native compilers, emscripten, wasip2 and wasip3. Each lane builds from scratch and records what b2 built and ran:
b2 -a --dump-tests --out-xml=<lane>.xml toolset=<toolset> libs/document/test libs/document/example
webcpp’s tools/report/report.py merges the files of every lane into a matrix, published with the
documentation at https://webcpporg.github.io/report/.
Benchmarks
document does what JavaScript does, and does it in C++: this section measures what that costs. Each operation is timed three ways: with Boost.JSON, the C++ library whose parser document uses, with document itself, and with JavaScript’s own objects in V8, the engine whose behaviour document reproduces. The C++ runs natively, and compiled to WebAssembly with Emscripten in the same Node.js and the same headless Chrome as the JavaScript, so that each row compares the three where a web application would run them.
The operations of the survey and the data come from recognised benchmarks, never from document’s own
tests; the operations of binary transmission, which no suite times, come from the case document is
built for, and are the first results below. Every implementation must give the same answer to each
before it is timed. One C++ program,
bench/bench.cpp, holds the Boost.JSON and document sides; one JavaScript module, bench/bench.mjs,
holds JavaScript’s and the same harness, which Node runs and Chrome runs in a page; and
bench/run.mjs runs them all, checks their answers and writes the results below.
The operations
The fourteen operations of the survey of JSON and dictionary benchmarks that this section follows,
each with the suites it is taken from. No recognised JSON suite times changes to a document (insert,
update, delete, splice) or deep copies: those come from the hash-map suites, V8’s own js-perf-test
and Dromaeo’s DOM tests. Equality, the survey’s fifteenth, is left out, since no suite times it.
Three more, 15 to 17, are binary transmission: a document sent to another place and opened there.
No suite times them, so the page takes them from the case the image is built for.
| # | Operation | Taken from | Boost.JSON | document | JavaScript |
|---|---|---|---|---|---|
1 |
Parse |
nativejson-benchmark, Boost.JSON’s bench, serde’s json-benchmark, nlohmann’s benchmarks, JetStream 2.1 |
|
|
|
2 |
Stringify, compact |
nativejson-benchmark, Boost.JSON’s bench, nlohmann, simdjson, JetStream 2.1 |
|
|
|
3 |
Prettify, indent 4 |
nativejson-benchmark, nlohmann, simdjson |
* by hand over its |
|
|
4 |
Full traversal (Statistics) |
nativejson-benchmark: its Stat counts of objects, arrays, numbers, strings, |
a walk by |
a walk by |
a walk by |
5 |
Key-path queries over |
simdjson: |
|
handles and |
property access |
6 |
Lookup, hit and miss |
Tessil’s hash-map benchmark, martinus’s map benchmark, V8’s PropertyQueries, abseil |
|
|
|
7 |
Insert a member |
Tessil, abseil’s TryEmplace, V8’s Map-Set |
|
|
|
8 |
Update a member |
abseil’s InsertOrAssign, martinus |
|
|
|
9 |
Delete members, then look every key up |
Tessil (reads after deleting), martinus, V8’s Map-Delete, Dromaeo’s removeChild |
* |
|
|
10 |
Iterate entries |
Tessil, martinus, abseil, V8’s Keys and Object, Dromaeo’s traverse |
a range |
|
|
11 |
Array push and pop |
Dromaeo’s appendChild and removeChild, martinus |
|
|
|
12 |
Array splice in the middle |
Dromaeo’s insertBefore and removeChild |
* |
|
|
13 |
Array methods: map, filter, reduce, find, slice, join |
V8’s |
* loops by hand; |
* loops by hand; |
the methods; * |
14 |
Deep copy |
martinus’s Copy, abseil’s CopyCtor, Dromaeo’s cloneNode |
the copy constructor |
|
|
15 |
Send and receive a document |
none: binary transmission, which document is built for |
|
the image copied into a buffer of the receiver’s, then |
|
16 |
Send and receive a sealed document |
none: the same, with the image’s seal checked |
- |
|
- |
17 |
Pass a document to a Worker, which answers each pass |
none: the Worker of a web application |
- |
the image copied into a transferable |
|
A * marks the closest form of an operation an implementation has no direct form of, as the
survey names it; the results’ tables carry the same mark. Rows 15 to 17 have no *: each
implementation has its own form of sending a document, and a - marks one that has none. Every array method of document but
slice is a loop by hand: document has the methods that change an array (push, pop, splice
and the others) and slice and concat, not the callbacks of map or filter. A loop that builds
an array copies each element it keeps into a new document, where JavaScript’s map, filter and
slice share the elements they keep with the array they read.
The data
-
nativejson-benchmark’s three files (Milo Yip’s, MIT):
canada.json, 2.2 MB of GeoJSON, mostly numbers;citm_catalog.json, 1.7 MB of wide objects;twitter.json, 0.6 MB of strings, many of them Japanese, and escapes. The repository is a submodule pinned at478d572,third_party/libs/nativejson-benchmark, which/webcpp/third_party//nativejson-benchmarkgives a program: the files are read where the checkout holds them, never copied. -
For the arrays, the largest ring of the polygon of
canada.json, an array of 14,310 points, each an array of two numbers, and the 100statusesoftwitter.json. -
For the dictionaries, Tessil’s synthetic objects: 10, 1,000 and 100,000 members whose keys are 15 or 50 characters long, a letter then letters and digits, so that none is an array index, each member’s value its index. The keys come from Mulberry32 with a fixed seed per object, in C++ and in JavaScript alike, bit for bit; the lookups take them in a shuffled order, from a seed of their own; the misses are as many keys of the same length from another seed. Beside them, the widest object of
citm_catalog.json,events, of 184 members, whose keys are nine-digit integers: array indices to JavaScript, which keeps such keys in ascending order, as document does and as the file lists them. -
A pass of operation 9 deletes every other key of the lookups’ order, at most 1,000 of them, then looks every key up: Boost.JSON’s
stable_erasemoves every member after the one it erases, and deleting half of 100,000 members takes it 25 s a pass.
The sizes on the wire
What a document weighs when it is sent is measured apart from its time, and not timed. Each file’s
value is sent three ways: as the JSON text JSON.stringify writes, as the text Boost.JSON’s
serializer writes, and as document’s image after shrink_to_fit. Each is shown as it is, gzipped and
brotli-compressed with Node’s zlib at its default levels (gzip’s level 6, brotli’s quality 11). The
sizes are in bytes:
| File | Form | Bytes | gzip | brotli |
|---|---|---|---|---|
|
JSON text, |
2,090,234 |
551,870 |
427,666 |
|
JSON text, Boost.JSON |
2,285,629 |
555,391 |
439,584 |
|
document’s image |
32,099,376 |
2,432,828 |
1,345,771 |
|
JSON text, |
500,299 |
17,024 |
7,848 |
|
JSON text, Boost.JSON |
500,299 |
17,024 |
7,848 |
|
document’s image |
8,531,152 |
617,425 |
186,897 |
|
JSON text, |
466,906 |
44,864 |
31,891 |
|
JSON text, Boost.JSON |
466,907 |
44,866 |
31,902 |
|
document’s image |
3,652,288 |
420,181 |
146,804 |
The image is the larger on the wire in every row. It holds a node of a tree for each value, with its
ids and offsets, where the text holds the value’s characters alone, so it is 8 to 17 times the
text’s size uncompressed; compressed it is still 3 to 9 times larger for canada.json and
twitter.json, and for citm_catalog.json the gzip and brotli of the image are 36 and 24 times the
text’s. The sizes come from bench/wire.mjs, which reads the files that bench-native --wire <dir>
writes; the results target does not write them.
A seal is a CRC-32C of the image’s bytes, which catches accidental damage (a flipped bit, a torn write). It is not authentication: anyone can compute a valid seal for any bytes, so an image from an untrusted source must not be opened, sealed or not.
Methodology
The survey’s section 4, which gathers the practice of the suites above:
-
Loading is never timed: the files are read, and the documents an operation reads are parsed and built, before its first pass. An operation that changes its input, such as delete, gets fresh copies, made before a sample and outside its time.
-
The first pass of each operation is timed alone, before any warm-up: the first-iteration time, as JetStream reports it, which tells how the code runs before the JIT or the caches have seen it. Its answer is then checked against every other implementation’s: the Stat counts of nativejson-benchmark for a parse, a stringify read back, a traversal, a copy or a document sent and received (sealed or not, and in a Worker, the Worker’s statistics of what it received); the queries’ answers; the members found and the sums of their values; lengths and checksums for the arrays. A run whose implementations disagree writes no results.
-
The warm-up doubles the passes of a sample until it lasts at least 10 ms, then runs one more sample, which is discarded. The result is the median of 10 samples of at least 10 ms, divided by the passes and by the operations of a pass.
-
JavaScript runs with
--expose-gc(in Chrome,--js-flags=--expose-gc), andgc()runs before each sample; its passes add their results to a checksum, which the run prints, and the C++ passes write theirs to avolatileobject, so that no compiler drops the work. -
Chrome loads its page from a server of the run, on 127.0.0.1, which makes it cross-origin isolated (COOP and COEP headers): its
performance.nowthen reads 5 us rather than 100 us. Node and Chrome share V8’s major version, 14: Node 26 and the Chrome for Testing 147 headless shell that Playwright 1.59 installs, whichbench/package-lock.jsonpins. -
Each implementation runs in a process of its own, or in a page of its own. The C++ is b2’s release variant:
-O3natively, and-O3for Emscripten too, at compilation and at link. -
document runs under
default_traits: exact decimal numbers and UTF-8 text. Parse and stringify also run underjs_traits, binary doubles and UTF-16, which JavaScript’s own representation is, in rows of their own. -
A C++ pass frees the result of the pass before it, so its time includes freeing it, as Boost.JSON’s own benchmark does; JavaScript’s garbage is collected when V8 chooses, and
gc()runs between samples.
The machine and the results
b2 libs/document/bench//results builds the programs, measures everything, and writes the
results below, with the machine and every version they were measured with; run again, on another
machine, it measures that one.
Measured on 2026-10-12, on this machine:
| Version | |
|---|---|
Machine |
Apple M5 Max, 18 cores, 128 GB |
Operating system |
macOS 27.0 (26A428), arm64 |
Native compiler |
Apple clang 21.0.0 (clang-2100.3.34.2), release variant ( |
Boost |
1.92 |
Emscripten |
6.0.11 (clang 24.0.0git), release variant, linked with |
Node.js |
v26.7.0, V8 14.6.202.34-node.28 |
Chrome |
Chrome 147.0.7727.15, Playwright’s headless shell, V8 14.7.173.6 |
Each cell is the median time of one operation, and below it, in italics, the time of the first
one, before any warm-up. A * marks the closest form of an operation that the implementation has
no direct form of (The operations); a -, an implementation that the row does
not concern.
1. Parse
From nativejson-benchmark, Boost.JSON bench, serde json-benchmark, nlohmann, JetStream 2.1; time per document.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
canada.json |
2.19 ms |
209 ms |
2.20 ms |
250 ms |
3.58 ms |
2.25 ms |
255 ms |
2.47 ms |
canada.json, |
- |
175 ms |
- |
209 ms |
- |
- |
213 ms |
- |
citm_catalog.json |
1.28 ms |
27.7 ms |
1.35 ms |
36.3 ms |
1.32 ms |
1.39 ms |
37.3 ms |
1.04 ms |
citm_catalog.json, |
- |
28.5 ms |
- |
36.6 ms |
- |
- |
36.7 ms |
- |
twitter.json |
751 µs |
11.6 ms |
830 µs |
15.1 ms |
589 µs |
835 µs |
16.0 ms |
403 µs |
twitter.json, |
- |
12.0 ms |
- |
15.0 ms |
- |
- |
15.5 ms |
- |
2. Stringify, compact
From nativejson-benchmark, Boost.JSON bench, nlohmann, simdjson, JetStream 2.1; time per document.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
canada.json |
1.70 ms |
33.9 ms |
4.63 ms |
36.7 ms |
2.62 ms |
4.68 ms |
39.3 ms |
1.91 ms |
canada.json, |
- |
20.5 ms |
- |
29.2 ms |
- |
- |
29.3 ms |
- |
citm_catalog.json |
348 µs |
4.23 ms |
498 µs |
4.85 ms |
745 µs |
510 µs |
4.87 ms |
582 µs |
citm_catalog.json, |
- |
4.45 ms |
- |
5.36 ms |
- |
- |
5.30 ms |
- |
twitter.json |
281 µs |
1.35 ms |
419 µs |
1.66 ms |
255 µs |
426 µs |
1.69 ms |
182 µs |
twitter.json, |
- |
1.63 ms |
- |
1.88 ms |
- |
- |
1.91 ms |
- |
3. Prettify, indent 4
From nativejson-benchmark, nlohmann, simdjson; time per document.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
canada.json |
3.58 ms * |
34.9 ms |
6.96 ms * |
37.7 ms |
6.34 ms |
7.00 ms * |
40.7 ms |
7.18 ms |
citm_catalog.json |
900 µs * |
4.51 ms |
1.45 ms * |
5.20 ms |
2.01 ms |
1.50 ms * |
5.24 ms |
1.79 ms |
twitter.json |
535 µs * |
1.44 ms |
792 µs * |
1.82 ms |
923 µs |
799 µs * |
1.81 ms |
626 µs |
4. Full traversal (Statistics)
From nativejson-benchmark; time per document.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
canada.json |
186 µs |
37.0 ms |
192 µs |
36.9 ms |
1.20 ms |
190 µs |
44.6 ms |
1.12 ms |
citm_catalog.json |
54.4 µs |
9.65 ms |
68.3 µs |
11.0 ms |
384 µs |
67.3 µs |
11.0 ms |
334 µs |
twitter.json |
32.0 µs |
2.71 ms |
32.1 µs |
2.98 ms |
174 µs |
30.8 µs |
3.04 ms |
170 µs |
5. Key-path queries
From simdjson (find_tweet, top_tweet, distinct_user_id); time per query, parse excluded.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
|
108 ns |
1.51 µs |
162 ns |
1.90 µs |
41.7 ns |
137 ns |
1.94 µs |
53.6 ns |
|
930 ns |
17.0 µs |
1.75 µs |
20.3 µs |
274 ns |
1.75 µs |
20.3 µs |
284 ns |
|
4.94 µs |
59.5 µs |
7.29 µs |
73.5 µs |
8.91 µs |
7.50 µs |
74.6 µs |
8.06 µs |
6. Lookup, hit and miss
From Tessil, martinus, V8 PropertyQueries, abseil; time per lookup.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
10 members, 15-character keys, hit |
10.4 ns |
42.3 ns |
8.86 ns |
56.0 ns |
5.65 ns |
9.24 ns |
56.3 ns |
5.25 ns |
10 members, 15-character keys, miss |
15.6 ns |
23.7 ns |
15.5 ns |
33.6 ns |
8.53 ns |
15.7 ns |
33.1 ns |
7.87 ns |
10 members, 50-character keys, hit |
9.69 ns |
81.7 ns |
11.1 ns |
93.4 ns |
5.35 ns |
10.9 ns |
93.6 ns |
5.15 ns |
10 members, 50-character keys, miss |
14.3 ns |
22.2 ns |
12.8 ns |
32.4 ns |
8.73 ns |
13.2 ns |
32.2 ns |
7.79 ns |
1,000 members, 15-character keys, hit |
13.0 ns |
132 ns |
16.7 ns |
189 ns |
5.57 ns |
16.2 ns |
179 ns |
4.85 ns |
1,000 members, 15-character keys, miss |
7.97 ns |
80.1 ns |
9.06 ns |
127 ns |
11.5 ns |
7.80 ns |
128 ns |
7.71 ns |
1,000 members, 50-character keys, hit |
17.2 ns |
197 ns |
26.0 ns |
254 ns |
5.69 ns |
25.7 ns |
248 ns |
4.91 ns |
1,000 members, 50-character keys, miss |
13.3 ns |
83.8 ns |
11.4 ns |
133 ns |
11.2 ns |
11.5 ns |
133 ns |
8.29 ns |
100,000 members, 15-character keys, hit |
19.0 ns |
576 ns |
27.2 ns |
705 ns |
24.2 ns |
27.9 ns |
707 ns |
15.9 ns |
100,000 members, 15-character keys, miss |
17.5 ns |
279 ns |
22.5 ns |
378 ns |
19.8 ns |
22.6 ns |
384 ns |
15.5 ns |
100,000 members, 50-character keys, hit |
28.0 ns |
932 ns |
38.5 ns |
965 ns |
16.7 ns |
39.0 ns |
1.06 µs |
13.9 ns |
100,000 members, 50-character keys, miss |
20.2 ns |
351 ns |
28.5 ns |
434 ns |
18.3 ns |
29.5 ns |
555 ns |
14.9 ns |
citm_catalog.json |
8.81 ns |
124 ns |
12.4 ns |
153 ns |
41.1 ns |
11.7 ns |
153 ns |
33.4 ns |
citm_catalog.json |
4.79 ns |
51.9 ns |
7.51 ns |
65.5 ns |
43.3 ns |
7.36 ns |
62.5 ns |
29.7 ns |
7. Insert a member
From Tessil, abseil TryEmplace, V8 Map-Set; time per insertion.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
10 members, 15-character keys |
30.2 ns |
435 ns |
27.8 ns |
617 ns |
6.26 ns |
28.1 ns |
647 ns |
6.09 ns |
10 members, 50-character keys |
30.6 ns |
453 ns |
28.7 ns |
631 ns |
6.25 ns |
28.6 ns |
670 ns |
6.27 ns |
1,000 members, 15-character keys |
32.6 ns |
652 ns |
40.2 ns |
959 ns |
19.8 ns |
41.6 ns |
992 ns |
15.4 ns |
1,000 members, 50-character keys |
38.3 ns |
689 ns |
55.1 ns |
1.01 µs |
20.4 ns |
56.6 ns |
1.01 µs |
15.4 ns |
100,000 members, 15-character keys |
38.6 ns |
1.14 µs |
50.8 ns |
1.59 µs |
43.3 ns |
51.6 ns |
1.63 µs |
34.4 ns |
100,000 members, 50-character keys |
45.9 ns |
1.42 µs |
68.7 ns |
1.88 µs |
42.7 ns |
69.2 ns |
1.98 µs |
33.7 ns |
8. Update a member
From abseil InsertOrAssign, martinus; time per assignment.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
10 members, 15-character keys |
12.3 ns |
143 ns |
12.9 ns |
205 ns |
5.12 ns |
12.8 ns |
203 ns |
4.72 ns |
10 members, 50-character keys |
11.1 ns |
185 ns |
15.1 ns |
246 ns |
4.97 ns |
14.9 ns |
241 ns |
4.67 ns |
1,000 members, 15-character keys |
18.1 ns |
258 ns |
26.6 ns |
397 ns |
4.95 ns |
27.1 ns |
391 ns |
4.04 ns |
1,000 members, 50-character keys |
23.1 ns |
335 ns |
35.2 ns |
443 ns |
5.02 ns |
35.0 ns |
448 ns |
4.52 ns |
100,000 members, 15-character keys |
21.7 ns |
633 ns |
31.1 ns |
797 ns |
8.66 ns |
31.2 ns |
805 ns |
7.32 ns |
100,000 members, 50-character keys |
29.8 ns |
959 ns |
40.4 ns |
1.12 µs |
9.63 ns |
41.1 ns |
1.16 µs |
7.46 ns |
citm_catalog.json |
111 ns |
3.71 µs |
150 ns |
5.18 µs |
44.9 ns |
158 ns |
5.14 µs |
32.6 ns |
9. Delete a member, then lookups
From Tessil, martinus, V8 Map-Delete, Dromaeo removeChild; time per deletion, and per lookup after them.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
10 members, 15-character keys, 5 deletions |
17.6 ns * |
177 ns |
19.8 ns * |
249 ns |
36.5 ns |
19.8 ns * |
251 ns |
21.2 ns |
10 members, 15-character keys, lookups after them |
8.56 ns * |
37.7 ns |
7.16 ns * |
53.6 ns |
5.98 ns |
7.35 ns * |
52.9 ns |
5.15 ns |
10 members, 50-character keys, 5 deletions |
21.9 ns * |
210 ns |
23.1 ns * |
286 ns |
36.0 ns |
23.3 ns * |
289 ns |
20.5 ns |
10 members, 50-character keys, lookups after them |
7.76 ns * |
57.3 ns |
8.26 ns * |
71.2 ns |
5.84 ns |
8.16 ns * |
70.2 ns |
5.27 ns |
1,000 members, 15-character keys, 500 deletions |
2.44 µs * |
343 ns |
3.00 µs * |
511 ns |
12.9 ns |
2.66 µs * |
510 ns |
7.38 ns |
1,000 members, 15-character keys, lookups after them |
9.52 ns * |
119 ns |
12.5 ns * |
193 ns |
6.50 ns |
11.9 ns * |
190 ns |
5.97 ns |
1,000 members, 50-character keys, 500 deletions |
3.17 µs * |
401 ns |
4.46 µs * |
562 ns |
12.7 ns |
4.49 µs * |
568 ns |
7.74 ns |
1,000 members, 50-character keys, lookups after them |
12.3 ns * |
158 ns |
18.4 ns * |
220 ns |
6.94 ns |
17.5 ns * |
217 ns |
5.81 ns |
100,000 members, 15-character keys, 1,000 deletions |
644 µs * |
709 ns |
686 µs * |
955 ns |
16.0 ns |
679 µs * |
946 ns |
12.6 ns |
100,000 members, 15-character keys, lookups after them |
22.1 ns * |
559 ns |
30.9 ns * |
709 ns |
15.4 ns |
30.6 ns * |
719 ns |
12.9 ns |
100,000 members, 50-character keys, 1,000 deletions |
774 µs * |
985 ns |
949 µs * |
1.28 µs |
17.7 ns |
960 µs * |
1.33 µs |
12.9 ns |
100,000 members, 50-character keys, lookups after them |
28.0 ns * |
894 ns |
42.2 ns * |
1.05 µs |
14.8 ns |
42.9 ns * |
1.05 µs |
12.2 ns |
citm_catalog.json |
552 ns * |
2.96 µs |
673 ns * |
3.87 µs |
60.9 ns |
674 ns * |
4.06 µs |
38.4 ns |
citm_catalog.json |
6.12 ns * |
140 ns |
8.48 ns * |
179 ns |
48.8 ns |
8.35 ns * |
177 ns |
39.4 ns |
10. Iterate entries
From Tessil, martinus, abseil, V8 Keys and Object, Dromaeo traverse; time per member.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
10 members, 15-character keys |
1.02 ns |
16.9 ns |
0.38 ns |
21.7 ns |
1.52 ns |
0.37 ns |
21.5 ns |
1.54 ns |
10 members, 50-character keys |
1.02 ns |
16.8 ns |
0.36 ns |
21.2 ns |
1.52 ns |
0.35 ns |
21.0 ns |
1.51 ns |
1,000 members, 15-character keys |
0.38 ns |
41.1 ns |
0.46 ns |
47.4 ns |
20.5 ns |
0.47 ns |
47.9 ns |
18.9 ns |
1,000 members, 50-character keys |
0.38 ns |
43.0 ns |
0.46 ns |
52.6 ns |
20.9 ns |
0.47 ns |
54.7 ns |
18.5 ns |
100,000 members, 15-character keys |
0.39 ns |
165 ns |
0.48 ns |
182 ns |
81.9 ns |
0.48 ns |
173 ns |
79.2 ns |
100,000 members, 50-character keys |
0.38 ns |
258 ns |
0.48 ns |
278 ns |
80.7 ns |
0.48 ns |
213 ns |
79.2 ns |
citm_catalog.json |
0.37 ns |
43.7 ns |
0.42 ns |
47.4 ns |
96.7 ns |
0.43 ns |
50.6 ns |
75.1 ns |
11. Array push and pop
From Dromaeo appendChild and removeChild, martinus; time per push or pop.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
twitter.json |
3.05 ns |
789 ns |
3.87 ns |
1.02 µs |
0.79 ns |
3.94 ns |
1.04 µs |
1.07 ns |
canada.json’s largest ring (14,310 points) |
3.11 ns |
1.26 µs |
3.83 ns |
1.57 µs |
0.82 ns |
3.96 ns |
1.61 µs |
1.08 ns |
12. Array splice in the middle
From Dromaeo insertBefore and removeChild; time per splice.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
canada.json’s largest ring (14,310 points) |
1.82 µs * |
666 µs |
855 ns * |
832 µs |
2.66 µs |
865 ns * |
853 µs |
2.80 µs |
13. Array methods
From V8 js-perf-test Array; time per element.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
|
5.31 ns * |
1.00 µs * |
6.96 ns * |
1.17 µs * |
6.20 ns |
6.98 ns * |
1.25 µs * |
5.37 ns |
|
8.37 ns * |
807 ns * |
9.21 ns * |
920 ns * |
5.64 ns |
9.40 ns * |
1.00 µs * |
4.37 ns |
|
7.80 ns * |
162 ns * |
14.7 ns * |
191 ns * |
1.72 ns |
14.4 ns * |
195 ns * |
1.73 ns |
|
18.1 ns * |
243 ns * |
23.4 ns * |
293 ns * |
2.85 ns |
22.9 ns * |
296 ns * |
2.57 ns |
|
18.4 ns * |
1.41 µs |
22.5 ns * |
1.75 µs |
0.09 ns |
22.6 ns * |
1.81 µs |
0.05 ns |
|
15.2 ns * |
148 ns * |
19.9 ns * |
178 ns * |
10.3 ns * |
18.9 ns * |
179 ns * |
9.28 ns * |
14. Deep copy
From martinus Copy, abseil CopyCtor, Dromaeo cloneNode; time per document.
| Dataset | Boost.JSON native | document native | Boost.JSON wasm, Node | document wasm, Node | JavaScript, Node | Boost.JSON wasm, Chrome | document wasm, Chrome | JavaScript, Chrome |
|---|---|---|---|---|---|---|---|---|
canada.json, |
1.12 ms |
441 µs |
1.20 ms |
438 µs |
5.43 ms |
1.24 ms |
440 µs |
4.38 ms |
citm_catalog.json, |
742 µs |
211 µs |
755 µs |
210 µs |
3.21 ms |
760 µs |
211 µs |
1.96 ms |
twitter.json, |
373 µs |
49.2 µs |
393 µs |
48.6 µs |
813 µs |
401 µs |
49.4 µs |
517 µs |
canada.json, |
- |
- |
- |
- |
6.02 ms |
- |
- |
4.26 ms |
citm_catalog.json, |
- |
- |
- |
- |
1.70 ms |
- |
- |
1.18 ms |
twitter.json, |
- |
- |
- |
- |
759 µs |
- |
- |
517 µs |
Reading the results
The figures below are this machine’s medians natively unless a place is named; the tables hold the rest. document is not a fast JSON tree, and this first version does not try to be one: it pays, on every operation, for what it guarantees, JavaScript’s behaviour, exact numbers and one relocatable image. Where it is slower, and why:
-
Binary transmission, which the image is built for. A document sent as its image is copied and opened in place: the receiver parses no text, builds no tree and allocates nothing per value, so its CPU cost is the copy and, for a sealed image, one CRC pass over the bytes (about 600 MB/s). Boost.JSON and JavaScript pay the parse on the receiving side, which the image does not. That is where the image wins: on the receiver’s CPU, in a Worker, where the image crosses as a transferred
ArrayBuffer(row 17) and not as a structured clone of every node, and in shared memory, which is not measured here. The rows’ timings are in the tables above. Over a constrained network the text wins: the image is 8 to 17 times larger, and still larger compressed, so on a link that carries the bytes the text is faster unless the receiver’s parse costs more than the extra bytes take to cross. On a LAN it depends on the rate. Forcanada.jsonthe image is 30 MB more than the text, which the link saves only when it carries more than about 143 MB/s (1.1 Gbit/s) against document’s own parse of canada, 209 ms; Boost.JSON’s 2.19 ms parse is never worth it. The seal is a CRC against accidental damage, not authentication: anyone can compute a valid seal, so an image from an untrusted source must not be opened. -
Building and walking the tree. document parses
canada.jsonin 209 ms, where Boost.JSON takes 2.19 ms and Node’sJSON.parse3.58 ms: 95 times Boost.JSON’s time, 22 times oncitm_catalog.jsonand 15 times ontwitter.json. A full traversal is slower still, 37.0 ms against 186 us oncanada.json. Every value is a node of a Boost.MultiIndex container inside the image, linked into three red-black trees by offset pointers: by its id, by its parent and position, and by its parent and key. Appending a value finds its parent by id and links the node into each tree, and a handle names a node by its id, so that every access looks it up again. A profile of the native parse ofcanada.json(macOS’ssample) spends most of its time looking nodes up by id, inserting into and rebalancing the trees, and converting numbers to exact decimals; underjs_traits, whose numbers are doubles, the same parse takes 175 ms, so the tree, not the decimals, is the larger cost. Boost.JSON appends to contiguous arrays and keeps doubles. -
Stringify is 20 times Boost.JSON’s time on
canada.json(33.9 ms against 1.70 ms) and 5 times ontwitter.json: it walks the same trees, and writes each exact decimal as JavaScript would write the number;js_traitsbringscanada.jsondown to 20.5 ms. -
Dictionaries. A lookup finds its key in an ordered tree, by comparisons, where Boost.JSON and V8 hash it: 42 ns at 10 members, 576 ns at 100,000, against 10 to 28 ns for Boost.JSON and 5 to 24 ns for JavaScript. Insertion (0.4 to 1.4 us), assignment and iteration (17 to 258 ns a member, against under 1 ns for Boost.JSON’s contiguous members) pay the same trees and the same lookups by id. JavaScript is slower than Boost.JSON on the
eventsofcitm_catalog.json(41 ns a lookup): its keys are array indices, which V8 keeps apart from named properties. -
Arrays. A splice in the middle of 14,310 points takes 666 us, against 2 or 3 us for Boost.JSON and JavaScript: every element after the splice is renumbered in the position tree. A push and a pop take 789 ns each, because
popreturns the removed value as a document of its own, whose image it allocates;set_lengthwould drop it without one.mapandfiltercopy each element they keep into a new document, about 1 us an element, where JavaScript’s share them, as itsslicedoes. -
Where document is faster: a deep copy is one copy of the image, 441 us for
canada.json, against 1.12 ms for Boost.JSON’s copy constructor and 5.43 ms forstructuredClonein Node. And deleting from a large object keeps the order without moving the members: 709 ns a deletion from 100,000 members, where Boost.JSON’sstable_erasetakes 644 us.
Across places: the C++ compiled to WebAssembly takes a median 1.26 times its native time, for
Boost.JSON and for document alike, at most 2.7 times (Boost.JSON’s stringify of canada.json, whose
doubles it formats more slowly in wasm32), and Node and Chrome run the same module within a quarter
of each other, their V8 sharing a major version. Boost.JSON’s splice is even faster in WebAssembly
(855 ns) than natively (1.82 us): its values are 16 bytes in wasm32, 24 natively, and a splice moves
them. JavaScript’s
first iteration is often several times its median, the time V8 takes to compile it, where the
C++'s first pass runs close to its median. Chrome’s clock reads 5 us, so the first iteration of a
pass shorter than that, such as ten lookups, reads in Chrome as 0 or 5 us divided by its operations.
These costs are the design’s, measured before any tuning, and the profile names where to begin:
the lookup of a node by id behind every handle, the renumbering of positions on a splice, and the
image that pop allocates.
Running them
b2 libs/document/bench//results
b2 libs/document/bench//smoke toolset=clang-wasip2 testing.launcher=wasmtime
The first measures on the machine it runs on, which took 9 minutes here, and writes
doc/benchmark-results.adoc; it is explicit, so that neither a default build nor the continuous
integration runs it. Node, npm and Emscripten’s toolset are needed beside the native one: npm
installs Playwright, and Playwright its headless shell, once. The second is one verifying pass of
every operation, on any target, which fails when Boost.JSON and document answer differently; the
library’s tests run it on every target as bench, without the objects of 100,000 members, which run
the same code as those of 1,000 and would take most of its time in a debug build.
Reference
Every name document declares, in the namespace webcpp::document, from the Doc Comments of its
headers: each class, function, type and enumerator with its declaration, what it does, what it asks
of its caller and how it fails. The other sections teach the library; this one is where a name is
looked up.
Macros
| Name | Description |
|---|---|
Marks a class that names a place in a document and owns what it needs to find it again, a handle or a range of a container’s children; empty outside GCC 14 and later, whose |
|
Defined when document is built without exceptions: when webcpp has none (the compiler has none, or the developer defined |
webcpp::document
Types
| Name | Description |
|---|---|
An array, as JavaScript writes |
|
The bytes policy that writes bytes as base64, RFC 4648 with padding, the default. |
|
The bytes policy that writes bytes as base64url, RFC 4648 section 5, without padding. |
|
An array as a vector-like container: |
|
A read-only handle: one place in one document, cheap to copy, which reads what is there now. |
|
A JSON document: one value, in one relocatable image. |
|
Reads a stream of images, one after another with nothing between them, into the blocks of an |
|
The members of an object in lexicographic key order, by UTF-16 code units whatever the encoding, with |
|
An object as a sorted-map-like container, in the traits’ key order (JavaScript’s by default): |
|
The streaming parser: |
|
The parser of a stream of documents: |
|
A handle that reads and writes: one place in one document, as |
|
No room left: a full external buffer, a document at its limit, a parser’s limit. |
|
A finite decimal number as JavaScript’s Number::toString sees it: its value is |
|
The default traits: exact numbers, UTF-8 strings, base64 bytes and JavaScript’s key order. |
|
The array policy: no holes, and writing past the end fills with |
|
What every exception of document derives from: a |
|
The storage of a document whose image is a buffer its user owns, such as a browser’s memory: when it is full, an operation returns |
|
Bytes that are not a document’s image, an image of another format, or a damaged one. |
|
The 64 bytes at the start of every image, in the byte order of the machine that wrote it. |
|
Fixed blocks for images, 16-aligned and zeroed, allocated once: |
|
The object policy that keeps every key in the order it was created, whatever the key. |
|
The bytes policy that writes bytes as |
|
The object policy of JavaScript: array-index keys ascending, then the others as created. |
|
The traits of JavaScript itself: doubles, UTF-16 strings, and bytes written as a Uint8Array. |
|
A document opened in place in a block of an |
|
One value of a literal, converted implicitly from the C++ value it is written as. |
|
The type of |
|
How the traits’ |
|
Text of at most 64 characters, held by value, so that writing a number or a date allocates nothing. |
|
An object, as JavaScript writes |
|
One member of an |
|
The storage of a document that owns its image: a buffer of its own, 16-aligned, which grows, twice the size each time, up to its |
|
The limits and the revivers of a parse. |
|
The path in the text of a value a selector delivers: each step a member’s key or an element’s index in the text, which |
|
A lease of one block of an |
|
JavaScript’s |
|
How a scalar type is stored in a node: its name in the image’s layout tag, its size, and its bytes, written the same on every target. |
|
A document in a block of a |
|
A pool of blocks for shared documents, over a region of memory the program gives: the process heap, or a region of shared memory that several processes map, each at an address of its own. |
|
The object policy that orders keys by their UTF-16 code units, with either encoding, as JavaScript’s |
|
The options of a stream: the limits and the revivers of each of its documents, its framing and its policy on errors. |
|
JavaScript’s |
|
JavaScript’s |
|
The type of |
|
The encoding of strings and keys that stores UTF-16 code units, as JavaScript holds strings. |
|
The encoding of strings and keys that stores UTF-8, the default. |
Type Aliases
| Name | Description |
|---|---|
A view of an array of a |
|
A view of an array of a |
|
A view of an object of a |
|
A handle of a |
|
A date as JavaScript’s |
|
A document of the default traits, owned: exact numbers, UTF-8, JavaScript’s key order. |
|
A reader of images of the default traits. |
|
A view of an object of a |
|
The streaming parser of a |
|
What every operation of document that can fail returns: a |
|
The parser of a stream of |
|
A handle of a |
Enums
| Name | Description |
|---|---|
The errors document reports, as |
|
How the documents of a stream are told apart. |
|
The kind of a value: JavaScript’s |
|
What a stream does with a bad document. |
|
Whether opening an image checks its seal. |
Functions
| Name | Description |
|---|---|
|
|
Reads the next image of a stream into a block of a pool and opens it there, with no copy and no allocation. |
|
Writes a document’s image to a stream as one buffer, after sealing the document. |
|
Writes the images of a range of documents to a stream in one gather write, after sealing every document of the range. |
|
Checks that bytes are an image this build reads, reading nothing past the header before its size is known, and that a sealed image’s bytes match its seal. |
|
The category of every |
|
Formats a buffer as a new, empty document, holding |
|
The digits of a finite value of a decimal type, normalised: every member of a cohort, |
|
The digits of a finite |
|
Copies an image into a new owned document. |
|
The digits of a signed 64-bit integer. |
|
The digits of an unsigned 64-bit integer. |
|
Makes the |
|
Opens the image already in a buffer, in place. |
|
|
|
Reads JSON text into a new document in a buffer its user owns, as |
|
|
|
Reads a JSON number’s text exactly, never through a double. |
|
|
|
|
|
Converts a number to a decimal type, exactly. |
|
Converts a number to |
|
Converts a number to a signed 64-bit integer, when it is one. |
|
Converts a number to an unsigned 64-bit integer, when it is one. |
|
The traits’ identity in the image’s layout tag: an FNV-1a hash, 32 bits, of each scalar codec’s name and size ( |
|
Writes a number as JavaScript’s Number::toString writes it, ECMA-262, radix 10. |
|
Equality operators |
Variables
| Name | Description |
|---|---|
The byte-order mark of the header, as the machine that wrote it writes this number. |
|
The size of the pieces |
|
The largest image an owned document grows to unless told otherwise: 1 GiB. |
|
The version of the image’s format that this library writes and opens. |
|
The first eight bytes of every image: |
|
JavaScript’s |
|
JavaScript’s |
Concepts
| Name | Description |
|---|---|
The decimal types of Boost.Decimal a document converts to and from: |
|
What a document’s traits must give: the scalar types, each a |
|
A type a node can store as a scalar: trivially copyable, with a |
webcpp::document::const_array_ref
A view of an array of a document, read only.
Synopsis
Declared in <webcpp/document/containers.hpp>
using const_array_ref = basic_array_ref<const_value>;
webcpp::document::const_object_ref
A view of an object of a document, read only.
Synopsis
Declared in <webcpp/document/containers.hpp>
using const_object_ref = basic_object_ref<const_value>;
webcpp::document::date_ms
A date as JavaScript’s Date holds one: milliseconds since the epoch, 1970-01-01T00:00:00Z.
Synopsis
Declared in <webcpp/document/traits.hpp>
using date_ms = std::chrono::sys_time<std::chrono::milliseconds>;
webcpp::document::document
A document of the default traits, owned: exact numbers, UTF-8, JavaScript’s key order.
webcpp::document::image_reader
A reader of images of the default traits.
webcpp::document::result
What every operation of document that can fail returns: a T, or the error_code of the failure.
Synopsis
Declared in <webcpp/document/errors.hpp>
template<class T>
using result = boost::system::result<T>;
Description
It is Boost.System’s result. document’s core never throws: every operation of it that can fail returns one, with an errc, or the error a sink or a selector’s callback of the user’s returned, passed on unchanged.
Template Parameters
| Name | Description |
|---|---|
T |
The type of the value; |
webcpp::document::array
An array, as JavaScript writes [...]: wd::array{1, "x", nullptr}.
Description
An array whose one element is an array writes that element as a literal, wd::array{wd::literal(wd::array{1})} for [[1]]: braces around one value of the class’s own type are a copy of that value where the compiler applies CWG 1467 to every class (MSVC, Clang before 20), so wd::array{wd::array{1}} is [1] there and [[1]] elsewhere.
See Also
"Handles", in the guide.
webcpp::document::array::array
Constructors
Synopses
Declared in <webcpp/document/literal.hpp>
An empty array, [].
constexpr
array() = default;
An array of these elements.
array(std::initializer_list<literal> elements);
Parameters
| Name | Description |
|---|---|
elements |
The elements, in order. |
webcpp::document::array::array
An array of these elements.
Parameters
| Name | Description |
|---|---|
elements |
The elements, in order. |
webcpp::document::array::elements
The elements, in order.
Synopsis
Declared in <webcpp/document/literal.hpp>
[[nodiscard]]
std::span<literal const>
elements() const noexcept;
webcpp::document::base64
The bytes policy that writes bytes as base64, RFC 4648 with padding, the default.
Static Data Members
| Name | Description |
|---|---|
The policy’s name in the image’s layout tag. |
See Also
"Strings, dates and bytes", in the guide.
webcpp::document::base64::name
The policy’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "base64";
webcpp::document::base64url
The bytes policy that writes bytes as base64url, RFC 4648 section 5, without padding.
Static Data Members
| Name | Description |
|---|---|
The policy’s name in the image’s layout tag. |
webcpp::document::base64url::name
The policy’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "base64url";
webcpp::document::basic_array_ref
An array as a vector-like container: front, back, operator[], at, push_back, emplace_back, pop_back, insert, erase, resize, clear and iteration both ways.
Description
Made by as_array() on a handle, it holds the document’s address and the array’s id, and is cheap to copy: a reference, like std::span, whose constness is its own, so a const view still writes; const_array_ref is the view that only reads. It is a std::ranges::bidirectional_range, a view and a borrowed range. The tree finds an element by its index in O(log n), so operator[] costs O(log n), not O(1), and the iterators are bidirectional, not random access, whose O(1) jumps they could not keep.
Iterators and handles hold ids, which no change reuses:
| Change | Iterators and handles | A std::string_view read from a value | | --- | --- | --- | | An element inserted, and any growth of the image | all follow their elements | invalid | | An element erased | those to it and its subtree; the others follow their elements | invalid | | An element’s value replaced | those to its old subtree | invalid | | The document moved from or destroyed | all | invalid |
What can fail returns result: an insertion, which needs room in the image, and at. An erasure needs no room and cannot fail. There are no throwing forms: they would differ from these only in their return type, which C++ does not overload on.
Type Aliases
| Name | Description |
|---|---|
The difference of two positions. |
|
A bidirectional iterator in index order. |
|
What dereferencing an iterator gives: a handle, made at each step. |
|
A reverse iterator in index order. |
|
A count of elements, or an index. |
|
The document’s storage. |
|
The document’s traits. |
|
An element: its handle. |
Member Functions
| Name | Description |
|---|---|
|
A view of nothing, which is empty and fails every insertion with |
The element |
|
The last element. |
|
The first element. |
|
Erases every element, which needs no room. |
|
Appends |
|
Whether the array has no element. |
|
The end of the elements, which every change leaves valid. |
|
|
|
The first element. |
|
Inserts |
|
The element |
|
Erases the last element, if there is one, which needs no room. |
|
Appends |
|
The last element, first in reverse. |
|
The end of the elements in reverse. |
|
Truncates the array to |
|
The array’s |
Friends
| Name | Description |
|---|---|
A handle that reads and writes: one place in one document, as |
|
A read-only handle: one place in one document, cheap to copy, which reads what is there now. |
Template Parameters
| Name | Description |
|---|---|
Handle |
The handle its elements are read through: |
See Also
"Handles", in the guide.
"Arrays", in the guide.
webcpp::document::basic_array_ref::difference_type
The difference of two positions.
webcpp::document::basic_array_ref::iterator
A bidirectional iterator in index order.
Synopsis
Declared in <webcpp/document/containers.hpp>
using iterator = /* implementation-defined */;
webcpp::document::basic_array_ref::reference
What dereferencing an iterator gives: a handle, made at each step.
webcpp::document::basic_array_ref::reverse_iterator
A reverse iterator in index order.
Synopsis
Declared in <webcpp/document/containers.hpp>
using reverse_iterator = std::reverse_iterator<iterator>;
webcpp::document::basic_array_ref::size_type
A count of elements, or an index.
webcpp::document::basic_array_ref::storage_type
The document’s storage.
Synopsis
Declared in <webcpp/document/containers.hpp>
using storage_type = /* implementation-defined */::storage;
webcpp::document::basic_array_ref::traits_type
The document’s traits.
Synopsis
Declared in <webcpp/document/containers.hpp>
using traits_type = /* implementation-defined */::traits;
webcpp::document::basic_array_ref::value_type
An element: its handle.
webcpp::document::basic_array_ref::basic_array_ref
A view of nothing, which is empty and fails every insertion with errc::type.
webcpp::document::basic_array_ref::at
The element index, checked, in O(log n).
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
result<Handle>
at(size_type index) const noexcept;
Return Value
Its handle; errc::range past the end, errc::type once this is no array.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::basic_array_ref::back
The last element.
Return Value
Its handle; one that names nothing when the array is empty.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_array_ref::begin
The first element.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
iterator
begin() const noexcept;
Return Value
The iterator; the end when there is none.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_array_ref::clear
Erases every element, which needs no room.
Synopsis
Declared in <webcpp/document/containers.hpp>
void
clear() const noexcept
requires detail::writable_handle<Handle>;
webcpp::document::basic_array_ref::emplace_back
Appends v and gives its handle.
Synopsis
Declared in <webcpp/document/containers.hpp>
template<class T>
[[nodiscard]]
result<Handle>
emplace_back(T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;
Return Value
The new element’s handle; the errors of push_back.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type, any that |
Parameters
| Name | Description |
|---|---|
v |
The value; a handle or a document is copied deeply, |
webcpp::document::basic_array_ref::empty
Whether the array has no element.
webcpp::document::basic_array_ref::end
The end of the elements, which every change leaves valid.
webcpp::document::basic_array_ref::erase
erase overloads
Synopses
Declared in <webcpp/document/containers.hpp>
Erases the element at pos and its subtree, the later elements moved one index down.
Erases the elements from first to last, last excluded, and their subtrees.
Return Value
-
The iterator of the element after it; the end when
posis the end, of another container, or at an element already erased. -
last, which follows its element; the end when either is of another container or at an element already erased, orlastis beforefirst, which erases nothing.
Parameters
| Name | Description |
|---|---|
pos |
An iterator of this array at an element. |
first |
An iterator of this array. |
last |
An iterator of this array at or after |
webcpp::document::basic_array_ref::erase
Erases the element at pos and its subtree, the later elements moved one index down.
Synopsis
Declared in <webcpp/document/containers.hpp>
Return Value
The iterator of the element after it; the end when pos is the end, of another container, or at an element already erased.
Parameters
| Name | Description |
|---|---|
pos |
An iterator of this array at an element. |
webcpp::document::basic_array_ref::erase
Erases the elements from first to last, last excluded, and their subtrees.
Synopsis
Declared in <webcpp/document/containers.hpp>
Return Value
last, which follows its element; the end when either is of another container or at an element already erased, or last is before first, which erases nothing.
Parameters
| Name | Description |
|---|---|
first |
An iterator of this array. |
last |
An iterator of this array at or after |
webcpp::document::basic_array_ref::front
The first element.
Return Value
Its handle; one that names nothing when the array is empty.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_array_ref::insert
Inserts v before pos, the later elements moved one index up, as a.splice(i, 0, v).
Synopsis
Declared in <webcpp/document/containers.hpp>
template<class T>
[[nodiscard]]
result<iterator>
insert(
iterator pos,
T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;
Return Value
The new element’s iterator; errc::type once this is no array or for an iterator of another container or at an element already erased, errc::capacity when there is no room, and the errors of set for the value.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type, any that |
Parameters
| Name | Description |
|---|---|
pos |
An iterator of this array, the end to append. |
v |
The value; a handle or a document is copied deeply, |
webcpp::document::basic_array_ref::operator[]
The element index, a[index], in O(log n): the tree finds it by its index.
Synopsis
Declared in <webcpp/document/containers.hpp>
Handle
operator[](size_type index) const noexcept;
Return Value
Its handle, as a handle’s operator[] gives it: past the end, a missing element, which set creates.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::basic_array_ref::pop_back
Erases the last element, if there is one, which needs no room.
Synopsis
Declared in <webcpp/document/containers.hpp>
void
pop_back() const noexcept
requires detail::writable_handle<Handle>;
webcpp::document::basic_array_ref::push_back
Appends v, a.push(v).
Synopsis
Declared in <webcpp/document/containers.hpp>
template<class T>
[[nodiscard]]
result<void>
push_back(T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;
Return Value
Success; errc::type once this is no array, errc::capacity when there is no room, and the errors of set for the value.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type, any that |
Parameters
| Name | Description |
|---|---|
v |
The value; a handle or a document is copied deeply, |
webcpp::document::basic_array_ref::rbegin
The last element, first in reverse.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
reverse_iterator
rbegin() const noexcept;
webcpp::document::basic_array_ref::rend
The end of the elements in reverse.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
reverse_iterator
rend() const noexcept;
webcpp::document::basic_array_ref::resize
Truncates the array to n elements, or appends null up to n: a.length = n.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
result<void>
resize(size_type n) const noexcept
requires detail::writable_handle<Handle>;
Return Value
Success; errc::type once this is no array, errc::capacity when there is no room.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
n |
The new length. |
webcpp::document::basic_array_ref::size
The array’s length.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
size_type
size() const noexcept;
webcpp::document::basic_const_value
A read-only handle: one place in one document, cheap to copy, which reads what is there now.
Synopsis
Declared in <webcpp/document/value.hpp>
template<
class Traits = default_traits,
class Storage = owned>
class basic_const_value;
Description
A handle names a node, a missing member or element of a container that exists (it reads undefined, and basic_value::set creates it), or nothing (it reads undefined, and set fails with errc::type). It holds the document’s address and node ids, so it stays valid when the document grows and its image moves; it reads undefined once its node is erased, and is invalid once its document is moved from or destroyed.
Member Functions
| Name | Description |
|---|---|
|
A handle that names nothing. |
Reads the value as |
|
The array as a vector-like container that reads: |
|
Reads a boolean, throwing on failure; only with exceptions. |
|
The object as a sorted-map-like container that reads: |
|
Reads a string as |
|
|
|
The first element of an array, for |
|
|
|
Whether an object has the member |
|
The end of an array’s elements. |
|
|
|
Reads the value as |
|
Whether the value is an array, |
|
Whether the value is |
|
Whether the value is bytes. |
|
Whether the value is a date. |
|
Whether the value is a number that is an integer, JavaScript’s |
|
Whether the value is |
|
Whether the value is a number. |
|
Whether the value is a plain object. |
|
Whether the value is a string. |
|
Whether nothing is there, |
|
|
|
The kind of the value, JavaScript’s |
|
An array’s |
|
Subscript operators |
|
An object’s key count or an array’s length. |
|
|
|
The value as |
|
The value as |
|
|
Friends
| Name | Description |
|---|---|
The streaming parser: |
|
A handle that reads and writes: one place in one document, as |
|
|
A read-only handle: one place in one document, cheap to copy, which reads what is there now. |
A JSON document: one value, in one relocatable image. |
|
Whether two values are equal, structurally: the same kind; numbers by value ( |
Derived Classes
| Name | Description |
|---|---|
A handle that reads and writes: one place in one document, as |
Template Parameters
| Name | Description |
|---|---|
Traits |
The document’s traits. |
Storage |
The document’s storage. |
See Also
"Handles", in the guide.
webcpp::document::basic_const_value::basic_const_value
A handle that names nothing.
webcpp::document::basic_const_value::as
Reads the value as T, as get does, throwing on failure; only with exceptions.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class T>
[[nodiscard]]
T
as() const
requires detail::exceptions_enabled<T> && detail::readable<Traits, T>;
Exceptions
| Name | Thrown on |
|---|---|
For another kind. |
|
For a number that does not fit. |
Template Parameters
| Name | Description |
|---|---|
T |
The type. |
See Also
"The sugar that throws", in the guide.
webcpp::document::basic_const_value::as_array
The array as a vector-like container that reads: front, back, at, and iterators that <algorithm> and <ranges> take.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<basic_array_ref<basic_const_value<Traits, Storage>>>
as_array() const noexcept;
Return Value
The view, defined in containers.hpp; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
See Also
"Arrays", in the guide.
webcpp::document::basic_const_value::as_bool
Reads a boolean, throwing on failure; only with exceptions.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class B = bool>
[[nodiscard]]
bool
as_bool() const
requires detail::exceptions_enabled<B>;
Exceptions
| Name | Thrown on |
|---|---|
For another kind. |
Template Parameters
| Name | Description |
|---|---|
B |
|
webcpp::document::basic_const_value::as_object
The object as a sorted-map-like container that reads: find, bounds by key, and iterators that <algorithm> and <ranges> take.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<basic_object_ref<basic_const_value<Traits, Storage>>>
as_object() const noexcept;
Return Value
The view, defined in containers.hpp; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
See Also
"Key order", in the guide.
webcpp::document::basic_const_value::as_string
Reads a string as std::string_view, throwing on failure; only with exceptions.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class C = char>
[[nodiscard]]
std::string_view
as_string() const
requires detail::exceptions_enabled<C>;
Exceptions
| Name | Thrown on |
|---|---|
For another kind. |
Return Value
The string, valid until the document’s next change.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
C |
|
webcpp::document::basic_const_value::at
Array.prototype.at: the element index, a negative index counting from the end.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
basic_const_value
at(int64_t index) const noexcept;
Return Value
Its handle; nothing out of range or for any other kind.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::basic_const_value::begin
The first element of an array, for for (auto v : arr).
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
/* implementation-defined */
begin() const noexcept;
Return Value
The iterator; the end for any other kind.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_const_value::concat
Array.prototype.concat: this array, then each argument, an array’s elements or the value.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class... T>
[[nodiscard]]
result<basic_document<Traits, owned>>
concat(T const&... v) const noexcept
requires (detail::settable<Traits, T> && ...);
Return Value
A new owned document holding the array; errc::type for any other kind, errc::capacity when it does not fit.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The arguments’ types, each settable. |
Parameters
| Name | Description |
|---|---|
v |
The arguments. |
webcpp::document::basic_const_value::contains
Whether an object has the member key, key in v.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
bool
contains(std::string_view key) const noexcept;
Return Value
Whether it is there; false for any other kind.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_const_value::end
The end of an array’s elements.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
/* implementation-defined */
end() const noexcept;
webcpp::document::basic_const_value::entries
Object.entries: pairs of each key and its value, in the policy’s order.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
/* implementation-defined */
entries() const noexcept;
Return Value
The range; empty for any other kind than an object.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_const_value::get
Reads the value as T.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class T>
[[nodiscard]]
result<T>
get() const noexcept
requires detail::readable<Traits, T>;
Description
T is bool, any integer type, a decimal type, the traits’ number, std::string, std::string_view (with utf8), std::u16string, std::u16string_view (with utf16), date_ms, std::vector<std::byte> or std::span<const std::byte>. A view is valid until the document’s next change. A string reads as a date when it is exactly Date.prototype.toJSON’s text, and as bytes when it is text of the traits’ bytes policy.
Return Value
The value; errc::type when nothing is there or for another kind, errc::range for a number that is not an integer, does not fit T or that T cannot hold exactly.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The type. |
webcpp::document::basic_const_value::is_array
Whether the value is an array, Array.isArray.
webcpp::document::basic_const_value::is_boolean
Whether the value is true or false.
webcpp::document::basic_const_value::is_bytes
Whether the value is bytes.
webcpp::document::basic_const_value::is_date
Whether the value is a date.
webcpp::document::basic_const_value::is_integer
Whether the value is a number that is an integer, JavaScript’s Number.isInteger.
Return Value
Whether it is a number with no fractional part.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_const_value::is_null
Whether the value is null.
webcpp::document::basic_const_value::is_number
Whether the value is a number.
webcpp::document::basic_const_value::is_object
Whether the value is a plain object.
webcpp::document::basic_const_value::is_string
Whether the value is a string.
webcpp::document::basic_const_value::is_undefined
Whether nothing is there, v === undefined.
webcpp::document::basic_const_value::keys
Object.keys: each key, in the policy’s order; a std::string_view with utf8, a std::u16string_view with utf16.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
/* implementation-defined */
keys() const noexcept;
Return Value
The range; empty for any other kind than an object.
|
Note
|
The return value should not be discarded. |
See Also
"Key order", in the guide.
webcpp::document::basic_const_value::kind
The kind of the value, JavaScript’s typeof with null, arrays, dates and bytes told apart.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
webcpp::document::kind
kind() const noexcept;
Return Value
The kind; kind::undefined when nothing is there.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_const_value::length
An array’s length, or a string’s in UTF-16 code units, as JavaScript counts it.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<uint64_t>
length() const noexcept;
Return Value
The length; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_const_value::operator[]
Subscript operators
Synopses
Declared in <webcpp/document/value.hpp>
The member key, v.key, which never fails.
basic_const_value
operator[](std::string_view key) const noexcept;
The element index, v[index], which never fails.
basic_const_value
operator[](uint64_t index) const noexcept;
Return Value
-
Its handle: the member, a missing member of an object, or nothing.
-
Its handle: the element, a missing element of an array, or nothing.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
index |
The index. |
webcpp::document::basic_const_value::operator[]
The member key, v.key, which never fails.
Synopsis
Declared in <webcpp/document/value.hpp>
basic_const_value
operator[](std::string_view key) const noexcept;
Return Value
Its handle: the member, a missing member of an object, or nothing.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_const_value::operator[]
The element index, v[index], which never fails.
Synopsis
Declared in <webcpp/document/value.hpp>
basic_const_value
operator[](uint64_t index) const noexcept;
Return Value
Its handle: the element, a missing element of an array, or nothing.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::basic_const_value::size
An object’s key count or an array’s length.
webcpp::document::basic_const_value::slice
Array.prototype.slice: a copy of the elements from start to stop, each a negative index counting from the end, clamped to the length.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<basic_document<Traits, owned>>
slice(
int64_t start = 0,
int64_t stop = std::numeric_limits<std::int64_t>::max()) const noexcept;
Return Value
A new owned document holding the array of the copies; errc::type for any other kind, errc::capacity when it does not fit.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
start |
The first index. |
stop |
The index after the last. |
webcpp::document::basic_const_value::stringify
The value as JSON.stringify(value, null, indent) writes it.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
std::string
stringify(uint32_t indent = 0) const;
Return Value
The text; empty when the value is undefined.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
indent |
The spaces of each level, clamped to 10; 0 writes compactly. |
webcpp::document::basic_const_value::value_or
The value as T, or fallback: JavaScript’s v ?? fallback, and the fallback too when the value cannot be read as T.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class T>
[[nodiscard]]
T
value_or(T fallback) const noexcept
requires detail::readable<Traits, T>;
Template Parameters
| Name | Description |
|---|---|
T |
The type, as |
Parameters
| Name | Description |
|---|---|
fallback |
The value when the value is |
webcpp::document::basic_const_value::values
Object.values: each value, in the policy’s order.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
/* implementation-defined */
values() const noexcept;
Return Value
The range; empty for any other kind than an object.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_document
A JSON document: one value, in one relocatable image.
Synopsis
Declared in <webcpp/document/document.hpp>
template<
class Traits = default_traits,
class Storage = owned>
class basic_document;
Description
The image is a 64-byte header and a heap that holds the tree, every pointer in it an offset, so that it works at any 16-aligned address and has the same bytes on 64-bit native and wasm32. A document is moved cheaply; moving it, or destroying it, invalidates its handles, as a container’s iterators would be. It is copied only with exceptions, or by clone.
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Frees an owned image. |
Assignment operators |
|
The image’s size in bytes, which the document’s changes fill before an owned image grows and moves, and before an external one fails. |
|
Copies the image into a new owned document, as |
|
Rebuilds an owned image as the compact image of its content: the nodes in document order, ids numbered from the first, each block right after the last with no hole between, the image exactly as large as they are. |
|
Writes the compact image of the document’s content into a buffer, as |
|
The size in bytes of the compact image of the document’s content, which |
|
The image: the header and the heap, which |
|
Subscript operators |
|
Makes the image hold at least |
|
|
|
Seals the image: writes into its header the CRC-32C of its bytes, which |
|
Whether the image carries a seal, which |
|
Shrinks an owned image to the end of its last block in use, its content unchanged. |
|
The document’s value as |
Static Member Functions
| Name | Description |
|---|---|
Makes a new owned document, holding |
Friends
| Name | Description |
|---|---|
The streaming parser: |
|
A handle that reads and writes: one place in one document, as |
|
A read-only handle: one place in one document, cheap to copy, which reads what is there now. |
|
|
A JSON document: one value, in one relocatable image. |
Whether two documents hold equal values, structurally, key order aside. |
Template Parameters
See Also
"Handles", in the guide.
"Images", in the guide.
webcpp::document::basic_document::basic_document
Constructors
Synopses
Declared in <webcpp/document/document.hpp>
Makes a new owned document, holding null, as create does, throwing on failure; only with exceptions.
basic_document()
requires detail::exceptions_enabled<Traits> && std::same_as<Storage, owned>;
Copies a document’s image into a new owned document, as clone does, throwing on failure; only with exceptions.
basic_document(basic_document const& other)
requires detail::exceptions_enabled<Traits> && std::same_as<Storage, owned>;
Takes the image of other, whose handles become invalid; other holds nothing after it.
basic_document(basic_document&& other) noexcept;
Exceptions
| Name | Thrown on |
|---|---|
When even an empty document does not fit. |
Parameters
| Name | Description |
|---|---|
other |
The document. |
See Also
"The sugar that throws", in the guide.
webcpp::document::basic_document::basic_document
Makes a new owned document, holding null, as create does, throwing on failure; only with exceptions.
Synopsis
Declared in <webcpp/document/document.hpp>
basic_document()
requires detail::exceptions_enabled<Traits> && std::same_as<Storage, owned>;
Exceptions
| Name | Thrown on |
|---|---|
When even an empty document does not fit. |
See Also
"The sugar that throws", in the guide.
webcpp::document::basic_document::basic_document
Copies a document’s image into a new owned document, as clone does, throwing on failure; only with exceptions.
Synopsis
Declared in <webcpp/document/document.hpp>
basic_document(basic_document const& other)
requires detail::exceptions_enabled<Traits> && std::same_as<Storage, owned>;
Exceptions
| Name | Thrown on |
|---|---|
When the copy does not fit. |
Parameters
| Name | Description |
|---|---|
other |
The document. |
webcpp::document::basic_document::basic_document
Takes the image of other, whose handles become invalid; other holds nothing after it.
Synopsis
Declared in <webcpp/document/document.hpp>
basic_document(basic_document&& other) noexcept;
Parameters
| Name | Description |
|---|---|
other |
The document. |
webcpp::document::basic_document::~basic_document
Frees an owned image.
webcpp::document::basic_document::operator=
Assignment operators
Synopses
Declared in <webcpp/document/document.hpp>
A document is copied by clone, or with exceptions by its copy constructor.
basic_document&
operator=(basic_document const&) = delete;
Takes the image of other, freeing this document’s own; the handles of both become invalid.
basic_document&
operator=(basic_document&& other) noexcept;
Return Value
This document.
Parameters
| Name | Description |
|---|---|
other |
The document. |
webcpp::document::basic_document::operator=
A document is copied by clone, or with exceptions by its copy constructor.
Synopsis
Declared in <webcpp/document/document.hpp>
basic_document&
operator=(basic_document const&) = delete;
webcpp::document::basic_document::operator=
Takes the image of other, freeing this document’s own; the handles of both become invalid.
Synopsis
Declared in <webcpp/document/document.hpp>
basic_document&
operator=(basic_document&& other) noexcept;
Return Value
This document.
Parameters
| Name | Description |
|---|---|
other |
The document. |
webcpp::document::basic_document::capacity
The image’s size in bytes, which the document’s changes fill before an owned image grows and moves, and before an external one fails.
Synopsis
Declared in <webcpp/document/document.hpp>
[[nodiscard]]
uint64_t
capacity() const noexcept;
Return Value
The size, as image gives it; 0 for a moved-from document.
|
Note
|
The return value should not be discarded. |
See Also
"Images", in the guide.
webcpp::document::basic_document::clone
Copies the image into a new owned document, as structuredClone would.
Synopsis
Declared in <webcpp/document/document.hpp>
[[nodiscard]]
result<basic_document<Traits, owned>>
clone() const noexcept;
Description
The copy keeps a seal, its bytes being the image’s; the seal is not checked, the image being this document’s own.
Return Value
The copy, equal and independent; errc::capacity when it does not fit, and errc::type for a moved-from document, which has no image.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_document::compact
Rebuilds an owned image as the compact image of its content: the nodes in document order, ids numbered from the first, each block right after the last with no hole between, the image exactly as large as they are.
Synopsis
Declared in <webcpp/document/document.hpp>
result<void>
compact() noexcept
requires std::same_as<Storage, owned>;
Description
Two documents of equal content, whatever edits made them, have byte-identical images after it, and a compact image compacts to itself. The copy is built before it replaces the image, so a failure changes nothing. As a vector’s reallocation does, it invalidates every handle, every std::string_view read from a value and every iterator, and, the bytes having changed, it clears a seal: sealing after it is the caller’s. The image’s size is compact_size afterwards, which is also its capacity, so the next change that needs room grows it.
Return Value
Success; errc::capacity when the new buffer cannot be allocated, and errc::type for a moved-from document.
See Also
"Images", in the guide.
webcpp::document::basic_document::compact_into
Writes the compact image of the document’s content into a buffer, as compact builds it, and opens it there: for a document that cannot reallocate, such as one leased from a pool or held in a shared block, or to send an image without waste.
Synopsis
Declared in <webcpp/document/document.hpp>
[[nodiscard]]
result<basic_document<Traits, external>>
compact_into(std::span<std::byte> buffer) const noexcept;
Description
The buffer is zeroed past the image. This document is not changed.
Return Value
The document, over the buffer’s first compact_size bytes; errc::capacity when the buffer is smaller or is not 16-aligned, writing nothing; errc::type for a moved-from document and for a buffer that overlaps the image.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
buffer |
The buffer, 16-aligned, of at least |
See Also
"Images", in the guide.
webcpp::document::basic_document::compact_size
The size in bytes of the compact image of the document’s content, which compact and compact_into produce exactly: a program sizes a block with it.
Synopsis
Declared in <webcpp/document/document.hpp>
[[nodiscard]]
uint64_t
compact_size() const noexcept;
Description
It reads every node and allocates nothing, a fraction of the cost of the copy.
See Also
"Images", in the guide.
webcpp::document::basic_document::image
The image: the header and the heap, which open and from_image read back.
Synopsis
Declared in <webcpp/document/document.hpp>
[[nodiscard]]
std::span<std::byte const>
image() const noexcept;
Return Value
Its bytes, valid until the document’s next change.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_document::operator[]
Subscript operators
Synopses
Declared in <webcpp/document/document.hpp>
The member key of the document’s value, as root()[key].
basic_value<Traits, Storage>
operator[](std::string_view key) noexcept;
The member key of the document’s value, read only.
basic_const_value<Traits, Storage>
operator[](std::string_view key) const noexcept;
The element index of the document’s value, as root()[index].
basic_value<Traits, Storage>
operator[](uint64_t index) noexcept;
The element index of the document’s value, read only.
basic_const_value<Traits, Storage>
operator[](uint64_t index) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
index |
The index. |
webcpp::document::basic_document::operator[]
The member key of the document’s value, as root()[key].
Synopsis
Declared in <webcpp/document/document.hpp>
basic_value<Traits, Storage>
operator[](std::string_view key) noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_document::operator[]
The member key of the document’s value, read only.
Synopsis
Declared in <webcpp/document/document.hpp>
basic_const_value<Traits, Storage>
operator[](std::string_view key) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_document::operator[]
The element index of the document’s value, as root()[index].
Synopsis
Declared in <webcpp/document/document.hpp>
basic_value<Traits, Storage>
operator[](uint64_t index) noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::basic_document::operator[]
The element index of the document’s value, read only.
Synopsis
Declared in <webcpp/document/document.hpp>
basic_const_value<Traits, Storage>
operator[](uint64_t index) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::basic_document::reserve
Makes the image hold at least bytes, as std::vector::reserve: an owned image grows in one step, as a change that needs room grows it, to bytes or twice its size, the larger, at most its max_bytes.
Synopsis
Declared in <webcpp/document/document.hpp>
[[nodiscard]]
result<void>
reserve(uint64_t bytes) noexcept;
Description
Growing moves the image, which invalidates every std::string_view read from a value but no handle or iterator, and clears a seal; a reservation within the capacity changes nothing. Building a large document after one reservation of its size moves the image once, not at each doubling.
Return Value
Success; errc::capacity when bytes is past max_bytes, or past an external image’s size, which never grows, or the larger buffer cannot be allocated, each changing nothing; errc::type for a moved-from document.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
bytes |
The size the image must reach. |
See Also
"Images", in the guide.
webcpp::document::basic_document::root
root overloads
Synopses
Declared in <webcpp/document/document.hpp>
The handle of the document’s value.
basic_value<Traits, Storage>
root() noexcept;
The handle of the document’s value, read only.
[[nodiscard]]
basic_const_value<Traits, Storage>
root() const noexcept;
Return Value
-
The handle, which reads and writes.
-
The handle.
webcpp::document::basic_document::root
The handle of the document’s value.
Return Value
The handle, which reads and writes.
webcpp::document::basic_document::root
The handle of the document’s value, read only.
Synopsis
Declared in <webcpp/document/document.hpp>
[[nodiscard]]
basic_const_value<Traits, Storage>
root() const noexcept;
webcpp::document::basic_document::seal
Seals the image: writes into its header the CRC-32C of its bytes, which open, from_image and the image reader then check, refusing an image whose bytes changed.
Description
The seal holds until the document’s next change, which clears it: the image is then simply not sealed, never wrongly sealed. It catches bytes changed inside an image (a flipped bit on a disk or a wire, a torn write, a stray write), not an old but valid image in reused memory, which carries a valid seal of its own. It costs a pass over the image, Boost.CRC’s table a byte at a time: measured natively at -O2 on one core of an Apple M5 Max, 1.7 ms per MiB (about 600 MB/s), and the check when an image is opened costs the same.
Return Value
Success; errc::type for a moved-from document, which has no image.
See Also
"Images", in the guide.
webcpp::document::basic_document::sealed
Whether the image carries a seal, which seal wrote and no change has cleared since.
Return Value
Whether it is sealed; false for a moved-from document. The seal is not checked here.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_document::shrink_to_fit
Shrinks an owned image to the end of its last block in use, its content unchanged.
Synopsis
Declared in <webcpp/document/document.hpp>
result<void>
shrink_to_fit() noexcept
requires std::same_as<Storage, owned>;
Description
Blocks never move, so the free blocks before the last one in use stay in the image: a document that grew and then let most of its values go may stay nearly as large. A deep copy into a new document, packed.root().set(doc), packs the content, and compact packs it in one order whatever the document’s history.
Return Value
Success; errc::capacity when the smaller buffer cannot be allocated.
webcpp::document::basic_document::stringify
The document’s value as JSON.stringify(value, null, indent) writes it.
Synopsis
Declared in <webcpp/document/document.hpp>
[[nodiscard]]
std::string
stringify(uint32_t indent = 0) const;
Return Value
The text; empty when the document holds undefined.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
indent |
The spaces of each level, clamped to 10; 0 writes compactly. |
webcpp::document::basic_document::create
Makes a new owned document, holding null.
Synopsis
Declared in <webcpp/document/document.hpp>
static
result<basic_document>
create(uint64_t max_bytes = default_max_bytes) noexcept
requires std::same_as<Storage, owned>;
Return Value
The document; errc::capacity when even an empty one does not fit max_bytes.
Parameters
| Name | Description |
|---|---|
max_bytes |
The size its image may grow to; past it an operation returns |
webcpp::document::basic_image_reader
Reads a stream of images, one after another with nothing between them, into the blocks of an image_pool, and opens each in place as a leased_document.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
template<class Traits = default_traits>
class basic_image_reader;
Description
Sans-IO: the bytes come from any source, copied by write, or read straight into the block by the source itself, through prepare and commit. The first 64 bytes of each image are checked as its header before a byte more is taken: its magic, format version, byte order, reserved bytes, the layout tag of Traits, and a size within a block. The rest of the image follows into the same block, and the image is opened there, a sealed one checked against its seal, and its heap checked. A refusal stops the reader with its error until reset, since the stream has lost its framing; the documents completed before it are still delivered.
The documents completed and not yet taken are queued in their own blocks, so the reader allocates nothing. When every block of the pool is leased, the reader takes no more bytes: that is the stream’s backpressure.
|
Note
|
Images are trusted input: this path is for peers the program trusts, and untrusted data enters as JSON text. |
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Gives back the blocks of the image being read and of the documents not taken. |
Assignment operators |
|
Takes |
|
How many images the reader has completed and opened. |
|
Ends the stream: an image cut short is an error, and a stream that ends between two images is whole. |
|
The oldest document completed and not yet taken. |
|
How many bytes of the stream the reader has taken. |
|
The room the stream’s next bytes go into, in a block: the rest of the image’s header, or the rest of the image it states, so that a read never crosses an image’s end. |
|
Makes the reader new: gives back every block it holds, its documents not taken included. |
|
|
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits of the images; another traits’ image is refused. |
See Also
"Images", in the guide.
webcpp::document::basic_image_reader::basic_image_reader
Constructors
Synopses
Declared in <webcpp/document/image_stream.hpp>
A reader is not copied.
basic_image_reader(basic_image_reader const&) = delete;
Takes the stream of other, its documents and its block, leaving other a new reader on the same pool.
basic_image_reader(basic_image_reader&& other) noexcept;
A reader whose images go into the blocks of pool.
explicit
basic_image_reader(image_pool& pool) noexcept;
Parameters
| Name | Description |
|---|---|
other |
The reader. |
pool |
The pool, which outlives the reader and its documents. |
webcpp::document::basic_image_reader::basic_image_reader
A reader is not copied.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_image_reader(basic_image_reader const&) = delete;
webcpp::document::basic_image_reader::basic_image_reader
Takes the stream of other, its documents and its block, leaving other a new reader on the same pool.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_image_reader(basic_image_reader&& other) noexcept;
Parameters
| Name | Description |
|---|---|
other |
The reader. |
webcpp::document::basic_image_reader::basic_image_reader
A reader whose images go into the blocks of pool.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
explicit
basic_image_reader(image_pool& pool) noexcept;
Parameters
| Name | Description |
|---|---|
pool |
The pool, which outlives the reader and its documents. |
webcpp::document::basic_image_reader::~basic_image_reader
Gives back the blocks of the image being read and of the documents not taken.
webcpp::document::basic_image_reader::operator=
Assignment operators
Synopses
Declared in <webcpp/document/image_stream.hpp>
A reader is not copied.
basic_image_reader&
operator=(basic_image_reader const&) = delete;
Gives back this reader’s blocks, then takes the stream of other, its documents and its block, leaving other a new reader on the same pool.
basic_image_reader&
operator=(basic_image_reader&& other) noexcept;
Return Value
This reader.
Parameters
| Name | Description |
|---|---|
other |
The reader. |
webcpp::document::basic_image_reader::operator=
A reader is not copied.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_image_reader&
operator=(basic_image_reader const&) = delete;
webcpp::document::basic_image_reader::operator=
Gives back this reader’s blocks, then takes the stream of other, its documents and its block, leaving other a new reader on the same pool.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_image_reader&
operator=(basic_image_reader&& other) noexcept;
Return Value
This reader.
Parameters
| Name | Description |
|---|---|
other |
The reader. |
webcpp::document::basic_image_reader::commit
Takes bytes bytes written at the start of the room prepare gave: the header is checked once it is whole, and the image opened once it is.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
result<std::size_t>
commit(std::size_t bytes) noexcept;
Return Value
How many documents the bytes completed, 0 or 1; errc::range for more bytes than the room holds, which changes nothing; the error that stops the reader: errc::not_a_document for bytes that are not an image’s or an image whose heap is not sound, errc::incompatible_document for an image of another format version, byte order, reserved bytes that are not zero or traits, errc::damaged_document for a sealed image whose bytes do not match its seal, and errc::capacity for an image larger than a block.
Parameters
| Name | Description |
|---|---|
bytes |
How many bytes were written, at most the room’s size. |
webcpp::document::basic_image_reader::documents
How many images the reader has completed and opened.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
uint64_t
documents() const noexcept;
Return Value
The count, since the reader was made or reset, the documents not yet taken included.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_image_reader::finish
Ends the stream: an image cut short is an error, and a stream that ends between two images is whole.
Return Value
Success; errc::not_a_document for an image cut short, which stops the reader; the error that stopped it.
webcpp::document::basic_image_reader::next
The oldest document completed and not yet taken.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
result<std::optional<leased_document<Traits>>>
next() noexcept;
Return Value
The document, or nothing when none is waiting; once the documents completed before an error are taken, the error that stopped the reader.
webcpp::document::basic_image_reader::offset
How many bytes of the stream the reader has taken.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
uint64_t
offset() const noexcept;
Return Value
The count, since the reader was made or reset.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_image_reader::prepare
The room the stream’s next bytes go into, in a block: the rest of the image’s header, or the rest of the image it states, so that a read never crosses an image’s end.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
result<std::span<std::byte>>
prepare() noexcept;
Description
The source writes some of the bytes there and gives their count to commit, so that a socket reads straight into the block, with no copy.
Return Value
The room, never empty; errc::capacity when a new image needs a block and every block is leased, which changes nothing; the error that stopped the reader.
webcpp::document::basic_image_reader::reset
Makes the reader new: gives back every block it holds, its documents not taken included.
webcpp::document::basic_image_reader::write
write overloads
Synopses
Declared in <webcpp/document/image_stream.hpp>
Copies the stream’s next bytes into blocks, opening each image they complete.
result<std::size_t>
write(std::span<std::byte const> bytes) noexcept;
Copies the stream’s next bytes into blocks, and gives on_document each document, in order, the documents not yet taken first: as soon as it returns, the block of a document it let go is free for the next image.
template<class F>
requires std::invocable<F&, leased_document<Traits>&&>
result<std::size_t>
write(
std::span<std::byte const> bytes,
F&& on_document);
Return Value
Template Parameters
| Name | Description |
|---|---|
F |
A callable taking a |
Parameters
| Name | Description |
|---|---|
bytes |
The next bytes of the stream. |
on_document |
The callable. |
webcpp::document::basic_image_reader::write
Copies the stream’s next bytes into blocks, opening each image they complete.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
result<std::size_t>
write(std::span<std::byte const> bytes) noexcept;
Return Value
How many bytes were taken: all of them, or fewer when every block of the pool is leased, the rest to be written again once a block is given back; the error that stops the reader, as commit returns it.
Parameters
| Name | Description |
|---|---|
bytes |
The next bytes of the stream. |
webcpp::document::basic_image_reader::write
Copies the stream’s next bytes into blocks, and gives on_document each document, in order, the documents not yet taken first: as soon as it returns, the block of a document it let go is free for the next image.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
template<class F>
requires std::invocable<F&, leased_document<Traits>&&>
result<std::size_t>
write(
std::span<std::byte const> bytes,
F&& on_document);
Return Value
As write without a callable returns.
Template Parameters
| Name | Description |
|---|---|
F |
A callable taking a |
Parameters
| Name | Description |
|---|---|
bytes |
The next bytes of the stream. |
on_document |
The callable. |
webcpp::document::basic_object_by_key
The members of an object in lexicographic key order, by UTF-16 code units whatever the encoding, with find, lower_bound, upper_bound and equal_range: prefix and range queries over keys.
Synopsis
Declared in <webcpp/document/containers.hpp>
template<class Handle>
class basic_object_by_key;
Description
It reads the tree’s index by key, so each step and each bound costs O(log n). Under the sorted policy it is also the object’s own order. It is a std::ranges::bidirectional_range and a borrowed one, made by basic_object_ref::by_key; its iterators are invalidated as basic_object_ref’s are.
Type Aliases
| Name | Description |
|---|---|
The difference of two positions. |
|
A bidirectional iterator in key order. |
|
What dereferencing an iterator gives: a value, made at each step. |
|
A reverse iterator in key order. |
|
A count of members. |
|
The document’s storage. |
|
The document’s traits. |
|
A member: its key, a view of the image valid until the next change, and its handle. |
Member Functions
| Name | Description |
|---|---|
|
A view of nothing, which is empty. |
The member whose key is first in key order. |
|
Whether the object has no member. |
|
The end of the members. |
|
The members whose key is |
|
The member |
|
The first member whose key is not before |
|
The member whose key is last in key order, first in reverse. |
|
The end of the members in reverse. |
|
How many members the object has. |
|
The first member whose key is after |
Friends
| Name | Description |
|---|---|
An object as a sorted-map-like container, in the traits’ key order (JavaScript’s by default): |
Template Parameters
| Name | Description |
|---|---|
Handle |
The handle its members are read through, |
See Also
"Key order", in the guide.
webcpp::document::basic_object_by_key::difference_type
The difference of two positions.
webcpp::document::basic_object_by_key::iterator
A bidirectional iterator in key order.
Synopsis
Declared in <webcpp/document/containers.hpp>
using iterator = /* implementation-defined */;
webcpp::document::basic_object_by_key::reference
What dereferencing an iterator gives: a value, made at each step.
webcpp::document::basic_object_by_key::reverse_iterator
A reverse iterator in key order.
Synopsis
Declared in <webcpp/document/containers.hpp>
using reverse_iterator = std::reverse_iterator<iterator>;
webcpp::document::basic_object_by_key::size_type
A count of members.
webcpp::document::basic_object_by_key::storage_type
The document’s storage.
Synopsis
Declared in <webcpp/document/containers.hpp>
using storage_type = /* implementation-defined */::storage;
webcpp::document::basic_object_by_key::traits_type
The document’s traits.
Synopsis
Declared in <webcpp/document/containers.hpp>
using traits_type = /* implementation-defined */::traits;
webcpp::document::basic_object_by_key::value_type
A member: its key, a view of the image valid until the next change, and its handle.
Synopsis
Declared in <webcpp/document/containers.hpp>
using value_type = std::pair</* implementation-defined */, Handle>;
webcpp::document::basic_object_by_key::basic_object_by_key
A view of nothing, which is empty.
webcpp::document::basic_object_by_key::begin
The member whose key is first in key order.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
iterator
begin() const noexcept;
Return Value
The iterator; the end when there is none.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_object_by_key::empty
Whether the object has no member.
webcpp::document::basic_object_by_key::end
The end of the members.
webcpp::document::basic_object_by_key::equal_range
The members whose key is key, as std::map::equal_range: one member or none.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
std::pair<iterator, iterator>
equal_range(std::string_view key) const noexcept;
Return Value
The pair of lower_bound and upper_bound.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_object_by_key::find
The member key, in O(log n).
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
iterator
find(std::string_view key) const noexcept;
Return Value
Its iterator; the end when it is not there.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_object_by_key::lower_bound
The first member whose key is not before key, as std::map::lower_bound: with upper_bound or another lower_bound, the bounds of a range of keys, such as those with a prefix.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
iterator
lower_bound(std::string_view key) const noexcept;
Return Value
The iterator; the end when every key is before key, or when key is not valid UTF-8.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8; it need not be a member’s. |
webcpp::document::basic_object_by_key::rbegin
The member whose key is last in key order, first in reverse.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
reverse_iterator
rbegin() const noexcept;
webcpp::document::basic_object_by_key::rend
The end of the members in reverse.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
reverse_iterator
rend() const noexcept;
webcpp::document::basic_object_by_key::size
How many members the object has.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
size_type
size() const noexcept;
webcpp::document::basic_object_by_key::upper_bound
The first member whose key is after key, as std::map::upper_bound.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
iterator
upper_bound(std::string_view key) const noexcept;
Return Value
The iterator; the end when no key is after key, or when key is not valid UTF-8.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8; it need not be a member’s. |
webcpp::document::basic_object_ref
An object as a sorted-map-like container, in the traits’ key order (JavaScript’s by default): find, insert, insert_or_assign, try_emplace, erase, iteration both ways, and by_key() for prefix and range queries in lexicographic key order.
Synopsis
Declared in <webcpp/document/containers.hpp>
template<class Handle>
class basic_object_ref;
Description
Made by as_object() on a handle, it holds the document’s address and the object’s id, and is cheap to copy: a reference, like std::span, whose constness is its own, so a const view still writes; const_object_ref is the view that only reads. It is a std::ranges::bidirectional_range, a view and a borrowed range, so <algorithm> and <ranges> work on it, and an iterator outlives the view that made it. Its iterators are bidirectional, not random access: each step finds the next member in O(log n), so a jump of n costs n steps.
Iterators and handles hold ids, which no change reuses:
| Change | Iterators and handles | A std::string_view read from a value | | --- | --- | --- | | A member inserted, and any growth of the image | all stay valid | invalid | | A member erased | those to it and its subtree, which read undefined | invalid | | A member’s value replaced | those to its old subtree | invalid | | The document moved from or destroyed | all | invalid |
What can fail returns result: an insertion, which needs room in the image. A lookup returns an iterator, the end when nothing is found. There are no throwing forms: they would differ from these only in their return type, which C++ does not overload on.
Type Aliases
| Name | Description |
|---|---|
The difference of two positions. |
|
A bidirectional iterator in the traits’ key order. |
|
A key as the members take it: UTF-8, whatever the traits’ encoding. |
|
A member’s value: its handle. |
|
What dereferencing an iterator gives: a value, made at each step. |
|
A reverse iterator in the traits’ key order. |
|
A count of members. |
|
The document’s storage. |
|
The document’s traits. |
|
A member: its key, a view of the image valid until the next change, and its handle. |
Member Functions
| Name | Description |
|---|---|
|
A view of nothing, which is empty and fails every insertion with |
The first member in the traits’ key order. |
|
The members in lexicographic key order, by UTF-16 code units, with bounds for prefix and range queries. |
|
Erases every member, which needs no room. |
|
Whether the object has the member |
|
How many members have the key |
|
Whether the object has no member. |
|
The end of the members, which every change leaves valid. |
|
|
|
The member |
|
Inserts the member |
|
Inserts the member |
|
The last member in the traits’ key order, first in reverse. |
|
The end of the members in reverse. |
|
How many members the object has, |
|
Inserts the member |
Friends
| Name | Description |
|---|---|
A handle that reads and writes: one place in one document, as |
|
A read-only handle: one place in one document, cheap to copy, which reads what is there now. |
Template Parameters
| Name | Description |
|---|---|
Handle |
The handle its members are read through: |
See Also
"Handles", in the guide.
"Key order", in the guide.
webcpp::document::basic_object_ref::difference_type
The difference of two positions.
webcpp::document::basic_object_ref::iterator
A bidirectional iterator in the traits’ key order.
Synopsis
Declared in <webcpp/document/containers.hpp>
using iterator = /* implementation-defined */;
webcpp::document::basic_object_ref::key_type
A key as the members take it: UTF-8, whatever the traits’ encoding.
webcpp::document::basic_object_ref::mapped_type
A member’s value: its handle.
webcpp::document::basic_object_ref::reference
What dereferencing an iterator gives: a value, made at each step.
webcpp::document::basic_object_ref::reverse_iterator
A reverse iterator in the traits’ key order.
Synopsis
Declared in <webcpp/document/containers.hpp>
using reverse_iterator = std::reverse_iterator<iterator>;
webcpp::document::basic_object_ref::size_type
A count of members.
webcpp::document::basic_object_ref::storage_type
The document’s storage.
Synopsis
Declared in <webcpp/document/containers.hpp>
using storage_type = /* implementation-defined */::storage;
webcpp::document::basic_object_ref::traits_type
The document’s traits.
Synopsis
Declared in <webcpp/document/containers.hpp>
using traits_type = /* implementation-defined */::traits;
webcpp::document::basic_object_ref::value_type
A member: its key, a view of the image valid until the next change, and its handle.
Synopsis
Declared in <webcpp/document/containers.hpp>
using value_type = std::pair</* implementation-defined */, Handle>;
webcpp::document::basic_object_ref::basic_object_ref
A view of nothing, which is empty and fails every insertion with errc::type.
webcpp::document::basic_object_ref::begin
The first member in the traits’ key order.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
iterator
begin() const noexcept;
Return Value
The iterator; the end when there is none.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_object_ref::by_key
The members in lexicographic key order, by UTF-16 code units, with bounds for prefix and range queries.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
basic_object_by_key<Handle>
by_key() const noexcept;
webcpp::document::basic_object_ref::clear
Erases every member, which needs no room.
Synopsis
Declared in <webcpp/document/containers.hpp>
void
clear() const noexcept
requires detail::writable_handle<Handle>;
webcpp::document::basic_object_ref::contains
Whether the object has the member key, key in o.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
bool
contains(std::string_view key) const noexcept;
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_object_ref::count
How many members have the key key: 1 or 0, as std::map::count.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
size_type
count(std::string_view key) const noexcept;
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_object_ref::empty
Whether the object has no member.
webcpp::document::basic_object_ref::end
The end of the members, which every change leaves valid.
webcpp::document::basic_object_ref::erase
erase overloads
Synopses
Declared in <webcpp/document/containers.hpp>
Erases the member at pos and its subtree, delete o[key].
Erases the member key and its subtree, delete o[key].
[[nodiscard]]
result<size_type>
erase(std::string_view key) const noexcept
requires detail::writable_handle<Handle>;
Erases the members from first to last, last excluded, and their subtrees.
Return Value
-
The iterator of the member after it; the end when
posis the end, of another container, or at a member already erased. -
How many members were erased, 1 or 0;
errc::typeonce this is no object. -
last.
Parameters
| Name | Description |
|---|---|
pos |
An iterator of this object at a member. |
key |
The key, in UTF-8. |
first |
An iterator of this object. |
last |
An iterator of this object at or after |
webcpp::document::basic_object_ref::erase
Erases the member at pos and its subtree, delete o[key].
Synopsis
Declared in <webcpp/document/containers.hpp>
Return Value
The iterator of the member after it; the end when pos is the end, of another container, or at a member already erased.
Parameters
| Name | Description |
|---|---|
pos |
An iterator of this object at a member. |
webcpp::document::basic_object_ref::erase
Erases the member key and its subtree, delete o[key].
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
result<size_type>
erase(std::string_view key) const noexcept
requires detail::writable_handle<Handle>;
Return Value
How many members were erased, 1 or 0; errc::type once this is no object.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_object_ref::erase
Erases the members from first to last, last excluded, and their subtrees.
Synopsis
Declared in <webcpp/document/containers.hpp>
Return Value
last.
Parameters
| Name | Description |
|---|---|
first |
An iterator of this object. |
last |
An iterator of this object at or after |
webcpp::document::basic_object_ref::find
The member key, in O(log n).
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
iterator
find(std::string_view key) const noexcept;
Return Value
Its iterator; the end when it is not there.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_object_ref::insert
Inserts the member key holding v unless it is there, as std::map::insert: a new key takes the place the traits’ policy gives it, and an existing one is left as it is.
Synopsis
Declared in <webcpp/document/containers.hpp>
template<class T>
[[nodiscard]]
result<std::pair<iterator, bool>>
insert(
std::string_view key,
T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;
Return Value
The member’s iterator and whether it was inserted; errc::type once this is no object, for a key that is not valid UTF-8 and for a source that reads undefined, errc::capacity when there is no room, and the errors of set for the value.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type, any that |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
v |
The value; a handle or a document is copied deeply. |
webcpp::document::basic_object_ref::insert_or_assign
Inserts the member key holding v, or assigns v to it when it is there, where it keeps its place: JavaScript’s o[key]= v.
Synopsis
Declared in <webcpp/document/containers.hpp>
template<class T>
[[nodiscard]]
result<std::pair<iterator, bool>>
insert_or_assign(
std::string_view key,
T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;
Return Value
The member’s iterator and whether it was inserted; the errors of insert.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type, any that |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
v |
The value; a handle or a document is copied deeply. |
webcpp::document::basic_object_ref::rbegin
The last member in the traits’ key order, first in reverse.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
reverse_iterator
rbegin() const noexcept;
webcpp::document::basic_object_ref::rend
The end of the members in reverse.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
reverse_iterator
rend() const noexcept;
webcpp::document::basic_object_ref::size
How many members the object has, Object.keys(o).length.
Synopsis
Declared in <webcpp/document/containers.hpp>
[[nodiscard]]
size_type
size() const noexcept;
webcpp::document::basic_object_ref::try_emplace
Inserts the member key holding v unless it is there, as std::map::try_emplace.
Synopsis
Declared in <webcpp/document/containers.hpp>
template<class T>
[[nodiscard]]
result<std::pair<iterator, bool>>
try_emplace(
std::string_view key,
T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;
Description
It is insert, since neither builds a value before the key is known to be absent, so v is read only when it is stored.
Return Value
The member’s iterator and whether it was inserted; the errors of insert.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type, any that |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
v |
The value; a handle or a document is copied deeply. |
webcpp::document::basic_parser
The streaming parser: write takes the text in chunks, finish returns the document.
Synopsis
Declared in <webcpp/document/parser.hpp>
template<
class Traits = default_traits,
class Storage = owned>
class basic_parser;
Description
Boost.JSON’s basic_parser is fed each chunk, and its handler builds the document node by node, by ids, so that the document may grow and move between chunks. A failed write leaves the parser failed: every later call returns the same error, until reset. The parser raises nothing of its own, since its input is the network’s; an exception a callback throws propagates from write or finish, and leaves the parser failed: every later call throws it again, until reset.
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Destroys the parser and the document it was building. |
Assignment operators |
|
The code of Boost.JSON behind an |
|
Ends the text and returns the document, without the values |
|
Delivers each value whose path matches |
|
How many bytes of the text were consumed. |
|
Delivers each value whose path matches |
|
Makes the parser ready for another text, its selectors kept. |
|
Feeds the next chunk of the text, delivering each value a selector matches as it completes. |
Friends
| Name | Description |
|---|---|
The parser of a stream of documents: |
Template Parameters
See Also
"Parsing", in the guide.
webcpp::document::basic_parser::basic_parser
Constructors
Synopses
Declared in <webcpp/document/parser.hpp>
A parser is not copied: its handler and its document refer to it.
basic_parser(basic_parser const&) = delete;
A parser is not moved: its handler and its document refer to it.
basic_parser(basic_parser&&) = delete;
A parser of an owned document.
explicit
basic_parser(parse_options const& o = {}) noexcept
requires std::same_as<Storage, owned>;
A parser of a document in a buffer its user owns, formatted when the text begins.
explicit
basic_parser(
std::span<std::byte> buffer,
parse_options const& o = {}) noexcept
requires std::same_as<Storage, external>;
Parameters
| Name | Description |
|---|---|
o |
The limits and the revivers. |
buffer |
The buffer, 16-aligned, which outlives the parser and its document. |
webcpp::document::basic_parser::basic_parser
A parser is not copied: its handler and its document refer to it.
webcpp::document::basic_parser::basic_parser
A parser is not moved: its handler and its document refer to it.
webcpp::document::basic_parser::basic_parser
A parser of an owned document.
Synopsis
Declared in <webcpp/document/parser.hpp>
explicit
basic_parser(parse_options const& o = {}) noexcept
requires std::same_as<Storage, owned>;
Parameters
| Name | Description |
|---|---|
o |
The limits and the revivers. |
webcpp::document::basic_parser::basic_parser
A parser of a document in a buffer its user owns, formatted when the text begins.
Synopsis
Declared in <webcpp/document/parser.hpp>
explicit
basic_parser(
std::span<std::byte> buffer,
parse_options const& o = {}) noexcept
requires std::same_as<Storage, external>;
Parameters
| Name | Description |
|---|---|
buffer |
The buffer, 16-aligned, which outlives the parser and its document. |
o |
The limits and the revivers. |
webcpp::document::basic_parser::~basic_parser
Destroys the parser and the document it was building.
webcpp::document::basic_parser::operator=
Assignment operators
Synopses
Declared in <webcpp/document/parser.hpp>
A parser is not copied: its handler and its document refer to it.
basic_parser&
operator=(basic_parser const&) = delete;
A parser is not moved: its handler and its document refer to it.
basic_parser&
operator=(basic_parser&&) = delete;
webcpp::document::basic_parser::operator=
A parser is not copied: its handler and its document refer to it.
Synopsis
Declared in <webcpp/document/parser.hpp>
basic_parser&
operator=(basic_parser const&) = delete;
webcpp::document::basic_parser::operator=
A parser is not moved: its handler and its document refer to it.
webcpp::document::basic_parser::cause
The code of Boost.JSON behind an errc::syntax.
Synopsis
Declared in <webcpp/document/parser.hpp>
[[nodiscard]]
boost::system::error_code
cause() const noexcept;
webcpp::document::basic_parser::finish
Ends the text and returns the document, without the values on took.
Description
After it the parser takes no more text, which is errc::syntax, until reset.
Return Value
The document; the errors of write, and errc::syntax for text that ends early.
webcpp::document::basic_parser::observe
Delivers each value whose path matches selector once it completes, and keeps it.
Synopsis
Declared in <webcpp/document/parser.hpp>
template<class F>
result<void>
observe(
std::string_view selector,
F&& f)
requires detail::selector_callback<std::decay_t<F>, Traits, Storage>;
Return Value
Success; errc::type for a malformed selector.
Template Parameters
| Name | Description |
|---|---|
F |
A callable taking a read-only handle and a |
Parameters
| Name | Description |
|---|---|
selector |
The selector, as |
f |
The callback. |
See Also
"Selectors", in the guide.
webcpp::document::basic_parser::offset
How many bytes of the text were consumed.
Return Value
The count; after an errc::syntax, the offset where the error was found.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_parser::on
Delivers each value whose path matches selector once it completes, then removes it from the document, its blocks zeroed and freed: a stream of a million elements needs the memory of one.
Synopsis
Declared in <webcpp/document/parser.hpp>
template<class F>
result<void>
on(
std::string_view selector,
F&& f)
requires detail::selector_callback<std::decay_t<F>, Traits, Storage>;
Description
The selector is $ then any sequence of .name, ['name'], [n], [] and .*. A scalar completes when its token ends, an array or object when it closes; a value inside another that also matches is delivered first.
Return Value
Success; errc::type for a malformed selector.
Template Parameters
| Name | Description |
|---|---|
F |
A callable taking a read-only handle and a |
Parameters
| Name | Description |
|---|---|
selector |
The selector. |
f |
The callback. |
See Also
"Selectors", in the guide.
webcpp::document::basic_parser::reset
Makes the parser ready for another text, its selectors kept.
webcpp::document::basic_parser::write
Feeds the next chunk of the text, delivering each value a selector matches as it completes.
Return Value
Success; errc::syntax for malformed text, a second text after the first included, errc::range for a number the traits refuse, errc::capacity past a limit, or the error a callback returned.
Parameters
| Name | Description |
|---|---|
chunk |
The chunk: any split of the text, down to one byte. |
webcpp::document::basic_stream_parser
The parser of a stream of documents: write takes the bytes in chunks of any size, down to one byte, and each document is delivered as soon as the bytes that end it are consumed.
Synopsis
Declared in <webcpp/document/stream.hpp>
template<class Traits = default_traits>
class basic_stream_parser;
Description
It is sans-IO: it reads no socket or file, so it serves any source, a socket read by hand, an actor’s messages, or JavaScript through Emscripten; <webcpp/document/asio.hpp> reads it from Boost.Asio’s streams. Each document is parsed as parse parses one text, with the same key order, numbers and limits, into a new owned document. The parser reads one document at a time: at a document’s end it moves it to a queue that next takes from, or hands it to the callback of the push form, and begins the next on the bytes that follow, in the same chunk.
It raises nothing of its own, since its input is the network’s: every failure is returned. An exception the callback of the push form throws propagates; the bytes up to the document it was given are consumed, which offset counts, and the parser takes the rest by another write.
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Destroys the parser, the document it was reading and those |
Assignment operators |
|
How many documents were delivered, by |
|
The code of Boost.JSON behind the error last delivered, when it is an |
|
The line where the error last delivered was found: by |
|
The offset in the stream where the error last delivered was found. |
|
|
|
The line of the last byte consumed, from 1; a |
|
Takes the oldest document the stream completed. |
|
How many bytes of the stream were consumed. |
|
Makes the parser ready for another stream: the documents not taken and any error dropped. |
|
|
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits of the documents. |
See Also
"Streams of documents", in the guide.
webcpp::document::basic_stream_parser::basic_stream_parser
Constructors
Synopses
Declared in <webcpp/document/stream.hpp>
A parser is not copied: its inner parser refers to itself.
basic_stream_parser(basic_stream_parser const&) = delete;
A parser is not moved: its inner parser refers to itself.
basic_stream_parser(basic_stream_parser&&) = delete;
A parser of a stream framed as o says.
explicit
basic_stream_parser(stream_options const& o = {});
Parameters
| Name | Description |
|---|---|
o |
The framing, the policy on errors, and the limits and revivers of each document. |
webcpp::document::basic_stream_parser::basic_stream_parser
A parser is not copied: its inner parser refers to itself.
Synopsis
Declared in <webcpp/document/stream.hpp>
basic_stream_parser(basic_stream_parser const&) = delete;
webcpp::document::basic_stream_parser::basic_stream_parser
A parser is not moved: its inner parser refers to itself.
Synopsis
Declared in <webcpp/document/stream.hpp>
basic_stream_parser(basic_stream_parser&&) = delete;
webcpp::document::basic_stream_parser::basic_stream_parser
A parser of a stream framed as o says.
Synopsis
Declared in <webcpp/document/stream.hpp>
explicit
basic_stream_parser(stream_options const& o = {});
Description
on_error::skip with framing::concatenated is refused: every call of the parser returns boost::system::errc::invalid_argument.
Parameters
| Name | Description |
|---|---|
o |
The framing, the policy on errors, and the limits and revivers of each document. |
webcpp::document::basic_stream_parser::~basic_stream_parser
Destroys the parser, the document it was reading and those next has not taken.
webcpp::document::basic_stream_parser::operator=
Assignment operators
Synopses
Declared in <webcpp/document/stream.hpp>
A parser is not copied: its inner parser refers to itself.
basic_stream_parser&
operator=(basic_stream_parser const&) = delete;
A parser is not moved: its inner parser refers to itself.
basic_stream_parser&
operator=(basic_stream_parser&&) = delete;
webcpp::document::basic_stream_parser::operator=
A parser is not copied: its inner parser refers to itself.
Synopsis
Declared in <webcpp/document/stream.hpp>
basic_stream_parser&
operator=(basic_stream_parser const&) = delete;
webcpp::document::basic_stream_parser::operator=
A parser is not moved: its inner parser refers to itself.
Synopsis
Declared in <webcpp/document/stream.hpp>
basic_stream_parser&
operator=(basic_stream_parser&&) = delete;
webcpp::document::basic_stream_parser::documents
How many documents were delivered, by next or to a callback.
Synopsis
Declared in <webcpp/document/stream.hpp>
[[nodiscard]]
uint64_t
documents() const noexcept;
webcpp::document::basic_stream_parser::error_cause
The code of Boost.JSON behind the error last delivered, when it is an errc::syntax that Boost.JSON’s parser found.
Synopsis
Declared in <webcpp/document/stream.hpp>
[[nodiscard]]
boost::system::error_code
error_cause() const noexcept;
Return Value
The code, extra_data for a second document on a line or in a record; or none.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_stream_parser::error_line
The line where the error last delivered was found: by next, to a callback, or returned by write or finish.
Synopsis
Declared in <webcpp/document/stream.hpp>
[[nodiscard]]
uint64_t
error_line() const noexcept;
webcpp::document::basic_stream_parser::error_offset
The offset in the stream where the error last delivered was found.
Synopsis
Declared in <webcpp/document/stream.hpp>
[[nodiscard]]
uint64_t
error_offset() const noexcept;
Return Value
The offset of the byte that the error was found at; 0 before any error.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_stream_parser::finish
finish overloads
Synopses
Declared in <webcpp/document/stream.hpp>
Ends the stream, closing its last document, a top-level number or a last line without its n, and queueing it for next.
Ends the stream, handing the last document to a callback, after those next has not taken.
template<class F>
requires detail::stream_callback<F, Traits>
result<std::size_t>
finish(F&& on_document);
Return Value
-
How many documents, and dropped documents’ errors, it queued; or the error that stopped the stream, as
writereturns it, anderrc::syntaxfor a document that the stream’s end cuts short. -
How many documents, and dropped documents’ errors, it handed over; or the error that stopped the stream, as the other
finishreturns it.
Template Parameters
| Name | Description |
|---|---|
F |
A callable taking a |
Parameters
| Name | Description |
|---|---|
on_document |
The callback, as the push form of |
webcpp::document::basic_stream_parser::finish
Ends the stream, closing its last document, a top-level number or a last line without its n, and queueing it for next.
Description
After it the parser takes no more bytes, which is errc::syntax, until reset; an empty write, and another finish, complete nothing.
Return Value
How many documents, and dropped documents’ errors, it queued; or the error that stopped the stream, as write returns it, and errc::syntax for a document that the stream’s end cuts short.
webcpp::document::basic_stream_parser::finish
Ends the stream, handing the last document to a callback, after those next has not taken.
Synopsis
Declared in <webcpp/document/stream.hpp>
template<class F>
requires detail::stream_callback<F, Traits>
result<std::size_t>
finish(F&& on_document);
Return Value
How many documents, and dropped documents’ errors, it handed over; or the error that stopped the stream, as the other finish returns it.
Template Parameters
| Name | Description |
|---|---|
F |
A callable taking a |
Parameters
| Name | Description |
|---|---|
on_document |
The callback, as the push form of |
webcpp::document::basic_stream_parser::finished
Description
Tip: async_read_document completes with eof without reading once it holds, for a socket reports its end to one read only on Linux’s epoll.
webcpp::document::basic_stream_parser::line
The line of the last byte consumed, from 1; a n belongs to the line it ends.
webcpp::document::basic_stream_parser::next
Takes the oldest document the stream completed.
Synopsis
Declared in <webcpp/document/stream.hpp>
Return Value
The document; none when no document is complete; with on_error::skip, the error of a document dropped, in its place, which error_line names; with on_error::stop, once the documents before it are taken, the error that stopped the stream, every time.
webcpp::document::basic_stream_parser::offset
How many bytes of the stream were consumed.
Return Value
The count; after the stream stopped, the offset where its error was found.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_stream_parser::reset
Makes the parser ready for another stream: the documents not taken and any error dropped.
webcpp::document::basic_stream_parser::write
write overloads
Synopses
Declared in <webcpp/document/stream.hpp>
Consumes the next bytes of the stream, queueing each document they complete for next.
result<std::size_t>
write(std::string_view bytes);
Consumes the next bytes of the stream, handing each document they complete to a callback, in order, after those next has not taken.
template<class F>
requires detail::stream_callback<F, Traits>
result<std::size_t>
write(
std::string_view bytes,
F&& on_document);
Return Value
-
How many documents, and dropped documents’ errors, it queued; or the error that stopped the stream:
errc::syntaxfor malformed text,errc::rangefor a number the traits refuse,errc::capacitypast a limit. The documents completed before it stay queued. -
How many documents, and dropped documents’ errors, it handed over; or the error that stopped the stream, as
writereturns it.
Template Parameters
| Name | Description |
|---|---|
F |
A callable taking a |
Parameters
| Name | Description |
|---|---|
bytes |
The bytes: any split of the stream, down to one byte. |
on_document |
The callback: given each document, and with |
webcpp::document::basic_stream_parser::write
Consumes the next bytes of the stream, queueing each document they complete for next.
Synopsis
Declared in <webcpp/document/stream.hpp>
result<std::size_t>
write(std::string_view bytes);
Return Value
How many documents, and dropped documents’ errors, it queued; or the error that stopped the stream: errc::syntax for malformed text, errc::range for a number the traits refuse, errc::capacity past a limit. The documents completed before it stay queued.
Parameters
| Name | Description |
|---|---|
bytes |
The bytes: any split of the stream, down to one byte. |
webcpp::document::basic_stream_parser::write
Consumes the next bytes of the stream, handing each document they complete to a callback, in order, after those next has not taken.
Synopsis
Declared in <webcpp/document/stream.hpp>
template<class F>
requires detail::stream_callback<F, Traits>
result<std::size_t>
write(
std::string_view bytes,
F&& on_document);
Return Value
How many documents, and dropped documents’ errors, it handed over; or the error that stopped the stream, as write returns it.
Template Parameters
| Name | Description |
|---|---|
F |
A callable taking a |
Parameters
| Name | Description |
|---|---|
bytes |
The bytes: any split of the stream, down to one byte. |
on_document |
The callback: given each document, and with |
webcpp::document::basic_value
A handle that reads and writes: one place in one document, as basic_const_value, with JavaScript’s assignment, delete, Object.assign and the array methods.
Synopsis
Declared in <webcpp/document/value.hpp>
template<
class Traits = default_traits,
class Storage = owned>
class basic_value
: public basic_const_value<Traits, Storage>
Description
Every writing function returns result and either completes or changes nothing, a full heap included. Assignment to a handle writes to its place, with exceptions only: it never rebinds the handle.
So a handle is not a regular value, and must not be swapped or sorted: std::swap(x, y) assigns through both, leaving both places holding y’s value, and std::sort over handles writes the document’s values the same way.
Base Classes
| Name | Description |
|---|---|
A read-only handle: one place in one document, cheap to copy, which reads what is there now. |
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Nothing to free: the document owns the value. |
Assignment operators |
|
The array as a vector-like container that reads and writes: |
|
The object as a sorted-map-like container that reads and writes: |
|
|
|
|
|
|
|
Subscript operators |
|
|
|
|
|
|
|
Stores a value at this place, |
|
|
|
|
|
|
|
|
Using Declarations
| Name | Description |
|---|---|
The read-only view of an array, for a |
|
The read-only view of an object, for a |
|
The read-only |
|
The read-only navigation of a read-only handle, for a |
Friends
| Name | Description |
|---|---|
A read-only handle: one place in one document, cheap to copy, which reads what is there now. |
|
A JSON document: one value, in one relocatable image. |
Non-Member Functions
| Name | Description |
|---|---|
Whether two values are equal, structurally: the same kind; numbers by value ( |
Template Parameters
| Name | Description |
|---|---|
Traits |
The document’s traits. |
Storage |
The document’s storage. |
See Also
"Handles", in the guide.
"Arrays", in the guide.
webcpp::document::basic_value::basic_value
Constructors
Synopses
Declared in <webcpp/document/value.hpp>
A handle that names nothing.
basic_value() noexcept = default;
A handle of the same place.
basic_value(basic_value const& other) = default;
A handle of the same place.
basic_value(basic_value&& other) noexcept = default;
Parameters
| Name | Description |
|---|---|
other |
The handle. |
webcpp::document::basic_value::basic_value
A handle that names nothing.
webcpp::document::basic_value::basic_value
A handle of the same place.
Parameters
| Name | Description |
|---|---|
other |
The handle. |
webcpp::document::basic_value::basic_value
A handle of the same place.
Synopsis
Declared in <webcpp/document/value.hpp>
basic_value(basic_value&& other) noexcept = default;
Parameters
| Name | Description |
|---|---|
other |
The handle. |
webcpp::document::basic_value::~basic_value
Nothing to free: the document owns the value.
webcpp::document::basic_value::operator=
Assignment operators
Synopses
Declared in <webcpp/document/value.hpp>
Stores a deep copy of another handle’s value at this place, as set does, throwing on failure; only with exceptions.
basic_value<Traits, Storage>&
operator=(basic_value const& v)
requires detail::exceptions_enabled<Traits>;
Stores a value at this place, v = x, as set does, throwing on failure; only with exceptions.
template<class T>
basic_value<Traits, Storage>&
operator=(T const& v)
requires detail::exceptions_enabled<T> &&
(detail::settable<Traits, T> || std::same_as<T, undefined_t>);
Exceptions
| Name | Thrown on |
|---|---|
For a place that is nothing. |
|
When there is no room. |
Return Value
This handle.
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type. |
Parameters
| Name | Description |
|---|---|
v |
The handle. |
See Also
"The sugar that throws", in the guide.
webcpp::document::basic_value::operator=
Stores a deep copy of another handle’s value at this place, as set does, throwing on failure; only with exceptions.
Synopsis
Declared in <webcpp/document/value.hpp>
basic_value<Traits, Storage>&
operator=(basic_value const& v)
requires detail::exceptions_enabled<Traits>;
Description
It never rebinds the handle: assignment to a handle writes to its place.
Exceptions
| Name | Thrown on |
|---|---|
For a place that is nothing. |
|
When there is no room. |
Return Value
This handle.
Parameters
| Name | Description |
|---|---|
v |
The handle. |
webcpp::document::basic_value::operator=
Stores a value at this place, v = x, as set does, throwing on failure; only with exceptions.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class T>
basic_value<Traits, Storage>&
operator=(T const& v)
requires detail::exceptions_enabled<T> &&
(detail::settable<Traits, T> || std::same_as<T, undefined_t>);
Exceptions
| Name | Thrown on |
|---|---|
For a place that is nothing. |
|
When there is no room. |
Return Value
This handle.
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type. |
Parameters
| Name | Description |
|---|---|
v |
The value. |
See Also
"The sugar that throws", in the guide.
webcpp::document::basic_value::as_array
The array as a vector-like container that reads and writes: push_back, insert, erase, resize, and iterators that <algorithm> and <ranges> take.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<basic_array_ref<basic_value<Traits, Storage>>>
as_array() noexcept;
Return Value
The view, defined in containers.hpp; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
See Also
"Arrays", in the guide.
webcpp::document::basic_value::as_object
The object as a sorted-map-like container that reads and writes: find, insert, erase, bounds by key, and iterators that <algorithm> and <ranges> take.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<basic_object_ref<basic_value<Traits, Storage>>>
as_object() noexcept;
Return Value
The view, defined in containers.hpp; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
See Also
"Key order", in the guide.
webcpp::document::basic_value::assign_from
Object.assign(v, source): sets each member of source here, in its order, as one operation.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class S>
[[nodiscard]]
result<void>
assign_from(basic_const_value<Traits, S> const& source) noexcept;
Description
An array’s elements are its members "0" to "n-1", and null or undefined adds nothing.
Return Value
Success; errc::type when this is not an object or source is a scalar, errc::capacity when there is no room.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
S |
The source’s storage. |
Parameters
| Name | Description |
|---|---|
source |
The source. |
webcpp::document::basic_value::at
Array.prototype.at: the element index, a negative index counting from the end.
Return Value
Its handle; nothing out of range or for any other kind.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::basic_value::erase
delete v[key]: erases an object’s member and its subtree.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<bool>
erase(std::string_view key) noexcept;
Return Value
Whether it was there; errc::type for any other kind than an object.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_value::operator[]
Subscript operators
Synopses
Declared in <webcpp/document/value.hpp>
The member key, v.key, which never fails.
basic_value
operator[](std::string_view key) noexcept;
The element index, v[index], which never fails.
basic_value
operator[](uint64_t index) noexcept;
Return Value
-
Its handle: the member, a missing member of an object, or nothing.
-
Its handle: the element, a missing element of an array, or nothing.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
index |
The index. |
webcpp::document::basic_value::operator[]
The member key, v.key, which never fails.
Synopsis
Declared in <webcpp/document/value.hpp>
basic_value
operator[](std::string_view key) noexcept;
Return Value
Its handle: the member, a missing member of an object, or nothing.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::basic_value::operator[]
The element index, v[index], which never fails.
Return Value
Its handle: the element, a missing element of an array, or nothing.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::basic_value::pop
Array.prototype.pop: removes the last element.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<basic_document<Traits, owned>>
pop() noexcept;
Return Value
A new owned document holding it, or holding undefined when the array is empty; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_value::push
Array.prototype.push: appends each value.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class... T>
[[nodiscard]]
result<uint64_t>
push(T const&... v) noexcept
requires (detail::settable<Traits, T> && ...);
Return Value
The new length; errc::type for any other kind than an array, errc::capacity when there is no room, and the errors of set for a value.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The values’ types, each settable. |
Parameters
| Name | Description |
|---|---|
v |
The values. |
webcpp::document::basic_value::reverse
Array.prototype.reverse: reverses the elements in place.
webcpp::document::basic_value::set
Stores a value at this place, v = x: a missing member is created, an element past the end fills the elements between with null, and an existing value is replaced.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class T>
[[nodiscard]]
result<void>
set(T const& v) noexcept
requires detail::settable<Traits, T> || std::same_as<T, undefined_t>;
Description
Settable: bool; any integer type; the decimal types (finite, else errc::range); the traits’ number; std::string_view, const char*, std::string (valid UTF-8, else errc::type), std::u16string_view; std::span<const std::byte> (bytes); a date_ms (within JavaScript’s range, else errc::range); nullptr or null; the literals object and array; a handle or a document of the same traits, copied deeply; and undefined, which erases a member, stores null in an element and makes a root hold undefined. Numbers are stored by value.
Return Value
Success; errc::type for a place that is nothing, errc::capacity when there is no room, and the errors above.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The value’s type. |
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::basic_value::set_length
a.length = n: truncates the array, or appends null up to n.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<void>
set_length(uint64_t n) noexcept;
Parameters
| Name | Description |
|---|---|
n |
The new length. |
webcpp::document::basic_value::shift
Array.prototype.shift: removes the first element.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<basic_document<Traits, owned>>
shift() noexcept;
Return Value
A new owned document holding it, or holding undefined when the array is empty; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
webcpp::document::basic_value::splice
splice overloads
Synopses
Declared in <webcpp/document/value.hpp>
Array.prototype.splice(start): removes every element from start.
[[nodiscard]]
result<basic_document<Traits, owned>>
splice(int64_t start) noexcept;
Array.prototype.splice: removes count elements from start and inserts each value there.
template<class... T>
[[nodiscard]]
result<basic_document<Traits, owned>>
splice(
int64_t start,
int64_t count,
T const&... v) noexcept
requires (detail::settable<Traits, T> && ...);
Return Value
A new owned document holding the array of the removed elements; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The values’ types, each settable. |
Parameters
| Name | Description |
|---|---|
start |
The first index, a negative one counting from the end. |
count |
How many to remove. |
v |
The values to insert. |
See Also
"Arrays", in the guide.
webcpp::document::basic_value::splice
Array.prototype.splice(start): removes every element from start.
Synopsis
Declared in <webcpp/document/value.hpp>
[[nodiscard]]
result<basic_document<Traits, owned>>
splice(int64_t start) noexcept;
Return Value
A new owned document holding the array of the removed elements; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
start |
The first index, a negative one counting from the end. |
webcpp::document::basic_value::splice
Array.prototype.splice: removes count elements from start and inserts each value there.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class... T>
[[nodiscard]]
result<basic_document<Traits, owned>>
splice(
int64_t start,
int64_t count,
T const&... v) noexcept
requires (detail::settable<Traits, T> && ...);
Description
A negative start counts from the end, clamped at 0, and one past the length is the length; a negative count is 0, and one past the end removes to the end.
Return Value
A new owned document holding the array of the removed elements; errc::type for any other kind.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The values’ types, each settable. |
Parameters
| Name | Description |
|---|---|
start |
The first index. |
count |
How many to remove. |
v |
The values to insert. |
See Also
"Arrays", in the guide.
webcpp::document::basic_value::unshift
Array.prototype.unshift: inserts each value at the front, in order.
Synopsis
Declared in <webcpp/document/value.hpp>
template<class... T>
[[nodiscard]]
result<uint64_t>
unshift(T const&... v) noexcept
requires (detail::settable<Traits, T> && ...);
Return Value
The new length; errc::type for any other kind than an array, errc::capacity when there is no room, and the errors of set for a value.
|
Note
|
The return value should not be discarded. |
Template Parameters
| Name | Description |
|---|---|
T |
The values’ types, each settable. |
Parameters
| Name | Description |
|---|---|
v |
The values. |
webcpp::document::basic_value::as_array
The read-only view of an array, for a const handle.
Synopsis
Declared in <webcpp/document/value.hpp>
using basic_const_value<Traits, Storage>::as_array;
webcpp::document::basic_value::as_object
The read-only view of an object, for a const handle.
Synopsis
Declared in <webcpp/document/value.hpp>
using basic_const_value<Traits, Storage>::as_object;
webcpp::document::basic_value::at
The read-only at of a read-only handle, for a const one.
webcpp::document::basic_value::operator[]
The read-only navigation of a read-only handle, for a const one.
Synopsis
Declared in <webcpp/document/value.hpp>
using basic_const_value<Traits, Storage>::operator[];
webcpp::document::capacity_error
No room left: a full external buffer, a document at its limit, a parser’s limit.
Base Classes
| Name | Description |
|---|---|
What every exception of document derives from: a |
Using Declarations
| Name | Description |
|---|---|
Constructs the exception of a code, as |
webcpp::document::capacity_error::capacity_error
Constructs the exception of a code, as error does.
Introduced Symbols
| Name | Description |
|---|---|
|
Constructs the exception of |
webcpp::document::decimal_digits
A finite decimal number as JavaScript’s Number::toString sees it: its value is 0.d1 d2 ... dk times ten to the power point, with k the count of its significant digits.
Description
Zero has no digits: count is 0, point 0 and negative false. Any other value has between 1 and 34 digits, the first and the last of them not '0', so that every number has exactly one decimal_digits: 1.5, 1.50 and 15e-1 are all {false, "15", 2, 1}.
Data Members
webcpp::document::decimal_digits::count
How many significant digits there are, k; 0 for zero.
webcpp::document::decimal_digits::digits
The significant digits, '0' to '9', in the first count places.
webcpp::document::decimal_digits::negative
Whether the number is below zero; never true for zero.
webcpp::document::decimal_digits::point
Where the decimal point is, n: after this many digits, which may be negative.
webcpp::document::default_traits
The default traits: exact numbers, UTF-8 strings, base64 bytes and JavaScript’s key order.
Description
A number is stored by value: an integer that fits integer as one, any other as number.
Type Aliases
| Name | Description |
|---|---|
The behaviour of arrays. |
|
The type of |
|
How |
|
The type of a date. |
|
The type of a number that is an integer and fits. |
|
The type of any other number: exact, 34 significant digits. |
|
The order of an object’s keys. |
|
The encoding strings and keys are stored in. |
Derived Classes
| Name | Description |
|---|---|
The traits of JavaScript itself: doubles, UTF-16 strings, and bytes written as a Uint8Array. |
See Also
"Traits", in the guide.
webcpp::document::default_traits::array
The behaviour of arrays.
webcpp::document::default_traits::boolean
The type of true and false.
webcpp::document::default_traits::bytes
How stringify writes bytes.
webcpp::document::default_traits::date
The type of a date.
webcpp::document::default_traits::integer
The type of a number that is an integer and fits.
webcpp::document::default_traits::number
The type of any other number: exact, 34 significant digits.
webcpp::document::default_traits::object
The order of an object’s keys.
webcpp::document::default_traits::string
The encoding strings and keys are stored in.
webcpp::document::dense
The array policy: no holes, and writing past the end fills with null.
Static Data Members
| Name | Description |
|---|---|
The policy’s name in the image’s layout tag. |
webcpp::document::dense::name
The policy’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "dense";
webcpp::document::error
What every exception of document derives from: a std::runtime_error holding its code.
Base Classes
| Name | Description |
|---|---|
|
One of two subclasses of exception. |
Derived Classes
| Name | Description |
|---|---|
No room left: a full external buffer, a document at its limit, a parser’s limit. |
|
Bytes that are not a document’s image, an image of another format, or a damaged one. |
|
JavaScript’s |
|
JavaScript’s |
|
JavaScript’s |
See Also
"The sugar that throws", in the guide.
webcpp::document::error::error
Constructors
Synopses
Declared in <webcpp/document/errors.hpp>
What every exception of document derives from: a `std::runtime_error` holding its code.
What every exception of document derives from: a `std::runtime_error` holding its code.
Constructs the exception of code, whose message is the code’s.
explicit
error(boost::system::error_code code);
Parameters
| Name | Description |
|---|---|
code |
The code of the failure. |
See Also
"The sugar that throws", in the guide.
webcpp::document::error::error
Constructs the exception of code, whose message is the code’s.
Parameters
| Name | Description |
|---|---|
code |
The code of the failure. |
webcpp::document::error::code
The code of the failure.
Synopsis
Declared in <webcpp/document/errors.hpp>
[[nodiscard]]
boost::system::error_code
code() const noexcept;
webcpp::document::external
The storage of a document whose image is a buffer its user owns, such as a browser’s memory: when it is full, an operation returns errc::capacity and changes nothing.
See Also
"Images", in the guide.
webcpp::document::image_error
Bytes that are not a document’s image, an image of another format, or a damaged one.
Base Classes
| Name | Description |
|---|---|
What every exception of document derives from: a |
Using Declarations
| Name | Description |
|---|---|
Constructs the exception of a code, as |
webcpp::document::image_error::image_error
Constructs the exception of a code, as error does.
Introduced Symbols
| Name | Description |
|---|---|
|
Constructs the exception of |
webcpp::document::image_header
The 64 bytes at the start of every image, in the byte order of the machine that wrote it.
Description
The heap follows it, at offset 64.
Data Members
| Name | Description |
|---|---|
|
|
The offset of the tree’s container from the image’s start. |
|
A hash of the node’s, the container’s and the heap’s sizes and alignments, and of the traits. |
|
The id the next node created takes; ids are never reused. |
|
Zero. |
|
Zero, and kept for a future version: an image whose reserved bytes are not zero is refused. |
|
Zero for an image that is not sealed; else the image’s CRC-32C, with these eight bytes taken as zero, in the low 32 bits, and the high bit set, which marks the image sealed. |
|
The size of the whole image, header included. |
|
See Also
"Images", in the guide.
webcpp::document::image_header::byte_order
byte_order_mark, as the writer’s machine writes it.
webcpp::document::image_header::container
The offset of the tree’s container from the image’s start.
webcpp::document::image_header::layout
A hash of the node’s, the container’s and the heap’s sizes and alignments, and of the traits.
webcpp::document::image_header::magic
webcpp::document::image_header::next_id
The id the next node created takes; ids are never reused.
webcpp::document::image_header::reserved
Zero.
webcpp::document::image_header::reserved_4
Zero, and kept for a future version: an image whose reserved bytes are not zero is refused.
webcpp::document::image_header::seal
Zero for an image that is not sealed; else the image’s CRC-32C, with these eight bytes taken as zero, in the low 32 bits, and the high bit set, which marks the image sealed.
webcpp::document::image_header::size
The size of the whole image, header included.
webcpp::document::image_header::version
webcpp::document::image_pool
Fixed blocks for images, 16-aligned and zeroed, allocated once: block_bytes times blocks.
Description
A block is leased by acquire, or by a basic_image_reader for each image it reads, and goes back when its lease does. When every block is leased, the pool says errc::capacity, the backpressure of a stream of images, whose reader takes no more bytes until a block is given back. The pool must outlive its leases and its readers; moving it moves neither its blocks nor its leases.
|
Note
|
A pool is not thread-safe: a block is leased and given back, a lease destroyed included, on one thread at a time. |
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Frees the blocks, every one of which must have been given back. |
Assignment operators |
|
Leases a free block, whose bytes are all zero. |
|
How many blocks are free. |
|
The size of each block. |
|
How many blocks the pool has. |
Static Member Functions
| Name | Description |
|---|---|
Makes a pool of |
Friends
| Name | Description |
|---|---|
Reads a stream of images, one after another with nothing between them, into the blocks of an |
See Also
"Images", in the guide.
webcpp::document::image_pool::image_pool
Constructors
Synopses
Declared in <webcpp/document/image_stream.hpp>
A pool is not copied.
image_pool(image_pool const&) = delete;
Takes the blocks of other, which holds none after it; the leases of its blocks stay valid.
image_pool(image_pool&& other) noexcept;
Parameters
| Name | Description |
|---|---|
other |
The pool. |
webcpp::document::image_pool::image_pool
A pool is not copied.
webcpp::document::image_pool::image_pool
Takes the blocks of other, which holds none after it; the leases of its blocks stay valid.
Parameters
| Name | Description |
|---|---|
other |
The pool. |
webcpp::document::image_pool::~image_pool
Frees the blocks, every one of which must have been given back.
webcpp::document::image_pool::operator=
Assignment operators
Synopses
Declared in <webcpp/document/image_stream.hpp>
A pool is not copied.
image_pool&
operator=(image_pool const&) = delete;
Frees this pool’s blocks, then takes the blocks of other, which holds none after it.
image_pool&
operator=(image_pool&& other) noexcept;
Return Value
This pool.
Parameters
| Name | Description |
|---|---|
other |
The pool. |
webcpp::document::image_pool::operator=
A pool is not copied.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
image_pool&
operator=(image_pool const&) = delete;
webcpp::document::image_pool::operator=
Frees this pool’s blocks, then takes the blocks of other, which holds none after it.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
image_pool&
operator=(image_pool&& other) noexcept;
Return Value
This pool.
Parameters
| Name | Description |
|---|---|
other |
The pool. |
webcpp::document::image_pool::acquire
Leases a free block, whose bytes are all zero.
Return Value
The lease; errc::capacity when every block is leased, or for a moved-from pool.
webcpp::document::image_pool::available
How many blocks are free.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
std::size_t
available() const noexcept;
webcpp::document::image_pool::block_bytes
The size of each block.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
std::size_t
block_bytes() const noexcept;
webcpp::document::image_pool::blocks
How many blocks the pool has.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
std::size_t
blocks() const noexcept;
webcpp::document::image_pool::create
Makes a pool of blocks blocks of block_bytes bytes each, in one allocation.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
static
result<image_pool>
create(
std::size_t block_bytes,
std::size_t blocks) noexcept;
Return Value
The pool; errc::range for a size or a count outside those bounds, and errc::capacity when the allocation fails.
Parameters
| Name | Description |
|---|---|
block_bytes |
The size of each block, a multiple of 16 and at least 64, the size of an image’s header; the largest image the pool holds. |
blocks |
How many blocks, at least one. |
webcpp::document::insertion_order
The object policy that keeps every key in the order it was created, whatever the key.
Static Data Members
| Name | Description |
|---|---|
The policy’s name in the image’s layout tag. |
webcpp::document::insertion_order::name
The policy’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "insertion";
webcpp::document::javascript_bytes
The bytes policy that writes bytes as JSON.stringify writes a Uint8Array: {"0":1,"1":2}.
Static Data Members
| Name | Description |
|---|---|
The policy’s name in the image’s layout tag. |
webcpp::document::javascript_bytes::name
The policy’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "javascript";
webcpp::document::javascript_order
The object policy of JavaScript: array-index keys ascending, then the others as created.
Static Data Members
| Name | Description |
|---|---|
The policy’s name in the image’s layout tag. |
See Also
"Key order", in the guide.
webcpp::document::javascript_order::name
The policy’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "javascript";
webcpp::document::js_traits
The traits of JavaScript itself: doubles, UTF-16 strings, and bytes written as a Uint8Array.
Base Classes
| Name | Description |
|---|---|
The default traits: exact numbers, UTF-8 strings, base64 bytes and JavaScript’s key order. |
Type Aliases
See Also
"Traits", in the guide.
webcpp::document::js_traits::bytes
How stringify writes bytes: as JSON.stringify writes a Uint8Array.
webcpp::document::js_traits::number
The type of a number that is not an integer that fits: JavaScript’s own double.
webcpp::document::js_traits::string
The encoding strings and keys are stored in: UTF-16 code units, as JavaScript holds them.
webcpp::document::leased_document
A document opened in place in a block of an image_pool, holding the block’s lease: when the document goes, the block goes back to the pool, its bytes zeroed.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
template<class Traits = default_traits>
class leased_document;
Description
The document is external: it reads and writes as any document does, within its image, and a change that needs more room than the image has returns errc::capacity. A leased document is moved, never copied; moving it invalidates its handles, and a moved-from one holds no document.
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Gives the block back, its bytes zeroed. |
Assignment operators |
|
Whether the lease holds no document: an empty lease, or one moved from. |
|
|
|
The document’s image, at the start of its block. |
|
Subscript operators |
|
|
|
Seals the image, as the document’s own |
|
Whether the image carries a seal. |
Static Member Functions
Friends
| Name | Description |
|---|---|
Reads a stream of images, one after another with nothing between them, into the blocks of an |
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits the image was written with. |
See Also
"Images", in the guide.
webcpp::document::leased_document::leased_document
Constructors
Synopses
Declared in <webcpp/document/image_stream.hpp>
The empty lease: no block and no document, the state a moved-from leased document is left in.
leased_document() noexcept;
A leased document is not copied: its block has one holder.
leased_document(leased_document const&) = delete;
Takes the document and the lease of other, which holds neither after it.
leased_document(leased_document&& other) noexcept = default;
Parameters
| Name | Description |
|---|---|
other |
The leased document. |
webcpp::document::leased_document::leased_document
The empty lease: no block and no document, the state a moved-from leased document is left in.
Description
An operation that fails completes with it, as a leased document of an error holds nothing.
webcpp::document::leased_document::leased_document
A leased document is not copied: its block has one holder.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
leased_document(leased_document const&) = delete;
webcpp::document::leased_document::leased_document
Takes the document and the lease of other, which holds neither after it.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
leased_document(leased_document&& other) noexcept = default;
Parameters
| Name | Description |
|---|---|
other |
The leased document. |
webcpp::document::leased_document::~leased_document
Gives the block back, its bytes zeroed.
webcpp::document::leased_document::operator=
Assignment operators
Synopses
Declared in <webcpp/document/image_stream.hpp>
A leased document is not copied: its block has one holder.
leased_document&
operator=(leased_document const&) = delete;
Gives this document’s block back, then takes the document and the lease of other, which holds neither after it.
leased_document&
operator=(leased_document&& other) noexcept;
Return Value
This leased document.
Parameters
| Name | Description |
|---|---|
other |
The leased document. |
webcpp::document::leased_document::operator=
A leased document is not copied: its block has one holder.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
leased_document&
operator=(leased_document const&) = delete;
webcpp::document::leased_document::operator=
Gives this document’s block back, then takes the document and the lease of other, which holds neither after it.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
leased_document&
operator=(leased_document&& other) noexcept;
Return Value
This leased document.
Parameters
| Name | Description |
|---|---|
other |
The leased document. |
webcpp::document::leased_document::empty
Whether the lease holds no document: an empty lease, or one moved from.
Return Value
Whether it holds no block; its document then reads as nothing.
|
Note
|
The return value should not be discarded. |
webcpp::document::leased_document::get
get overloads
Synopses
Declared in <webcpp/document/image_stream.hpp>
The document.
basic_document<Traits, external>&
get() noexcept;
The document, read only.
[[nodiscard]]
basic_document<Traits, external> const&
get() const noexcept;
Return Value
The document, valid while this leased document is neither moved nor destroyed.
webcpp::document::leased_document::get
The document.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_document<Traits, external>&
get() noexcept;
Return Value
The document, valid while this leased document is neither moved nor destroyed.
webcpp::document::leased_document::get
The document, read only.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
basic_document<Traits, external> const&
get() const noexcept;
Return Value
The document, valid while this leased document is neither moved nor destroyed.
|
Note
|
The return value should not be discarded. |
webcpp::document::leased_document::image
The document’s image, at the start of its block.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
std::span<std::byte const>
image() const noexcept;
Return Value
Its bytes, empty for a moved-from leased document.
|
Note
|
The return value should not be discarded. |
webcpp::document::leased_document::operator[]
Subscript operators
Synopses
Declared in <webcpp/document/image_stream.hpp>
The member key of the document’s value, as root()[key].
basic_value<Traits, external>
operator[](std::string_view key) noexcept;
The member key of the document’s value, read only.
basic_const_value<Traits, external>
operator[](std::string_view key) const noexcept;
The element index of the document’s value, as root()[index].
basic_value<Traits, external>
operator[](uint64_t index) noexcept;
The element index of the document’s value, read only.
basic_const_value<Traits, external>
operator[](uint64_t index) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
index |
The index. |
webcpp::document::leased_document::operator[]
The member key of the document’s value, as root()[key].
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_value<Traits, external>
operator[](std::string_view key) noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::leased_document::operator[]
The member key of the document’s value, read only.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_const_value<Traits, external>
operator[](std::string_view key) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::leased_document::operator[]
The element index of the document’s value, as root()[index].
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_value<Traits, external>
operator[](uint64_t index) noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::leased_document::operator[]
The element index of the document’s value, read only.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_const_value<Traits, external>
operator[](uint64_t index) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::leased_document::root
root overloads
Synopses
Declared in <webcpp/document/image_stream.hpp>
The handle of the document’s value.
basic_value<Traits, external>
root() noexcept;
The handle of the document’s value, read only.
[[nodiscard]]
basic_const_value<Traits, external>
root() const noexcept;
Return Value
-
The handle, which reads and writes.
-
The handle.
webcpp::document::leased_document::root
The handle of the document’s value.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
basic_value<Traits, external>
root() noexcept;
Return Value
The handle, which reads and writes.
webcpp::document::leased_document::root
The handle of the document’s value, read only.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
basic_const_value<Traits, external>
root() const noexcept;
webcpp::document::leased_document::seal
Seals the image, as the document’s own seal does: the next change clears it.
Return Value
Success; errc::type for a moved-from leased document.
webcpp::document::leased_document::sealed
Whether the image carries a seal.
Return Value
Whether it is sealed; false for a moved-from leased document.
|
Note
|
The return value should not be discarded. |
webcpp::document::leased_document::format
Formats a leased block as a new, empty document, holding null, which may grow to the whole block.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
static
result<leased_document>
format(pool_block block) noexcept;
Return Value
The document; the errors of the function format of an external buffer, the block then given back.
Parameters
| Name | Description |
|---|---|
block |
The lease. |
webcpp::document::leased_document::open
Opens the image in a leased block, in place.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
static
result<leased_document>
open(pool_block block) noexcept;
Return Value
The document; the errors of the function open of an external buffer, the block then given back.
Parameters
| Name | Description |
|---|---|
block |
The lease, whose block holds the image from its first byte. |
webcpp::document::literal
One value of a literal, converted implicitly from the C++ value it is written as.
Description
It holds bool, any integer (character types aside), the decimal types, a binary64 floating-point value (stored only by traits whose number is binary, such as js_traits), text in UTF-8 or UTF-16, a date_ms, bytes, nullptr or null, and an object or an array. Text and bytes are copied, so a literal owns what it holds.
Member Functions
| Name | Description |
|---|---|
|
Constructors |
Calls |
webcpp::document::literal::literal
Constructors
Synopses
Declared in <webcpp/document/literal.hpp>
An array.
A date.
JavaScript’s null.
JavaScript’s null.
literal(std::nullptr_t v) noexcept;
An object.
Text in UTF-8, copied.
literal(std::string_view v);
Text in UTF-16, copied.
literal(std::u16string_view v);
Bytes, copied.
literal(std::span<std::byte const> v);
Text in UTF-8, copied.
literal(std::string const& v);
Text in UTF-8, copied; a null pointer holds null.
literal(char const* v);
Text in UTF-16, copied; a null pointer holds null.
literal(char16_t const* v);
A boolean, constrained so that a pointer does not convert to it.
template<std::same_as<std::vector<std::byte>> B>
literal(B v);
A binary64 floating-point value, which only traits whose number is binary store.
template<std::floating_point F>
requires (sizeof(F) == 8)
literal(F v) noexcept;
An integer, of any width and signedness, character types aside.
template<std::integral I>
requires (!std::same_as<I, bool> && !std::same_as<I, char> && !std::same_as<I, char8_t> &&
!std::same_as<I, char16_t> && !std::same_as<I, char32_t> &&
!std::same_as<I, wchar_t>)
literal(I v) noexcept;
Template Parameters
| Name | Description |
|---|---|
B |
|
D |
The decimal type. |
S |
|
V |
|
F |
The floating-point type, of 8 bytes. |
I |
The integer type. |
Parameters
| Name | Description |
|---|---|
v |
The array. |
webcpp::document::literal::literal
Text in UTF-8, copied; a null pointer holds null.
Parameters
| Name | Description |
|---|---|
v |
The text, terminated by a zero, or a null pointer. |
webcpp::document::literal::literal
Text in UTF-16, copied; a null pointer holds null.
Parameters
| Name | Description |
|---|---|
v |
The text, terminated by a zero, or a null pointer. |
webcpp::document::literal::literal
A boolean, constrained so that a pointer does not convert to it.
Synopsis
Declared in <webcpp/document/literal.hpp>
template<std::same_as<std::vector<std::byte>> B>
literal(B v);
Template Parameters
| Name | Description |
|---|---|
B |
|
D |
The decimal type. |
S |
|
V |
|
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::literal::literal
A binary64 floating-point value, which only traits whose number is binary store.
Synopsis
Declared in <webcpp/document/literal.hpp>
template<std::floating_point F>
requires (sizeof(F) == 8)
literal(F v) noexcept;
Template Parameters
| Name | Description |
|---|---|
F |
The floating-point type, of 8 bytes. |
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::literal::literal
An integer, of any width and signedness, character types aside.
Synopsis
Declared in <webcpp/document/literal.hpp>
template<std::integral I>
requires (!std::same_as<I, bool> && !std::same_as<I, char> && !std::same_as<I, char8_t> &&
!std::same_as<I, char16_t> && !std::same_as<I, char32_t> &&
!std::same_as<I, wchar_t>)
literal(I v) noexcept;
Template Parameters
| Name | Description |
|---|---|
I |
The integer type. |
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::literal::visit
Calls f with the value this literal holds, as one of: null_t, bool, std::int64_t, std::uint64_t, const decimal_digits& (a finite decimal), detail::non_finite_decimal, detail::binary64, std::string_view, std::u16string_view, date_ms, std::span<const std::byte>, const object& or const array&.
Synopsis
Declared in <webcpp/document/literal.hpp>
template<class F>
decltype(auto)
visit(F&& f) const;
Return Value
What f returns.
Template Parameters
| Name | Description |
|---|---|
F |
The callable, invocable with each of those. |
Parameters
| Name | Description |
|---|---|
f |
The callable. |
webcpp::document::number_codec
How the traits’ number type is read from JSON text, written as JavaScript writes it, and converted; the primary template has none.
Description
Each specialisation has the static members of number_codec<boost::decimal::decimal128_t>.
Specializations
| Name | Description |
|---|---|
The number codec of |
|
The number codec of |
Template Parameters
| Name | Description |
|---|---|
T |
The number type. |
See Also
"Numbers", in the guide.
webcpp::document::number_codec<boost::decimal::decimal128_t>
The number codec of decimal128_t, exact: what is stored is what was sent.
Synopsis
Declared in <webcpp/document/traits.hpp>
template<>
struct number_codec<boost::decimal::decimal128_t>;
Static Member Functions
| Name | Description |
|---|---|
The digits of a finite value, normalised. |
|
Whether a value is finite. |
|
The value of a number’s digits, exactly. |
|
The value of a signed 64-bit integer, exactly. |
|
The value as a signed 64-bit integer, when it is one. |
|
Reads a JSON number’s text, exactly. |
|
Writes a value as JavaScript’s Number::toString writes its digits. |
Static Data Members
| Name | Description |
|---|---|
Whether the type stores NaN and the infinities: a decimal refuses them. |
webcpp::document::number_codec<boost::decimal::decimal128_t>::digits
The digits of a finite value, normalised.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
decimal_digits
digits(boost::decimal::decimal128_t const& v) noexcept;
Return Value
Its digits.
Parameters
| Name | Description |
|---|---|
v |
The value, finite. |
webcpp::document::number_codec<boost::decimal::decimal128_t>::finite
Whether a value is finite.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
bool
finite(boost::decimal::decimal128_t const& v) noexcept;
Return Value
False for NaN and the infinities.
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::number_codec<boost::decimal::decimal128_t>::from_digits
The value of a number’s digits, exactly.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
result<boost::decimal::decimal128_t>
from_digits(decimal_digits const& d) noexcept;
Return Value
The value; errc::range when its exponent is outside decimal128_t’s range.
Parameters
| Name | Description |
|---|---|
d |
The digits. |
webcpp::document::number_codec<boost::decimal::decimal128_t>::from_int64
The value of a signed 64-bit integer, exactly.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
boost::decimal::decimal128_t
from_int64(int64_t v) noexcept;
Return Value
The value.
Parameters
| Name | Description |
|---|---|
v |
The integer. |
webcpp::document::number_codec<boost::decimal::decimal128_t>::integral
The value as a signed 64-bit integer, when it is one.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
std::optional<int64_t>
integral(boost::decimal::decimal128_t const& v) noexcept;
Return Value
The integer, or nothing when v is not an integer or does not fit.
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::number_codec<boost::decimal::decimal128_t>::parse
Reads a JSON number’s text, exactly.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
result<boost::decimal::decimal128_t>
parse(std::string_view json_number) noexcept;
Return Value
The value; errc::range past 34 significant digits or decimal128_t’s exponents.
Parameters
| Name | Description |
|---|---|
json_number |
The text. |
webcpp::document::number_codec<boost::decimal::decimal128_t>::write
Writes a value as JavaScript’s Number::toString writes its digits.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
number_text
write(boost::decimal::decimal128_t const& v) noexcept;
Return Value
The text.
Parameters
| Name | Description |
|---|---|
v |
The value, finite. |
webcpp::document::number_codec<boost::decimal::decimal128_t>::stores_non_finite
Whether the type stores NaN and the infinities: a decimal refuses them.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
bool stores_non_finite = false;
webcpp::document::number_codec<double>
The number codec of double, for the traits js_traits, with which numbers round as JavaScript’s do, a number past a double’s range is an infinity, and NaN and the infinities are written null.
Static Member Functions
| Name | Description |
|---|---|
The shortest digits that read back as a finite value. |
|
Whether a value is finite. |
|
The double nearest a number’s digits. |
|
The double nearest a signed 64-bit integer. |
|
The value as a signed 64-bit integer, when it is one. |
|
Reads a JSON number’s text, rounded to the nearest double, as |
|
Writes a value as JavaScript’s Number::toString does: the shortest digits that read back. |
Static Data Members
| Name | Description |
|---|---|
Whether the type stores NaN and the infinities: a double does, and writes them |
webcpp::document::number_codec<double>::digits
The shortest digits that read back as a finite value.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
decimal_digits
digits(double const& v) noexcept;
Return Value
Its digits.
Parameters
| Name | Description |
|---|---|
v |
The value, finite. |
webcpp::document::number_codec<double>::finite
Whether a value is finite.
Return Value
False for NaN and the infinities.
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::number_codec<double>::from_digits
The double nearest a number’s digits.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
result<double>
from_digits(decimal_digits const& d) noexcept;
Return Value
The value, an infinity past a double’s range, as JavaScript rounds it; errc::range for more digits than d holds.
Parameters
| Name | Description |
|---|---|
d |
The digits. |
webcpp::document::number_codec<double>::from_int64
The double nearest a signed 64-bit integer.
Return Value
The value, rounded as JavaScript rounds it.
Parameters
| Name | Description |
|---|---|
v |
The integer. |
webcpp::document::number_codec<double>::integral
The value as a signed 64-bit integer, when it is one.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
std::optional<int64_t>
integral(double const& v) noexcept;
Return Value
The integer, or nothing when v is not an integer or does not fit.
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::number_codec<double>::parse
Reads a JSON number’s text, rounded to the nearest double, as JSON.parse does.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
result<double>
parse(std::string_view json_number) noexcept;
Return Value
The value, an infinity past a double’s range, as JSON.parse makes 1e400 Infinity; errc::syntax for text outside JSON’s grammar.
Parameters
| Name | Description |
|---|---|
json_number |
The text. |
webcpp::document::number_codec<double>::write
Writes a value as JavaScript’s Number::toString does: the shortest digits that read back.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
number_text
write(double const& v) noexcept;
Return Value
The text; null for NaN and the infinities.
Parameters
| Name | Description |
|---|---|
v |
The value. |
webcpp::document::number_codec<double>::stores_non_finite
Whether the type stores NaN and the infinities: a double does, and writes them null.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
bool stores_non_finite = true;
webcpp::document::number_text
Text of at most 64 characters, held by value, so that writing a number or a date allocates nothing.
Member Functions
| Name | Description |
|---|---|
The text, valid while this object lives. |
webcpp::document::number_text::view
The text, valid while this object lives.
Synopsis
Declared in <webcpp/document/number.hpp>
[[nodiscard]]
std::string_view
view() const noexcept;
webcpp::document::number_text::chars
The characters, in the first size places.
webcpp::document::number_text::size
How many characters there are.
webcpp::document::object
An object, as JavaScript writes {...}: wd::object{{"city", "Recife"}, {"zip", "50000"}}.
Description
Its members are stored in the order written, by the traits’ key order, and a key written twice keeps the last value, in the first one’s place, as JavaScript does.
Member Functions
See Also
"Handles", in the guide.
webcpp::document::object::object
Constructors
Synopses
Declared in <webcpp/document/literal.hpp>
An empty object, {}.
constexpr
object() = default;
An object of these members.
object(std::initializer_list<object_member> members);
Parameters
| Name | Description |
|---|---|
members |
The members, in the order written. |
webcpp::document::object::object
An object of these members.
Synopsis
Declared in <webcpp/document/literal.hpp>
object(std::initializer_list<object_member> members);
Parameters
| Name | Description |
|---|---|
members |
The members, in the order written. |
webcpp::document::object::members
The members, in the order written.
Synopsis
Declared in <webcpp/document/literal.hpp>
[[nodiscard]]
std::span<object_member const>
members() const noexcept;
webcpp::document::object_member
One member of an object literal: its key, in UTF-8, and its value.
Member Functions
| Name | Description |
|---|---|
|
A member, written |
webcpp::document::object_member::object_member
A member, written {"key", value} inside an object, as a std::pair is: a constructor rather than an aggregate, which tools would ask to write with designated initializers.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
value |
The value. |
webcpp::document::object_member::key
The key, in UTF-8.
webcpp::document::object_member::value
The value.
webcpp::document::owned
The storage of a document that owns its image: a buffer of its own, 16-aligned, which grows, twice the size each time, up to its max_bytes.
See Also
"Images", in the guide.
webcpp::document::parse_options
The limits and the revivers of a parse.
Data Members
| Name | Description |
|---|---|
The most bytes of text, every chunk counted; past it is |
|
The deepest nesting of arrays and objects; one deeper is |
|
The largest image an owned document grows to, 64 MiB; past it is |
|
Whether a string valid in the traits’ bytes policy becomes bytes. |
|
Whether a string in exactly Date.prototype.toJSON’s format becomes a date. |
Derived Classes
| Name | Description |
|---|---|
The options of a stream: the limits and the revivers of each of its documents, its framing and its policy on errors. |
See Also
"Parsing", in the guide.
webcpp::document::parse_options::max_bytes
The most bytes of text, every chunk counted; past it is errc::capacity.
Synopsis
Declared in <webcpp/document/parse.hpp>
uint64_t max_bytes = std::numeric_limits<std::uint64_t>::max();
webcpp::document::parse_options::max_depth
The deepest nesting of arrays and objects; one deeper is errc::capacity.
webcpp::document::parse_options::max_document_bytes
The largest image an owned document grows to, 64 MiB; past it is errc::capacity.
Synopsis
Declared in <webcpp/document/parse.hpp>
uint64_t max_document_bytes = std::uint64_t{64} << 20U;
Description
Each value takes at least 192 bytes of image, so a short value’s image is a hundred times its text or more: 2 MB of [0,0,...] makes an image of 268 MB. The default is low so that text from the network is refused early: under 1 GiB, 11 MB of such text cost 9 seconds and 2.2 GB of memory before it failed; under 64 MiB, 700 KB fails in 0.4 seconds with 137 MB. A program that parses larger documents raises it.
webcpp::document::parse_options::revive_bytes
Whether a string valid in the traits’ bytes policy becomes bytes.
webcpp::document::parse_options::revive_dates
Whether a string in exactly Date.prototype.toJSON’s format becomes a date.
webcpp::document::path_view
The path in the text of a value a selector delivers: each step a member’s key or an element’s index in the text, which on does not renumber.
Description
It is valid during the callback that receives it.
Member Functions
See Also
"Selectors", in the guide.
webcpp::document::path_view::path_view
Constructors
Synopses
Declared in <webcpp/document/parser.hpp>
The path of the root, which has no step.
constexpr
path_view() noexcept = default;
The path of these steps.
explicit
path_view(std::span</* implementation-defined */ const> steps) noexcept;
Parameters
| Name | Description |
|---|---|
steps |
The steps, from the root. |
webcpp::document::path_view::path_view
The path of these steps.
Synopsis
Declared in <webcpp/document/parser.hpp>
explicit
path_view(std::span</* implementation-defined */ const> steps) noexcept;
Parameters
| Name | Description |
|---|---|
steps |
The steps, from the root. |
webcpp::document::path_view::index
An element’s index in the text.
Synopsis
Declared in <webcpp/document/parser.hpp>
[[nodiscard]]
uint64_t
index(std::size_t i) const noexcept;
Parameters
| Name | Description |
|---|---|
i |
The step, below |
webcpp::document::path_view::is_index
Whether a step is an element’s.
Synopsis
Declared in <webcpp/document/parser.hpp>
[[nodiscard]]
bool
is_index(std::size_t i) const noexcept;
Return Value
Whether it is an index rather than a key.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
i |
The step, below |
webcpp::document::path_view::key
A member’s key.
Synopsis
Declared in <webcpp/document/parser.hpp>
[[nodiscard]]
std::string_view
key(std::size_t i) const noexcept;
Parameters
| Name | Description |
|---|---|
i |
The step, below |
webcpp::document::path_view::to_string
The path as a selector of its place, which basic_parser::on parses back: $['items'][3].
Description
A key is quoted, its ' and escaped and every other byte written as it is, control characters included; so it is not RFC 9535’s normalized path, which escapes those too.
webcpp::document::pool_block
A lease of one block of an image_pool; the block is its holder’s until the lease is destroyed or assigned, and then goes back to the pool, its bytes zeroed.
Description
A lease is moved, never copied; a moved-from lease holds no block.
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Gives the block back to its pool, its bytes zeroed. |
Assignment operators |
|
The block’s bytes, 16-aligned, the pool’s |
Friends
| Name | Description |
|---|---|
Reads a stream of images, one after another with nothing between them, into the blocks of an |
|
Fixed blocks for images, 16-aligned and zeroed, allocated once: |
See Also
"Images", in the guide.
webcpp::document::pool_block::pool_block
Constructors
Synopses
Declared in <webcpp/document/image_stream.hpp>
A lease of no block.
constexpr
pool_block() noexcept = default;
A lease is not copied: a block has one holder.
pool_block(pool_block const&) = delete;
Takes the block of other, which holds none after it.
pool_block(pool_block&& other) noexcept;
Parameters
| Name | Description |
|---|---|
other |
The lease. |
webcpp::document::pool_block::pool_block
A lease of no block.
webcpp::document::pool_block::pool_block
A lease is not copied: a block has one holder.
webcpp::document::pool_block::pool_block
Takes the block of other, which holds none after it.
Parameters
| Name | Description |
|---|---|
other |
The lease. |
webcpp::document::pool_block::~pool_block
Gives the block back to its pool, its bytes zeroed.
webcpp::document::pool_block::operator=
Assignment operators
Synopses
Declared in <webcpp/document/image_stream.hpp>
A lease is not copied: a block has one holder.
pool_block&
operator=(pool_block const&) = delete;
Gives this lease’s block back, then takes the block of other, which holds none after it.
pool_block&
operator=(pool_block&& other) noexcept;
Return Value
This lease.
Parameters
| Name | Description |
|---|---|
other |
The lease. |
webcpp::document::pool_block::operator=
A lease is not copied: a block has one holder.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
pool_block&
operator=(pool_block const&) = delete;
webcpp::document::pool_block::operator=
Gives this lease’s block back, then takes the block of other, which holds none after it.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
pool_block&
operator=(pool_block&& other) noexcept;
Return Value
This lease.
Parameters
| Name | Description |
|---|---|
other |
The lease. |
webcpp::document::pool_block::bytes
The block’s bytes, 16-aligned, the pool’s block_bytes of them.
Synopsis
Declared in <webcpp/document/image_stream.hpp>
[[nodiscard]]
std::span<std::byte>
bytes() const noexcept;
Return Value
The bytes, empty for a lease of no block.
|
Note
|
The return value should not be discarded. |
webcpp::document::range_error
JavaScript’s RangeError: a number that does not fit, or an index out of range.
Base Classes
| Name | Description |
|---|---|
What every exception of document derives from: a |
Using Declarations
| Name | Description |
|---|---|
Constructs the exception of a code, as |
webcpp::document::range_error::range_error
Constructs the exception of a code, as error does.
Introduced Symbols
| Name | Description |
|---|---|
|
Constructs the exception of |
webcpp::document::scalar_codec
How a scalar type is stored in a node: its name in the image’s layout tag, its size, and its bytes, written the same on every target.
Description
Each specialisation has name, size, store(const T&, std::byte*) and load(const std::byte*); the primary template has none, so a type without one is no stored_scalar.
Specializations
| Name | Description |
|---|---|
How a |
|
How a |
|
How a |
|
How a |
|
How a |
Template Parameters
| Name | Description |
|---|---|
T |
The scalar type. |
webcpp::document::scalar_codec<bool>
How a bool is stored: one byte, 0 or 1, since bool’s size is not fixed by the standard.
Static Member Functions
webcpp::document::scalar_codec<bool>::load
Reads a value written by store.
Return Value
The value.
Parameters
| Name | Description |
|---|---|
in |
Its |
webcpp::document::scalar_codec<bool>::store
Writes v into out.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
void
store(
bool const& v,
std::byte* out) noexcept;
Parameters
| Name | Description |
|---|---|
v |
The value. |
out |
Where to write its |
webcpp::document::scalar_codec<bool>::name
The codec’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "bool";
webcpp::document::scalar_codec<bool>::size
How many bytes it takes.
webcpp::document::scalar_codec<date_ms>
How a date_ms is stored: its milliseconds as a std::int64_t.
Static Member Functions
webcpp::document::scalar_codec<date_ms>::load
Reads a value written by store.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
date_ms
load(std::byte const* in) noexcept;
Return Value
The value.
Parameters
| Name | Description |
|---|---|
in |
Its |
webcpp::document::scalar_codec<date_ms>::store
Writes v into out.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
void
store(
date_ms const& v,
std::byte* out) noexcept;
Parameters
| Name | Description |
|---|---|
v |
The value. |
out |
Where to write its |
webcpp::document::scalar_codec<date_ms>::name
The codec’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "date-ms";
webcpp::document::scalar_codec<date_ms>::size
How many bytes it takes.
webcpp::document::scalar_codec<boost::decimal::decimal128_t>
How a decimal128_t is stored: its 16 bytes of IEEE 754 BID, least significant first.
Synopsis
Declared in <webcpp/document/traits.hpp>
template<>
struct scalar_codec<boost::decimal::decimal128_t>;
Static Member Functions
webcpp::document::scalar_codec<boost::decimal::decimal128_t>::load
Reads a value written by store.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
boost::decimal::decimal128_t
load(std::byte const* in) noexcept;
Return Value
The value.
Parameters
| Name | Description |
|---|---|
in |
Its |
webcpp::document::scalar_codec<boost::decimal::decimal128_t>::store
Writes v into out.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
void
store(
boost::decimal::decimal128_t const& v,
std::byte* out) noexcept;
Parameters
| Name | Description |
|---|---|
v |
The value. |
out |
Where to write its |
webcpp::document::scalar_codec<boost::decimal::decimal128_t>::name
The codec’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "decimal128";
webcpp::document::scalar_codec<boost::decimal::decimal128_t>::size
How many bytes it takes.
webcpp::document::scalar_codec<double>
How a double is stored, for the traits js_traits, as its IEEE 754 binary64 bits, least significant first.
Static Member Functions
webcpp::document::scalar_codec<double>::load
Reads a value written by store.
Return Value
The value.
Parameters
| Name | Description |
|---|---|
in |
Its |
webcpp::document::scalar_codec<double>::store
Writes v into out.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
void
store(
double const& v,
std::byte* out) noexcept;
Parameters
| Name | Description |
|---|---|
v |
The value. |
out |
Where to write its |
webcpp::document::scalar_codec<double>::name
The codec’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "binary64";
webcpp::document::scalar_codec<double>::size
How many bytes it takes.
webcpp::document::scalar_codec<int64_t>
How a std::int64_t is stored: eight bytes, least significant first.
Static Member Functions
webcpp::document::scalar_codec<int64_t>::load
Reads a value written by store.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
int64_t
load(std::byte const* in) noexcept;
Return Value
The value.
Parameters
| Name | Description |
|---|---|
in |
Its |
webcpp::document::scalar_codec<int64_t>::store
Writes v into out.
Synopsis
Declared in <webcpp/document/traits.hpp>
static
void
store(
int64_t const& v,
std::byte* out) noexcept;
Parameters
| Name | Description |
|---|---|
v |
The value. |
out |
Where to write its |
webcpp::document::scalar_codec<int64_t>::name
The codec’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "int64";
webcpp::document::scalar_codec<int64_t>::size
How many bytes it takes.
webcpp::document::shared_document
A document in a block of a shared_pool, holding one reference to it, as std::shared_ptr holds its object: copying adds a reference, destroying drops one, and the last reference zeroes the block and gives it back to the pool.
Synopsis
Declared in <webcpp/document/shared.hpp>
template<class Traits = default_traits>
class shared_document;
Description
Every holder reads and writes the same image, in place: a write through one is every holder’s. Whether several holders may write at once is the program’s decision, and so is the synchronisation of writers, with each other and with readers, which the library does not do. A holder that wants a document of its own to change calls make_mutable, which copies the image into a new block when others hold it (copy on write). The references themselves are thread-safe: copies made and dropped on any number of threads and processes at once, as std::shared_ptr’s are. A reference passes to another process as its image’s offset in the region: release gives it up without dropping it, and adopt takes it over.
The document is external: a change that needs more room than its block has returns errc::capacity. A shared document allocates nothing; moving it invalidates its handles.
Member Functions
| Name | Description |
|---|---|
|
Constructors |
|
Drops the reference; the last one zeroes the block and gives it back. |
Assignment operators |
|
|
|
The document’s image, in its block. |
|
Makes the document this holder’s alone to change: the same block when it holds the only reference, else a copy of the image in a new block, this holder’s only, while the other holders keep the document as it was. |
|
The image’s offset from the start of the pool’s region, the same in every process. |
|
Subscript operators |
|
Gives up this holder’s reference without dropping it, for |
|
|
|
Seals the image, as the document’s own |
|
Whether the image carries a seal. |
|
How many references the document has, in every process. |
Static Member Functions
| Name | Description |
|---|---|
Takes over a reference that |
|
Makes a new, empty document, holding |
|
Copies an image into a free block of |
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits of the document. |
See Also
"Images", in the guide.
webcpp::document::shared_document::shared_document
Constructors
Synopses
Declared in <webcpp/document/shared.hpp>
A holder of no document.
shared_document() noexcept = default;
Adds a reference to the document of other.
shared_document(shared_document const& other) noexcept;
Takes the reference of other, which holds none after it.
shared_document(shared_document&& other) noexcept;
Parameters
| Name | Description |
|---|---|
other |
The shared document. |
webcpp::document::shared_document::shared_document
A holder of no document.
webcpp::document::shared_document::shared_document
Adds a reference to the document of other.
Synopsis
Declared in <webcpp/document/shared.hpp>
shared_document(shared_document const& other) noexcept;
Parameters
| Name | Description |
|---|---|
other |
The shared document. |
webcpp::document::shared_document::shared_document
Takes the reference of other, which holds none after it.
Synopsis
Declared in <webcpp/document/shared.hpp>
shared_document(shared_document&& other) noexcept;
Parameters
| Name | Description |
|---|---|
other |
The shared document. |
webcpp::document::shared_document::~shared_document
Drops the reference; the last one zeroes the block and gives it back.
webcpp::document::shared_document::operator=
Assignment operators
Synopses
Declared in <webcpp/document/shared.hpp>
Adds a reference to the document of other, then drops this one’s.
shared_document&
operator=(shared_document const& other) noexcept;
Drops this one’s reference, then takes the reference of other, which holds none after it.
shared_document&
operator=(shared_document&& other) noexcept;
Return Value
This shared document.
Parameters
| Name | Description |
|---|---|
other |
The shared document. |
webcpp::document::shared_document::operator=
Adds a reference to the document of other, then drops this one’s.
Synopsis
Declared in <webcpp/document/shared.hpp>
shared_document&
operator=(shared_document const& other) noexcept;
Return Value
This shared document.
Parameters
| Name | Description |
|---|---|
other |
The shared document. |
webcpp::document::shared_document::operator=
Drops this one’s reference, then takes the reference of other, which holds none after it.
Synopsis
Declared in <webcpp/document/shared.hpp>
shared_document&
operator=(shared_document&& other) noexcept;
Return Value
This shared document.
Parameters
| Name | Description |
|---|---|
other |
The shared document. |
webcpp::document::shared_document::get
get overloads
Synopses
Declared in <webcpp/document/shared.hpp>
The document.
basic_document<Traits, external>&
get() noexcept;
The document, read only.
[[nodiscard]]
basic_document<Traits, external> const&
get() const noexcept;
Return Value
The document, valid while this holder neither moves, drops nor releases it.
webcpp::document::shared_document::get
The document.
Return Value
The document, valid while this holder neither moves, drops nor releases it.
webcpp::document::shared_document::get
The document, read only.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
basic_document<Traits, external> const&
get() const noexcept;
Return Value
The document, valid while this holder neither moves, drops nor releases it.
|
Note
|
The return value should not be discarded. |
webcpp::document::shared_document::image
The document’s image, in its block.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
std::span<std::byte const>
image() const noexcept;
Return Value
Its bytes, empty for a holder of no document.
|
Note
|
The return value should not be discarded. |
webcpp::document::shared_document::make_mutable
Makes the document this holder’s alone to change: the same block when it holds the only reference, else a copy of the image in a new block, this holder’s only, while the other holders keep the document as it was.
Description
The copy is the image’s bytes, its seal included. Reading the image to copy it races with a holder that writes it at the same moment, which the program’s synchronisation prevents.
Return Value
Success; errc::capacity when a copy is needed and every block is leased, the holder then keeping its reference; errc::type for a holder of no document.
webcpp::document::shared_document::offset
The image’s offset from the start of the pool’s region, the same in every process.
Return Value
The offset; 0 for a holder of no document.
|
Note
|
The return value should not be discarded. |
webcpp::document::shared_document::operator[]
Subscript operators
Synopses
Declared in <webcpp/document/shared.hpp>
The member key of the document’s value, as root()[key].
basic_value<Traits, external>
operator[](std::string_view key) noexcept;
The member key of the document’s value, read only.
basic_const_value<Traits, external>
operator[](std::string_view key) const noexcept;
The element index of the document’s value, as root()[index].
basic_value<Traits, external>
operator[](uint64_t index) noexcept;
The element index of the document’s value, read only.
basic_const_value<Traits, external>
operator[](uint64_t index) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
index |
The index. |
webcpp::document::shared_document::operator[]
The member key of the document’s value, as root()[key].
Synopsis
Declared in <webcpp/document/shared.hpp>
basic_value<Traits, external>
operator[](std::string_view key) noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::shared_document::operator[]
The member key of the document’s value, read only.
Synopsis
Declared in <webcpp/document/shared.hpp>
basic_const_value<Traits, external>
operator[](std::string_view key) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
key |
The key, in UTF-8. |
webcpp::document::shared_document::operator[]
The element index of the document’s value, as root()[index].
Synopsis
Declared in <webcpp/document/shared.hpp>
basic_value<Traits, external>
operator[](uint64_t index) noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::shared_document::operator[]
The element index of the document’s value, read only.
Synopsis
Declared in <webcpp/document/shared.hpp>
basic_const_value<Traits, external>
operator[](uint64_t index) const noexcept;
Return Value
The handle, which never fails.
Parameters
| Name | Description |
|---|---|
index |
The index. |
webcpp::document::shared_document::release
Gives up this holder’s reference without dropping it, for adopt to take over, in this process or in another that maps the same region.
Return Value
The image’s offset from the region’s start, which adopt takes; 0 for a holder of no document. The holder holds nothing after it.
|
Note
|
The return value should not be discarded. |
webcpp::document::shared_document::root
root overloads
Synopses
Declared in <webcpp/document/shared.hpp>
The handle of the document’s value.
basic_value<Traits, external>
root() noexcept;
The handle of the document’s value, read only.
[[nodiscard]]
basic_const_value<Traits, external>
root() const noexcept;
Return Value
-
The handle, which reads and writes.
-
The handle.
webcpp::document::shared_document::root
The handle of the document’s value.
Return Value
The handle, which reads and writes.
webcpp::document::shared_document::root
The handle of the document’s value, read only.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
basic_const_value<Traits, external>
root() const noexcept;
webcpp::document::shared_document::seal
Seals the image, as the document’s own seal does: the next change, through any holder, clears it.
Return Value
Success; errc::type for a holder of no document.
webcpp::document::shared_document::sealed
Whether the image carries a seal.
Return Value
Whether it is sealed; false for a holder of no document.
|
Note
|
The return value should not be discarded. |
webcpp::document::shared_document::use_count
How many references the document has, in every process.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
uint64_t
use_count() const noexcept;
Return Value
The count, which other holders may change at any moment; 0 for a holder of no document.
|
Note
|
The return value should not be discarded. |
webcpp::document::shared_document::adopt
Takes over a reference that release gave up, in this process or in another that maps the same region.
Synopsis
Declared in <webcpp/document/shared.hpp>
static
result<shared_document>
adopt(
shared_pool pool,
uint64_t offset,
verify check = verify::seal) noexcept;
Return Value
The document; errc::range for an offset that is no block’s image or a block that no one holds; the errors of the function open, which leave the reference as it was: a receiver that refuses it gives it back with shared_pool::drop.
Parameters
| Name | Description |
|---|---|
pool |
The pool, whose region outlives the document. |
offset |
The image’s offset from the region’s start, which |
check |
Whether a sealed image’s bytes are checked against its seal. |
webcpp::document::shared_document::create
Makes a new, empty document, holding null, in a free block of pool, whose image is the whole block.
Synopsis
Declared in <webcpp/document/shared.hpp>
static
result<shared_document>
create(shared_pool pool) noexcept;
Return Value
The document, its only holder; errc::capacity when every block is leased, or when a block is too small for an empty document.
Parameters
| Name | Description |
|---|---|
pool |
The pool, whose region outlives the document. |
webcpp::document::shared_document::from_image
Copies an image into a free block of pool, where it then has the whole block’s room.
Synopsis
Declared in <webcpp/document/shared.hpp>
static
result<shared_document>
from_image(
shared_pool pool,
std::span<std::byte const> bytes,
verify check = verify::seal) noexcept;
Description
A sealed image is checked against its seal first; the image grows to its block, which clears its seal unless it already filled one.
Return Value
The document, its only holder; the errors of the function open, and errc::capacity for an image larger than a block or when every block is leased.
Parameters
| Name | Description |
|---|---|
pool |
The pool, whose region outlives the document. |
bytes |
The image, at any address. |
check |
Whether a sealed image’s bytes are checked against its seal. |
webcpp::document::shared_pool
A pool of blocks for shared documents, over a region of memory the program gives: the process heap, or a region of shared memory that several processes map, each at an address of its own.
Description
The region holds a 64-byte header, then the blocks, each a 64-byte header, which counts the references to its document, and the room of one image. A shared_pool is a view of the region, copied freely: the pool is the region’s, and every view of it, in any process, is the same pool. Leasing a block and giving one back are lock-free and safe from any number of threads and processes at once; nothing is allocated, and a free block is all zero.
|
Note
|
A process that dies holding references leaves their blocks counted for good, as Boost.Interprocess’s |
Member Functions
| Name | Description |
|---|---|
|
A view of no pool, which has no block. |
How many blocks are free. |
|
The room of each block’s image. |
|
How many blocks the pool has. |
|
Gives up one reference to a document by its image’s offset, without opening the image; the last reference zeroes the block and gives it back. |
|
The pool’s bytes, its header and its blocks, at the start of the region it was made over. |
Static Member Functions
| Name | Description |
|---|---|
Attaches to a pool another view formatted, in this process or in another that maps the same region at any address. |
|
Formats a region as a new pool of as many blocks of |
|
The size of a region that holds |
Static Data Members
| Name | Description |
|---|---|
The size of a block’s header, before its image. |
|
The size of the pool’s header, at its region’s start. |
Friends
| Name | Description |
|---|---|
A document in a block of a |
See Also
"Images", in the guide.
webcpp::document::shared_pool::shared_pool
A view of no pool, which has no block.
webcpp::document::shared_pool::available
How many blocks are free.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
std::size_t
available() const noexcept;
Return Value
The count, exact when no block is being leased or given back at the same moment; 0 for a view of no pool.
|
Note
|
The return value should not be discarded. |
webcpp::document::shared_pool::block_bytes
The room of each block’s image.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
std::size_t
block_bytes() const noexcept;
webcpp::document::shared_pool::blocks
How many blocks the pool has.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
std::size_t
blocks() const noexcept;
webcpp::document::shared_pool::drop
Gives up one reference to a document by its image’s offset, without opening the image; the last reference zeroes the block and gives it back.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
result<void>
drop(uint64_t offset) const noexcept;
Description
A receiver that refuses an offset it was handed, because adopt refused its image as damaged or of another layout, or because the program does not want it, gives the reference back so: its image is never read, so a broken header cannot make the block leak.
Return Value
Success; errc::range for an offset that is no block’s image, or a block no one holds, which changes nothing.
|
Note
|
The return value should not be discarded. |
Parameters
| Name | Description |
|---|---|
offset |
The image’s offset from the region’s start, as |
webcpp::document::shared_pool::region
The pool’s bytes, its header and its blocks, at the start of the region it was made over.
Synopsis
Declared in <webcpp/document/shared.hpp>
[[nodiscard]]
std::span<std::byte>
region() const noexcept;
webcpp::document::shared_pool::attach
Attaches to a pool another view formatted, in this process or in another that maps the same region at any address.
Synopsis
Declared in <webcpp/document/shared.hpp>
static
result<shared_pool>
attach(std::span<std::byte> region) noexcept;
Return Value
The pool; errc::not_a_document for a region that is not 16-aligned, holds no pool or is shorter than its pool, errc::incompatible_document for a pool of another layout version or byte order.
Parameters
| Name | Description |
|---|---|
region |
The region, 16-aligned, which holds the pool from its first byte. |
webcpp::document::shared_pool::format
Formats a region as a new pool of as many blocks of block_bytes as it holds, every one free and zero.
Synopsis
Declared in <webcpp/document/shared.hpp>
static
result<shared_pool>
format(
std::span<std::byte> region,
std::size_t block_bytes) noexcept;
Return Value
The pool; errc::range for a region that is not 16-aligned or a size outside those bounds, errc::capacity for a region too small for one block.
Parameters
| Name | Description |
|---|---|
region |
The region, 16-aligned, which the program keeps mapped while the pool and its documents are used. |
block_bytes |
The room of each block’s image, a multiple of 16 and at least 64: the largest image a block holds. |
webcpp::document::shared_pool::region_bytes
The size of a region that holds blocks blocks of block_bytes each.
Synopsis
Declared in <webcpp/document/shared.hpp>
static
result<std::size_t>
region_bytes(
std::size_t block_bytes,
std::size_t blocks) noexcept;
Return Value
The size; errc::range for a size or a count outside those bounds, and errc::capacity when the size overflows.
Parameters
| Name | Description |
|---|---|
block_bytes |
The room of each block’s image, a multiple of 16 and at least 64. |
blocks |
How many blocks, at least one and at most 4,294,967,294. |
webcpp::document::shared_pool::block_header_bytes
The size of a block’s header, before its image.
Synopsis
Declared in <webcpp/document/shared.hpp>
inline constexpr static
std::size_t block_header_bytes = detail::shared_block_header_bytes;
webcpp::document::shared_pool::header_bytes
The size of the pool’s header, at its region’s start.
Synopsis
Declared in <webcpp/document/shared.hpp>
inline constexpr static
std::size_t header_bytes = detail::shared_header_bytes;
webcpp::document::sorted
The object policy that orders keys by their UTF-16 code units, with either encoding, as JavaScript’s sort() and RFC 8785 do, for canonical JSON.
Static Data Members
| Name | Description |
|---|---|
The policy’s name in the image’s layout tag. |
webcpp::document::sorted::name
The policy’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "sorted";
webcpp::document::stream_options
The options of a stream: the limits and the revivers of each of its documents, its framing and its policy on errors.
Description
Each document is held to the limits a parse holds one text to: max_depth, max_document_bytes, and max_bytes, which counts the bytes of one document from its first byte, not those of the stream.
Base Classes
| Name | Description |
|---|---|
The limits and the revivers of a parse. |
Data Members
See Also
"Streams of documents", in the guide.
webcpp::document::stream_options::framing
How the documents are told apart; documents one after another unless told otherwise.
Synopsis
Declared in <webcpp/document/stream.hpp>
webcpp::document::framing framing = framing::concatenated;
webcpp::document::stream_options::on_error
What a bad document does; it stops the stream unless told otherwise.
Synopsis
Declared in <webcpp/document/stream.hpp>
webcpp::document::on_error on_error = on_error::stop;
webcpp::document::syntax_error
JavaScript’s SyntaxError: malformed JSON text, at a byte offset.
Base Classes
| Name | Description |
|---|---|
What every exception of document derives from: a |
Member Functions
| Name | Description |
|---|---|
|
Constructs the exception of |
The byte offset of the failure in the text. |
webcpp::document::syntax_error::syntax_error
Constructs the exception of code at byte offset of the text.
Synopsis
Declared in <webcpp/document/errors.hpp>
syntax_error(
boost::system::error_code code,
uint64_t offset);
Parameters
| Name | Description |
|---|---|
code |
The code of the failure, |
offset |
The byte offset of the failure in the text. |
webcpp::document::syntax_error::offset
The byte offset of the failure in the text.
webcpp::document::type_error
JavaScript’s TypeError: a value of another kind, or a write through nothing.
Base Classes
| Name | Description |
|---|---|
What every exception of document derives from: a |
Using Declarations
| Name | Description |
|---|---|
Constructs the exception of a code, as |
webcpp::document::type_error::type_error
Constructs the exception of a code, as error does.
Introduced Symbols
| Name | Description |
|---|---|
|
Constructs the exception of |
webcpp::document::undefined_t
The type of undefined, JavaScript’s undefined, which set takes to erase a member.
webcpp::document::utf16
The encoding of strings and keys that stores UTF-16 code units, as JavaScript holds strings.
Static Data Members
| Name | Description |
|---|---|
The encoding’s name in the image’s layout tag. |
See Also
"Strings, dates and bytes", in the guide.
webcpp::document::utf16::name
The encoding’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "utf16";
webcpp::document::utf8
The encoding of strings and keys that stores UTF-8, the default.
Static Data Members
| Name | Description |
|---|---|
The encoding’s name in the image’s layout tag. |
See Also
"Strings, dates and bytes", in the guide.
webcpp::document::utf8::name
The encoding’s name in the image’s layout tag.
Synopsis
Declared in <webcpp/document/traits.hpp>
inline constexpr static
std::string_view name = "utf8";
webcpp::document::errc
The errors document reports, as boost::system::error_code values of the category error_category.
Description
The values start at 1, so an error_code of value 0 is no error. An errc converts to an error_code and compares with one: made.error() == wd::errc::syntax.
|
Note
|
The values are fixed: they are never reused for a different meaning, so a stored value keeps its meaning across versions. |
Members
| Name | Description |
|---|---|
Malformed JSON text, JavaScript’s |
|
A value of another kind, a write through nothing, or text invalid in the encoding, JavaScript’s |
|
A number that does not fit or is not exact, an index out of range where JavaScript throws, or a date out of range, JavaScript’s |
|
No room left: a full external buffer, an owned document at its limit, a parser’s limit. |
|
Bytes that are not a document’s image. |
|
An image of another format version, nonzero reserved bytes, layout, byte order or traits. |
|
A sealed image whose bytes do not match its seal: changed, torn or cut short. |
See Also
"Errors", in the guide.
webcpp::document::errc::type
A value of another kind, a write through nothing, or text invalid in the encoding, JavaScript’s TypeError.
webcpp::document::errc::range
A number that does not fit or is not exact, an index out of range where JavaScript throws, or a date out of range, JavaScript’s RangeError.
webcpp::document::errc::capacity
No room left: a full external buffer, an owned document at its limit, a parser’s limit.
webcpp::document::errc::incompatible_document
An image of another format version, nonzero reserved bytes, layout, byte order or traits.
webcpp::document::errc::damaged_document
A sealed image whose bytes do not match its seal: changed, torn or cut short.
webcpp::document::framing
How the documents of a stream are told apart.
Members
| Name | Description |
|---|---|
Documents one after another, any JSON whitespace or none between them: indented JSON and |
|
NDJSON, JSON Lines: one document per line. |
|
JSON text sequences, RFC 7464: each document in a record that 0x1E opens, which ends before the next 0x1E; a document may span lines, and its record holds only whitespace after it. |
See Also
"Streams of documents", in the guide.
webcpp::document::framing::concatenated
Documents one after another, any JSON whitespace or none between them: indented JSON and {"a":1} {"b":2} alike.
Description
A document ends where its value closes, a top-level number at the next byte that cannot continue it. Nothing tells where a bad document ends, so a stream of this framing stops at its first error.
webcpp::document::framing::lines
NDJSON, JSON Lines: one document per line.
Description
A document ends at its line’s n, which JSON never holds inside a string; JSON whitespace around it, r included, and empty lines are ignored, and the last line may end without one.
webcpp::document::framing::rfc7464
JSON text sequences, RFC 7464: each document in a record that 0x1E opens, which ends before the next 0x1E; a document may span lines, and its record holds only whitespace after it.
webcpp::document::kind
The kind of a value: JavaScript’s typeof, with null, arrays, dates and bytes told apart.
Description
|
Note
|
The values are fixed: they are never reused for a different meaning, so a stored value keeps its meaning across versions. |
Members
| Name | Description |
|---|---|
JavaScript’s |
|
JavaScript’s |
|
|
|
A number, held as the traits’ |
|
A string. |
|
A |
|
A |
|
An array. |
|
A plain object. |
See Also
"Handles", in the guide.
webcpp::document::kind::undefined
JavaScript’s undefined: never stored; what a handle reads when nothing is there.
webcpp::document::on_error
What a stream does with a bad document.
Members
| Name | Description |
|---|---|
The first error ends the stream: it is returned again, by every call, until a reset. |
|
A bad document is reported in its place and dropped, and the stream goes on: with |
See Also
"Streams of documents", in the guide.
webcpp::document::on_error::stop
The first error ends the stream: it is returned again, by every call, until a reset.
webcpp::document::on_error::skip
A bad document is reported in its place and dropped, and the stream goes on: with framing::lines at the next line, with framing::rfc7464 at the next 0x1E.
Description
It is not allowed with framing::concatenated, which has no place to go on from.
webcpp::document::verify
Whether opening an image checks its seal.
Members
| Name | Description |
|---|---|
A sealed image’s bytes are checked against its seal, a pass over the image, and refused with |
|
The seal is not checked: for a program that trusts its memory, such as an image it has just written itself. |
See Also
"Images", in the guide.
webcpp::document::verify::seal
A sealed image’s bytes are checked against its seal, a pass over the image, and refused with errc::damaged_document when they differ; an image that is not sealed is not checked.
webcpp::document::verify::none
The seal is not checked: for a program that trusts its memory, such as an image it has just written itself.
webcpp::document::async_read_document
async_read_document overloads
Synopses
Declared in <webcpp/document/asio.hpp>
Reads the next document of a stream into a buffer of Boost.Asio’s second model, taken by value: the overload of async_read_document for DynamicBuffer_v2.
template<
class AsyncReadStream,
class DynamicBuffer,
class Traits,
boost::asio::completion_token_for<void(boost::system::error_code, basic_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
requires boost::asio::is_dynamic_buffer_v2<DynamicBuffer>::value
auto
async_read_document(
AsyncReadStream& stream,
DynamicBuffer buffer,
basic_stream_parser<Traits>& parser,
CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());
Reads the next document of a stream: from the parser when it holds one, else from the stream, reading only while the parser has none ready.
template<
class AsyncReadStream,
class DynamicBuffer,
class Traits,
boost::asio::completion_token_for<void(boost::system::error_code, basic_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
requires (boost::asio::is_dynamic_buffer_v1<DynamicBuffer>::value &&
!boost::asio::is_dynamic_buffer_v2<DynamicBuffer>::value)
auto
async_read_document(
AsyncReadStream& stream,
DynamicBuffer& buffer,
basic_stream_parser<Traits>& parser,
CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());
Return Value
What the token makes of the operation.
Template Parameters
| Name | Description |
|---|---|
AsyncReadStream |
A stream of Boost.Asio’s |
DynamicBuffer |
A dynamic buffer of Boost.Asio’s second model, |
Traits |
The traits of the documents. |
CompletionToken |
The completion token. |
Parameters
| Name | Description |
|---|---|
stream |
The stream. |
buffer |
A copy of the buffer, which the operation keeps; the storage it names outlives the operation. |
parser |
The parser of the stream’s framing. |
token |
The completion token. |
See Also
"Streams of documents", in the guide.
webcpp::document::async_read_document
Reads the next document of a stream into a buffer of Boost.Asio’s second model, taken by value: the overload of async_read_document for DynamicBuffer_v2.
Synopsis
Declared in <webcpp/document/asio.hpp>
template<
class AsyncReadStream,
class DynamicBuffer,
class Traits,
boost::asio::completion_token_for<void(boost::system::error_code, basic_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
requires boost::asio::is_dynamic_buffer_v2<DynamicBuffer>::value
auto
async_read_document(
AsyncReadStream& stream,
DynamicBuffer buffer,
basic_stream_parser<Traits>& parser,
CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());
Return Value
What the token makes of the operation.
Template Parameters
| Name | Description |
|---|---|
AsyncReadStream |
A stream of Boost.Asio’s |
DynamicBuffer |
A dynamic buffer of Boost.Asio’s second model, |
Traits |
The traits of the documents. |
CompletionToken |
The completion token. |
Parameters
| Name | Description |
|---|---|
stream |
The stream. |
buffer |
A copy of the buffer, which the operation keeps; the storage it names outlives the operation. |
parser |
The parser of the stream’s framing. |
token |
The completion token. |
See Also
"Streams of documents", in the guide.
webcpp::document::async_read_document
Reads the next document of a stream: from the parser when it holds one, else from the stream, reading only while the parser has none ready.
Synopsis
Declared in <webcpp/document/asio.hpp>
template<
class AsyncReadStream,
class DynamicBuffer,
class Traits,
boost::asio::completion_token_for<void(boost::system::error_code, basic_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
requires (boost::asio::is_dynamic_buffer_v1<DynamicBuffer>::value &&
!boost::asio::is_dynamic_buffer_v2<DynamicBuffer>::value)
auto
async_read_document(
AsyncReadStream& stream,
DynamicBuffer& buffer,
basic_stream_parser<Traits>& parser,
CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());
Description
It is a composed operation of Boost.Asio, which takes any completion token: a callback, asio::use_awaitable, asio::deferred, boost::cobalt::use_op. Its signature is void(boost::system::error_code, basic_document<Traits>), and it completes with the next document; with the error of a document dropped, under on_error::skip, after which the next call goes on with the stream; with the error that stopped the stream, under on_error::stop, every time; with asio::error::eof once the stream ended and the parser’s finish delivered what it held; or with the error of a read. On an error the document is empty. A completion is never called inside this function: one that needs no read is dispatched to the handler’s immediate executor, else posted to the stream’s.
The buffer is a dynamic buffer of either of Boost.Asio’s models, and the bytes it already holds, left by an earlier read of the same stream, are the parser’s before any read. A buffer of the first model, DynamicBuffer_v1 (asio::streambuf, Beast’s multi_buffer, whatever the pieces of its data), is taken by reference and used through it: the caller keeps it alive until the operation completes. A buffer of the second model, DynamicBuffer_v2 (grow, shrink, data(pos, n): what asio::dynamic_buffer makes of a string, or a vector, also when BOOST_ASIO_NO_DYNAMIC_BUFFER_V1 leaves out the first model) is taken by value, as Boost.Asio’s own reads take it, and the operation keeps its copy: the caller keeps alive the storage it names, the string, and the function never needs the original. A buffer of both models is a buffer of the second, as in Boost.Asio. One operation at a time reads a stream, its buffer and its parser, which outlive it.
Return Value
What the token makes of the operation.
Template Parameters
| Name | Description |
|---|---|
AsyncReadStream |
A stream of Boost.Asio’s |
DynamicBuffer |
A dynamic buffer of Boost.Asio’s first model, |
Traits |
The traits of the documents. |
CompletionToken |
The completion token. |
Parameters
| Name | Description |
|---|---|
stream |
The stream. |
buffer |
The buffer each read fills; the parser consumes what it holds before each read. |
parser |
The parser of the stream’s framing. |
token |
The completion token. |
See Also
"Streams of documents", in the guide.
webcpp::document::async_read_image
Reads the next image of a stream into a block of a pool and opens it there, with no copy and no allocation.
Synopsis
Declared in <webcpp/document/asio.hpp>
template<
class AsyncReadStream,
class Traits = default_traits,
boost::asio::completion_token_for<void(boost::system::error_code, leased_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
auto
async_read_image(
AsyncReadStream& stream,
image_pool& pool,
CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());
Description
The image’s header is read into the block and checked before the rest of the image follows into the same block, which then holds the document. It reads exactly the bytes the reader asks for, so the stream is left at the end of the image it completed.
It is a composed operation of Boost.Asio, which takes any completion token. Its signature is void(boost::system::error_code, leased_document<Traits>): on success the document, which gives its block back when it goes; on error the empty lease (leased_document::empty), with the error: errc::capacity when every block of the pool is leased, in which case the stream is not read, errc::not_a_document or errc::damaged_document for an image the reader refuses, or the error of a read. The end of the stream is asio::error::eof, and it too is reported as errc::capacity when every block is leased: the reader needs a block to learn that the stream ended, so a pool that is full hides the end until a block is given back.
Images are trusted input: this path is for peers the program trusts.
Return Value
What the token makes of the operation.
Template Parameters
| Name | Description |
|---|---|
AsyncReadStream |
A stream of Boost.Asio’s |
Traits |
The traits of the images. |
CompletionToken |
The completion token. |
Parameters
| Name | Description |
|---|---|
stream |
The stream. |
pool |
The pool whose blocks the image is read into; it outlives the operation and the document. |
token |
The completion token. |
See Also
"Images on the wire", in the guide.
webcpp::document::async_write_image
Writes a document’s image to a stream as one buffer, after sealing the document.
Synopsis
Declared in <webcpp/document/asio.hpp>
template<
class AsyncWriteStream,
class Traits,
class Storage,
boost::asio::completion_token_for<void(boost::system::error_code)> CompletionToken = boost::asio::default_completion_token_t<AsyncWriteStream::executor_type>>
auto
async_write_image(
AsyncWriteStream& stream,
basic_document<Traits, Storage>& document,
CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncWriteStream::executor_type>());
Description
The image carries a checksum of its bytes, which the reader checks, so a byte changed on the way is refused with errc::damaged_document. The document is changed by the seal, which is why it is taken by non-const reference.
It is a composed operation of Boost.Asio, which takes any completion token. Its signature is void(boost::system::error_code): the error of the seal, which writes nothing, or of the write. The caller keeps the document alive until the operation completes: the write reads its image.
Return Value
What the token makes of the operation.
Template Parameters
| Name | Description |
|---|---|
AsyncWriteStream |
A stream of Boost.Asio’s |
Traits |
The traits of the document. |
Storage |
The storage of the document, owned or external. |
CompletionToken |
The completion token. |
Parameters
| Name | Description |
|---|---|
stream |
The stream. |
document |
The document to send, sealed by the operation. |
token |
The completion token. |
See Also
"Sealed images", in the guide.
webcpp::document::async_write_images
Writes the images of a range of documents to a stream in one gather write, after sealing every document of the range.
Synopsis
Declared in <webcpp/document/asio.hpp>
template<
class AsyncWriteStream,
/* implementation-defined */ Range,
boost::asio::completion_token_for<void(boost::system::error_code, std::size_t)> CompletionToken = boost::asio::default_completion_token_t<AsyncWriteStream::executor_type>>
auto
async_write_images(
AsyncWriteStream& stream,
Range& documents,
CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncWriteStream::executor_type>());
Description
The seals come first: the first document that fails to seal completes the operation with its error, having written nothing. Otherwise the images are written as one gather write, which Boost.Asio sends with writev and continues over partial writes, with no copy of their bytes. The bytes are read in place, so the documents must not change until the operation completes.
It is a composed operation of Boost.Asio, which takes any completion token. Its signature is void(boost::system::error_code, std::size_t): the bytes written, the sum of the images’ sizes on success. On an error it is the bytes that reached the stream, which may end inside an image: the whole documents sent are the leading ones whose image sizes, added in order, do not exceed it. An empty range completes with 0 and writes nothing.
Return Value
What the token makes of the operation.
Template Parameters
| Name | Description |
|---|---|
AsyncWriteStream |
A stream of Boost.Asio’s |
Range |
The range of documents: its elements are lvalues of documents, or of any type that seals its image and shows its bytes, such as a leased or a shared document. |
CompletionToken |
The completion token. |
Parameters
| Name | Description |
|---|---|
stream |
The stream. |
documents |
The range, read by this call only: the operation seals its documents, and the caller keeps them alive until it completes. A temporary range is refused. |
token |
The completion token. |
See Also
"Images on the wire", in the guide.
webcpp::document::check_image
Checks that bytes are an image this build reads, reading nothing past the header before its size is known, and that a sealed image’s bytes match its seal.
Synopsis
Declared in <webcpp/document/image.hpp>
result<void>
check_image(
std::span<std::byte const> bytes,
uint32_t layout,
verify check = verify::seal) noexcept;
Description
A seal is checked first, before anything else its header states is trusted, so that a change anywhere in a sealed image, its magic and its size included, is reported as damage. Then the magic, the format version and the byte order, before the size, which an image of another version or byte order states in another way; then the reserved bytes and the layout.
Return Value
Success; errc::not_a_document for bytes shorter than the header or than the size it states, a size below 64, or another magic; errc::incompatible_document for another format version, byte order, nonzero reserved bytes or layout; errc::damaged_document for a sealed image whose bytes do not match its seal, or are fewer than its size, and for a seal that is neither zero nor marked.
Parameters
| Name | Description |
|---|---|
bytes |
The bytes, at any address. |
layout |
The layout tag this build expects. |
check |
Whether a sealed image’s bytes are checked against its seal. |
webcpp::document::error_category
The category of every errc.
Synopsis
Declared in <webcpp/document/errors.hpp>
boost::system::error_category const&
error_category() noexcept;
Description
Its name() is "webcpp.document", and its message(value) the human message of the error, "no room left" for @ref errc::capacity, or "unknown webcpp.document error" for a value errc does not define.
Return Value
The category, a constant with a fixed identity.
webcpp::document::format
Formats a buffer as a new, empty document, holding null.
Synopsis
Declared in <webcpp/document/document.hpp>
template<class Traits = default_traits>
result<basic_document<Traits, external>>
format(std::span<std::byte> buffer) noexcept;
Return Value
The document; errc::capacity when the buffer is too small for an empty document or is not 16-aligned.
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits. |
Parameters
| Name | Description |
|---|---|
buffer |
The buffer, 16-aligned, which the document uses in place and its user keeps alive. |
See Also
"Images", in the guide.
webcpp::document::from_decimal
The digits of a finite value of a decimal type, normalised: every member of a cohort, 1.5 and 1.50, has the same digits.
Synopsis
Declared in <webcpp/document/number.hpp>
template<decimal_type Decimal>
decimal_digits
from_decimal(Decimal v) noexcept;
Return Value
Its digits.
Template Parameters
| Name | Description |
|---|---|
Decimal |
The decimal type. |
Parameters
| Name | Description |
|---|---|
v |
The value, which must be finite. |
webcpp::document::from_decimal128
The digits of a finite decimal128_t, normalised, as from_decimal gives them.
Synopsis
Declared in <webcpp/document/number.hpp>
decimal_digits
from_decimal128(boost::decimal::decimal128_t v) noexcept;
Return Value
Its digits.
Parameters
| Name | Description |
|---|---|
v |
The value, which must be finite. |
webcpp::document::from_image
Copies an image into a new owned document.
Synopsis
Declared in <webcpp/document/document.hpp>
template<class Traits = default_traits>
result<basic_document<Traits, owned>>
from_image(
std::span<std::byte const> bytes,
uint64_t max_bytes = default_max_bytes,
verify check = verify::seal) noexcept;
Description
A sealed image is checked against its seal first, as open checks it; the copy keeps the seal, its bytes being the image’s.
Return Value
The document; the errors of open, and errc::capacity when the image is larger than max_bytes.
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits the image was written with. |
Parameters
| Name | Description |
|---|---|
bytes |
The image, at any address. |
max_bytes |
The size the copy may grow to. |
check |
Whether a sealed image’s bytes are checked against its seal. |
See Also
"Images", in the guide.
webcpp::document::from_int64
The digits of a signed 64-bit integer.
Return Value
Its digits, without trailing zeros: 1000 is {false, "1", 1, 4}.
Parameters
| Name | Description |
|---|---|
v |
The integer. |
webcpp::document::from_uint64
The digits of an unsigned 64-bit integer.
Return Value
Its digits, without trailing zeros.
Parameters
| Name | Description |
|---|---|
v |
The integer. |
webcpp::document::make_error_code
Makes the error_code of an errc, as its implicit conversion does.
Synopsis
Declared in <webcpp/document/errors.hpp>
boost::system::error_code
make_error_code(errc e) noexcept;
Return Value
e as an error_code of the category error_category.
Parameters
| Name | Description |
|---|---|
e |
The error. |
webcpp::document::open
Opens the image already in a buffer, in place.
Synopsis
Declared in <webcpp/document/document.hpp>
template<class Traits = default_traits>
result<basic_document<Traits, external>>
open(
std::span<std::byte> buffer,
verify check = verify::seal) noexcept;
Description
A sealed image is checked against its seal first, a pass over the image, unless check is verify::none.
Return Value
The document; errc::not_a_document for bytes that are not an image or a buffer that is not 16-aligned, errc::incompatible_document for an image of another format version, reserved bytes, layout, byte order or traits, errc::damaged_document for a sealed image whose bytes do not match its seal.
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits the image was written with. |
Parameters
| Name | Description |
|---|---|
buffer |
The buffer, which the document uses in place and its user keeps alive. |
check |
Whether a sealed image’s bytes are checked against its seal. |
See Also
"Images", in the guide.
webcpp::document::parse
JSON.parse(text): reads JSON text into a new owned document.
Synopsis
Declared in <webcpp/document/parse.hpp>
template<class Traits = default_traits>
result<basic_document<Traits, owned>>
parse(
std::string_view text,
parse_options const& o = {}) noexcept;
Description
It accepts what RFC 8259 and JSON.parse accept: no comments, no trailing commas, whitespace of space, tab, line feed and carriage return only. A key written twice keeps the last value in the first one’s place, as JavaScript does. A lone surrogate, "\ud800", is refused with either encoding, since Boost.JSON’s parser, which document uses, cannot deliver one.
Return Value
The document; errc::syntax for malformed text, errc::range for a number the traits’ number refuses, errc::capacity past a limit.
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits of the document. |
Parameters
| Name | Description |
|---|---|
text |
The text. |
o |
The limits and the revivers. |
See Also
"Parsing", in the guide.
webcpp::document::parse_into
Reads JSON text into a new document in a buffer its user owns, as parse does.
Synopsis
Declared in <webcpp/document/parse.hpp>
template<class Traits = default_traits>
result<basic_document<Traits, external>>
parse_into(
std::span<std::byte> buffer,
std::string_view text,
parse_options const& o = {}) noexcept;
Return Value
The document; the errors of parse, and errc::capacity when the buffer is full.
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits of the document. |
Parameters
| Name | Description |
|---|---|
buffer |
The buffer, 16-aligned, which the document uses in place. |
text |
The text. |
o |
The limits and the revivers. |
webcpp::document::parse_or_throw
JSON.parse(text), throwing on failure as JavaScript does; only with exceptions.
Synopsis
Declared in <webcpp/document/parse.hpp>
template<class Traits = default_traits>
requires detail::exceptions_enabled<Traits>
basic_document<Traits, owned>
parse_or_throw(
std::string_view text,
parse_options const& o = {});
Exceptions
| Name | Thrown on |
|---|---|
For malformed text, with the byte offset where it was found. |
|
For a number the traits’ |
|
Past a limit. |
Return Value
The document.
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits of the document. |
Parameters
| Name | Description |
|---|---|
text |
The text. |
o |
The limits and the revivers. |
See Also
"The sugar that throws", in the guide.
webcpp::document::scan_number
Reads a JSON number’s text exactly, never through a double.
Synopsis
Declared in <webcpp/document/number.hpp>
result<decimal_digits>
scan_number(std::string_view json_number) noexcept;
Description
The text follows JSON’s grammar, -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]? [0-9]+)?. Leading zeros and trailing zeros are dropped, so that 1.50, 1.5 and 15e-1 read the same.
Return Value
Its digits; errc::syntax for text outside the grammar, errc::range for a 35th significant digit, or for a number other than zero whose exponent or point leaves std::int32_t. Zero is zero whatever its exponent: 0e2147483648 reads as 0.
Parameters
| Name | Description |
|---|---|
json_number |
The number’s text, as a JSON parser delivers it. |
webcpp::document::stringify
stringify overloads
Synopses
Declared in <webcpp/document/stringify.hpp>
JSON.stringify(v, null, indent): writes a value as JSON text.
template<
class Traits,
class Storage>
std::string
stringify(
basic_const_value<Traits, Storage> v,
uint32_t indent = 0);
JSON.stringify(doc, null, indent): writes a document’s value as JSON text.
template<
class Traits,
class Storage>
std::string
stringify(
basic_document<Traits, Storage> const& d,
uint32_t indent = 0);
Return Value
-
The text; empty when the value is
undefined, sinceJSON.stringify(undefined)returns no text. The standard library’sstd::bad_allocis its only failure. -
The text, as
stringifyof its root writes it.
Template Parameters
| Name | Description |
|---|---|
Traits |
The document’s traits. |
Storage |
The document’s storage. |
Parameters
| Name | Description |
|---|---|
v |
The value. |
indent |
The spaces of each level, clamped to 10; 0 writes compactly. |
d |
The document. |
See Also
"Writing JSON", in the guide.
webcpp::document::stringify
JSON.stringify(v, null, indent): writes a value as JSON text.
Synopsis
Declared in <webcpp/document/stringify.hpp>
template<
class Traits,
class Storage>
std::string
stringify(
basic_const_value<Traits, Storage> v,
uint32_t indent = 0);
Description
Keys come out in the policy’s order; strings are escaped as JSON.stringify escapes them; numbers are written as JavaScript writes them; a date as Date.prototype.toJSON writes it; bytes by the traits’ bytes policy.
Return Value
The text; empty when the value is undefined, since JSON.stringify(undefined) returns no text. The standard library’s std::bad_alloc is its only failure.
Template Parameters
| Name | Description |
|---|---|
Traits |
The document’s traits. |
Storage |
The document’s storage. |
Parameters
| Name | Description |
|---|---|
v |
The value. |
indent |
The spaces of each level, clamped to 10; 0 writes compactly. |
See Also
"Writing JSON", in the guide.
webcpp::document::stringify
JSON.stringify(doc, null, indent): writes a document’s value as JSON text.
Synopsis
Declared in <webcpp/document/stringify.hpp>
template<
class Traits,
class Storage>
std::string
stringify(
basic_document<Traits, Storage> const& d,
uint32_t indent = 0);
Return Value
The text, as stringify of its root writes it.
Template Parameters
| Name | Description |
|---|---|
Traits |
The document’s traits. |
Storage |
The document’s storage. |
Parameters
| Name | Description |
|---|---|
d |
The document. |
indent |
The spaces of each level, clamped to 10; 0 writes compactly. |
webcpp::document::stringify_to
stringify_to overloads
Synopses
Declared in <webcpp/document/stringify.hpp>
Writes a value as stringify does, in pieces of at most chunk bytes, to a sink.
template<
class Traits,
class Storage,
class Sink>
requires detail::sink<std::remove_reference_t<Sink>>
result<void>
stringify_to(
basic_const_value<Traits, Storage> v,
Sink&& sink,
uint32_t indent = 0,
uint64_t chunk = default_chunk);
Writes a document’s value as stringify does, in pieces of at most chunk bytes, to a sink.
template<
class Traits,
class Storage,
class Sink>
requires detail::sink<std::remove_reference_t<Sink>>
result<void>
stringify_to(
basic_document<Traits, Storage> const& d,
Sink&& sink,
uint32_t indent = 0,
uint64_t chunk = default_chunk);
Return Value
-
Success, or the first failure the sink returned, which stops the writing; an exception the sink throws propagates. The document is unchanged either way.
-
Success, or the first failure the sink returned.
Template Parameters
| Name | Description |
|---|---|
Traits |
The document’s traits. |
Storage |
The document’s storage. |
Sink |
A callable taking |
Parameters
| Name | Description |
|---|---|
v |
The value. |
sink |
The sink. |
indent |
The spaces of each level, clamped to 10; 0 writes compactly. |
chunk |
The largest piece, at least 1. |
d |
The document. |
See Also
"Writing JSON", in the guide.
webcpp::document::stringify_to
Writes a value as stringify does, in pieces of at most chunk bytes, to a sink.
Synopsis
Declared in <webcpp/document/stringify.hpp>
template<
class Traits,
class Storage,
class Sink>
requires detail::sink<std::remove_reference_t<Sink>>
result<void>
stringify_to(
basic_const_value<Traits, Storage> v,
Sink&& sink,
uint32_t indent = 0,
uint64_t chunk = default_chunk);
Return Value
Success, or the first failure the sink returned, which stops the writing; an exception the sink throws propagates. The document is unchanged either way.
Template Parameters
| Name | Description |
|---|---|
Traits |
The document’s traits. |
Storage |
The document’s storage. |
Sink |
A callable taking |
Parameters
| Name | Description |
|---|---|
v |
The value. |
sink |
The sink. |
indent |
The spaces of each level, clamped to 10; 0 writes compactly. |
chunk |
The largest piece, at least 1. |
See Also
"Writing JSON", in the guide.
webcpp::document::stringify_to
Writes a document’s value as stringify does, in pieces of at most chunk bytes, to a sink.
Synopsis
Declared in <webcpp/document/stringify.hpp>
template<
class Traits,
class Storage,
class Sink>
requires detail::sink<std::remove_reference_t<Sink>>
result<void>
stringify_to(
basic_document<Traits, Storage> const& d,
Sink&& sink,
uint32_t indent = 0,
uint64_t chunk = default_chunk);
Return Value
Success, or the first failure the sink returned.
Template Parameters
| Name | Description |
|---|---|
Traits |
The document’s traits. |
Storage |
The document’s storage. |
Sink |
A callable taking |
Parameters
| Name | Description |
|---|---|
d |
The document. |
sink |
The sink. |
indent |
The spaces of each level, clamped to 10; 0 writes compactly. |
chunk |
The largest piece, at least 1. |
webcpp::document::to_decimal
Converts a number to a decimal type, exactly.
Synopsis
Declared in <webcpp/document/number.hpp>
template<decimal_type Decimal>
result<Decimal>
to_decimal(decimal_digits const& d) noexcept;
Return Value
The value; errc::range when Decimal cannot hold it exactly: more digits than its precision, or an exponent outside its range.
Template Parameters
| Name | Description |
|---|---|
Decimal |
The decimal type. |
Parameters
| Name | Description |
|---|---|
d |
The number. |
webcpp::document::to_decimal128
Converts a number to decimal128_t, exactly, as to_decimal does.
Synopsis
Declared in <webcpp/document/number.hpp>
result<boost::decimal::decimal128_t>
to_decimal128(decimal_digits const& d) noexcept;
Return Value
The value; errc::range when its exponent is outside decimal128_t’s range.
Parameters
| Name | Description |
|---|---|
d |
The number. |
webcpp::document::to_int64
Converts a number to a signed 64-bit integer, when it is one.
Synopsis
Declared in <webcpp/document/number.hpp>
std::optional<int64_t>
to_int64(decimal_digits const& d) noexcept;
Return Value
Its value when it is an integer from INT64_MIN to INT64_MAX, else nothing.
Parameters
| Name | Description |
|---|---|
d |
The number. |
webcpp::document::to_uint64
Converts a number to an unsigned 64-bit integer, when it is one.
Synopsis
Declared in <webcpp/document/number.hpp>
std::optional<uint64_t>
to_uint64(decimal_digits const& d) noexcept;
Return Value
Its value when it is an integer from 0 to UINT64_MAX, else nothing.
Parameters
| Name | Description |
|---|---|
d |
The number. |
webcpp::document::traits_tag
The traits’ identity in the image’s layout tag: an FNV-1a hash, 32 bits, of each scalar codec’s name and size (boolean, integer, number, date), then the names of the string, bytes, object and array policies, each followed by a zero byte.
Synopsis
Declared in <webcpp/document/traits.hpp>
template<class Traits>
constexpr
uint32_t
traits_tag() noexcept;
Return Value
The hash: two traits that store or order differently have different tags.
Template Parameters
| Name | Description |
|---|---|
Traits |
The traits. |
webcpp::document::write_javascript
Writes a number as JavaScript’s Number::toString writes it, ECMA-262, radix 10.
Synopsis
Declared in <webcpp/document/number.hpp>
number_text
write_javascript(decimal_digits const& d) noexcept;
Description
With k significant digits and the decimal point after n of them:
| Condition | Written as | Example | | --- | --- | --- | | k <= n <= 21 | the digits, then n - k zeros | 100000000000000000000 | | 0 < n <= 21 | the digits with a point after n | 123.45 | | -6 < n <= 0 | 0., -n zeros, the digits | 0.000001 | | otherwise | one digit, . and the rest if any, e, the sign, n - 1 | 1.5e+300 |
Return Value
Its text, which never holds more than 64 characters.
Parameters
| Name | Description |
|---|---|
d |
The number. |
See Also
"Numbers", in the guide.
webcpp::document::operator==
Equality operators
Synopses
Declared in <webcpp/document/document.hpp>
Whether two documents hold equal values, structurally, key order aside.
bool
operator==(
basic_document const& a,
basic_document const& b) noexcept;
Whether two values are equal, structurally: the same kind; numbers by value (1, 1.0 and 1e0 are equal); strings, dates and bytes by value; arrays element by element; objects with the same keys, each with an equal value, in any order.
bool
operator==(
basic_const_value const& a,
basic_const_value const& b) noexcept;
Return Value
-
Whether
a.root() == b.root(). -
Whether their values are equal.
Parameters
| Name | Description |
|---|---|
a |
A document. |
b |
A document. |
webcpp::document::operator==
Whether two documents hold equal values, structurally, key order aside.
Synopsis
Declared in <webcpp/document/document.hpp>
bool
operator==(
basic_document const& a,
basic_document const& b) noexcept;
|
Note
|
This function is a hidden friend of basic_document, declared only inside that class: it can be found only through argument-dependent lookup. |
Return Value
Whether a.root() == b.root().
Parameters
| Name | Description |
|---|---|
a |
A document. |
b |
A document. |
webcpp::document::operator==
Whether two values are equal, structurally: the same kind; numbers by value (1, 1.0 and 1e0 are equal); strings, dates and bytes by value; arrays element by element; objects with the same keys, each with an equal value, in any order.
Synopsis
Declared in <webcpp/document/value.hpp>
bool
operator==(
basic_const_value const& a,
basic_const_value const& b) noexcept;
|
Note
|
This function is a hidden friend of basic_const_value, declared only inside that class: it can be found only through argument-dependent lookup. |
Description
Two undefined are equal; JavaScript has no deep equality, and this one is the document’s.
Return Value
Whether their values are equal.
Parameters
| Name | Description |
|---|---|
a |
A handle. |
b |
A handle. |
webcpp::document::byte_order_mark
The byte-order mark of the header, as the machine that wrote it writes this number.
Synopsis
Declared in <webcpp/document/image.hpp>
inline constexpr uint32_t byte_order_mark = 0x01020304;
webcpp::document::default_chunk
The size of the pieces stringify_to hands its sink, unless told otherwise.
Synopsis
Declared in <webcpp/document/stringify.hpp>
inline constexpr uint64_t default_chunk = 4096;
webcpp::document::default_max_bytes
The largest image an owned document grows to unless told otherwise: 1 GiB.
Synopsis
Declared in <webcpp/document/document.hpp>
inline constexpr uint64_t default_max_bytes = std::uint64_t{1} << 30U;
webcpp::document::format_version
The version of the image’s format that this library writes and opens.
Description
The library raises it when the layout of an image changes, and then provides a migration from the previous version; none exists yet, since 1 is the first. It is the library’s constant, not an option: a new Boost release does not change it, and the format test watches that the layout the Boost libraries give the image has not changed.
See Also
"Images", in the guide.
webcpp::document::image_magic
The first eight bytes of every image: WCPPDOC and a zero byte.
Synopsis
Declared in <webcpp/document/image.hpp>
inline constexpr std::array<char, 8> image_magic = {'W', 'C', 'P', 'P', 'D', 'O', 'C', '\0'};
webcpp::document::null
JavaScript’s null, which set stores, as nullptr does.
webcpp::document::undefined
JavaScript’s undefined: v.set(wd::undefined) erases a member, as delete does, and stores null in an element.
webcpp::document::decimal_type
The decimal types of Boost.Decimal a document converts to and from: decimal32_t, decimal64_t, decimal128_t and their fast variants.
Synopsis
Declared in <webcpp/document/number.hpp>
template<class T>
concept decimal_type = std::same_as<T, boost::decimal::decimal32_t> || std::same_as<T, boost::decimal::decimal64_t> ||
std::same_as<T, boost::decimal::decimal128_t> ||
std::same_as<T, boost::decimal::decimal_fast32_t> ||
std::same_as<T, boost::decimal::decimal_fast64_t> ||
std::same_as<T, boost::decimal::decimal_fast128_t>;
Template Parameters
| Name | Description |
|---|---|
T |
The type. |
webcpp::document::document_traits
What a document’s traits must give: the scalar types, each a stored_scalar, a number_codec for number, and one of the encodings and policies.
Synopsis
Declared in <webcpp/document/traits.hpp>
template<class T>
concept document_traits = std::same_as<typename T::boolean, bool> && std::same_as<typename T::integer, std::int64_t> &&
std::same_as<typename T::date, date_ms> && stored_scalar<typename T::number> &&
requires { number_codec<typename T::number>::stores_non_finite; } &&
(std::same_as<typename T::string, utf8> || std::same_as<typename T::string, utf16>) &&
(std::same_as<typename T::bytes, base64> || std::same_as<typename T::bytes, base64url> ||
std::same_as<typename T::bytes, javascript_bytes>) &&
(std::same_as<typename T::object, javascript_order> ||
std::same_as<typename T::object, insertion_order> ||
std::same_as<typename T::object, sorted>) &&
std::same_as<typename T::array, dense>;
Description
This version requires bool, std::int64_t and date_ms for boolean, integer and date; number is the traits’ choice.
Template Parameters
| Name | Description |
|---|---|
T |
The traits. |
See Also
"Traits", in the guide.
webcpp::document::stored_scalar
A type a node can store as a scalar: trivially copyable, with a scalar_codec whose size is 1, 2, 4, 8 or 16 bytes.
Synopsis
Declared in <webcpp/document/traits.hpp>
template<class T>
concept stored_scalar = std::is_trivially_copyable_v<T> &&
requires {
{ scalar_codec<T>::name } -> std::convertible_to<std::string_view>;
{ scalar_codec<T>::size } -> std::convertible_to<std::size_t>;
} &&
(scalar_codec<T>::size == 1 || scalar_codec<T>::size == 2 || scalar_codec<T>::size == 4 ||
scalar_codec<T>::size == 8 || scalar_codec<T>::size == 16);
Template Parameters
| Name | Description |
|---|---|
T |
The type. |
WEBCPP_DOCUMENT_NO_DANGLING
Marks a class that names a place in a document and owns what it needs to find it again, a handle or a range of a container’s children; empty outside GCC 14 and later, whose -Wdangling-reference it quiets.
WEBCPP_DOCUMENT_NO_EXCEPTIONS
Defined when document is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone.
Description
document’s core never throws, so the macro changes none of it. It removes the members that throw (value::operator=, as, as_string, as_bool, parse_or_throw) and the exception types: a call to one of those members does not compile, and the compiler names detail::exceptions_enabled.
See Also
"Testing", in the guide.
Appendix A: Changelog
What changed in document in each version of webcpp, newest first, one line per change. "Unreleased" is what the next version brings. The release notes of every version are on webcpp’s index page.
Unreleased
webcpp 0.1.0
-
Added the library: a JSON document that behaves as JavaScript does.
-
Added traits that choose the stored types, and JavaScript’s key order.
-
Added handles, array methods and JavaScript-style sugar that throws, compiled only with exceptions.
-
Added object and array views that the standard algorithms read, with key-ordered queries.
-
Added stringify, parse, and selectors that stream in bounded memory.
-
Added streams of documents: NDJSON, JSON text sequences, concatenated JSON, Asio, Cobalt and xactor.
-
Added one relocatable binary image per document, with reserve, shrink_to_fit and compact.
-
Added sealed images, and Asio readers and writers that open them in place in pools of blocks.
-
Added shared documents, reference counted over shared memory, with copy on write.
-
Added a JavaScript module: exact decimals, key ranges, images, streams and Workers.