mirror of
https://github.com/simdjson/simdjson
synced 2026-06-08 17:27:07 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 263e58d9d7 | |||
| ed345772cd | |||
| 92123faa7d | |||
| 12518a278a | |||
| dc9a8e2890 | |||
| 12062bc5bc | |||
| 6e0abb7f9c | |||
| 686c9bff54 | |||
| e1654be9b3 |
+20
-32
@@ -1528,10 +1528,6 @@ void f() {
|
||||
```
|
||||
|
||||
|
||||
**Performance tip**: You will get better performance if you order the attributes (make, model)
|
||||
in the order they appear in the JSON document. Alternatively, [key selectors](#key-selectors)
|
||||
let you extract a fixed, known set of fields in a single pass, independently of the
|
||||
order in which they appear in the document.
|
||||
|
||||
#### Special cases
|
||||
|
||||
@@ -1684,29 +1680,23 @@ std::map<std::string, std::string> obj =
|
||||
|
||||
The `simdjson::from` construction is EXPERIMENTAL and subject to changes.
|
||||
|
||||
### Order-independent reflective deserialization (experimental)
|
||||
### Order-independent reflective deserialization
|
||||
|
||||
> **Experimental and opt-in.** This feature is disabled by default. Enable it by
|
||||
> defining the macro `SIMDJSON_USE_KEY_SELECTOR_REFLECTION=1` before including
|
||||
> simdjson (e.g. as a compiler flag `-DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1`).
|
||||
> It requires C++26 static reflection (`SIMDJSON_STATIC_REFLECTION`).
|
||||
> **Default.** Reflective deserialization uses an order-independent, single-pass
|
||||
> key-selector strategy by default. You can opt out — falling back to the
|
||||
> per-member ordered-lookup path — by defining the macro
|
||||
> `SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1` before including simdjson (e.g. as
|
||||
> a compiler flag `-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1`). It requires
|
||||
> C++26 static reflection (`SIMDJSON_STATIC_REFLECTION`).
|
||||
|
||||
By default, reflective deserialization (`doc.get<T>()` / `simdjson::from`) reads
|
||||
each struct member with an ordered field lookup. This is fastest when the JSON
|
||||
keys appear in the same order as the struct's members, which is the common case.
|
||||
|
||||
When you genuinely cannot rely on the JSON key order — for example when the data
|
||||
comes from a producer that emits members in an arbitrary or varying order — the
|
||||
ordered lookups degrade, because each out-of-order key forces a rescan of the
|
||||
object. For that situation simdjson offers an optional, order-independent
|
||||
strategy: when `SIMDJSON_USE_KEY_SELECTOR_REFLECTION=1` is defined, the
|
||||
reflective deserializer builds a compile-time [key selector](#key-selectors)
|
||||
from the struct's members and walks each object **once** with
|
||||
`object::for_each`, classifying every key through a perfect hash regardless of
|
||||
its position.
|
||||
By default, reflective deserialization (`doc.get<T>()` / `simdjson::from`) builds
|
||||
a compile-time [key selector](#key-selectors) from the struct's members and walks
|
||||
each object **once** with `object::for_each`, classifying every key through a
|
||||
perfect hash regardless of its position. This deserializes structs correctly even
|
||||
when the JSON keys are not in declaration order, without forcing a rescan of the
|
||||
object per out-of-order key.
|
||||
|
||||
```cpp
|
||||
// Compile this translation unit with -DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1
|
||||
#include "simdjson.h"
|
||||
using namespace simdjson;
|
||||
|
||||
@@ -1721,17 +1711,15 @@ auto json = R"({ "retweet_count": 7, "id": 12345, "text": "hello" })"_padded;
|
||||
ondemand::parser parser;
|
||||
ondemand::document doc = parser.iterate(json);
|
||||
Tweet t;
|
||||
auto error = doc.get(t); // uses the key-selector path under the macro
|
||||
auto error = doc.get(t); // uses the key-selector path by default
|
||||
```
|
||||
|
||||
**This is not a universal speedup — run your own benchmarks before adopting it.**
|
||||
In our measurements the key-selector path is roughly on par with the default on
|
||||
small structs (e.g. the Twitter user/tweet objects) but **slower** on documents
|
||||
dominated by many small nested objects (e.g. the CITM catalog), because walking
|
||||
every field of every object and hashing each key costs more than ordered,
|
||||
short-circuiting lookups when the keys *are* in order. Only consider enabling the
|
||||
macro when (a) your inputs really do present keys out of order, and (b) your own
|
||||
benchmarks on your own data show a win. Otherwise, leave it off.
|
||||
The alternative, opt-out path reads each struct member with an ordered field
|
||||
lookup. This can edge ahead when the JSON keys reliably match declaration order,
|
||||
which is a common case. **Neither path is a universal speedup — run your own
|
||||
benchmarks before switching.** Throughput numbers carry some run-to-run noise, so
|
||||
measure on your own data before defining
|
||||
`-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1`.
|
||||
|
||||
|
||||
Minifying JSON strings without parsing
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <string_view>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#if defined(__aarch64__) || defined(__ARM_NEON)
|
||||
#include <arm_neon.h>
|
||||
@@ -37,10 +36,7 @@ namespace key_selector_detail {
|
||||
|
||||
// ============================================================================
|
||||
// Compile-time perfect-hash generator.
|
||||
//
|
||||
// This is a port of the ConstexprCore perfect-hash generator
|
||||
// (https://github.com/ConstexprCore/perfect_hash). It scales to ~100 keys at
|
||||
// compile time by determining association values one (position, character)
|
||||
// It scales to ~100 keys at compile time by determining association values one (position, character)
|
||||
// symbol at a time (gperf-style) instead of an exhaustive offset search, and
|
||||
// falls back to a Hash-and-Displace construction for large/awkward key sets.
|
||||
//
|
||||
@@ -331,11 +327,9 @@ consteval bool try_generate_gperf(
|
||||
std::array<std::size_t, N> phash{};
|
||||
for (std::size_t k = 0; k < N; ++k) { phash[k] = keys[k].size(); }
|
||||
|
||||
// Equivalence-class signatures: sig[k] = XOR of a per-symbol salt over the
|
||||
// key's undetermined symbols. Updated incrementally as symbols are fixed.
|
||||
std::array<std::array<std::size_t, 256>, MAX_POSITIONS> salt{};
|
||||
std::array<std::array<uint64_t, 256>, MAX_POSITIONS> salt{};
|
||||
{
|
||||
std::size_t s = 0x9e3779b97f4a7c15ULL;
|
||||
uint64_t s = 0x9e3779b97f4a7c15ULL;
|
||||
for (std::size_t p = 0; p < num_positions; ++p) {
|
||||
for (std::size_t c = 0; c < 256; ++c) {
|
||||
s = s * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
@@ -343,9 +337,9 @@ consteval bool try_generate_gperf(
|
||||
}
|
||||
}
|
||||
}
|
||||
std::array<std::size_t, N> sig{};
|
||||
std::array<uint64_t, N> sig{};
|
||||
for (std::size_t k = 0; k < N; ++k) {
|
||||
std::size_t s = 0;
|
||||
uint64_t s = 0;
|
||||
for (std::size_t p = 0; p < num_positions; ++p) {
|
||||
std::size_t c = kchars[k][p];
|
||||
if (c < 256) { s ^= salt[p][c]; }
|
||||
@@ -365,14 +359,14 @@ consteval bool try_generate_gperf(
|
||||
std::size_t sp = syms[si].pos;
|
||||
std::size_t sc = syms[si].ch;
|
||||
|
||||
std::size_t sp_salt = salt[sp][sc];
|
||||
uint64_t sp_salt = salt[sp][sc];
|
||||
for (std::size_t k = 0; k < N; ++k) {
|
||||
if (kchars[k][sp] == sc) { sig[k] ^= sp_salt; }
|
||||
}
|
||||
|
||||
for (std::size_t i = 1; i < N; ++i) {
|
||||
std::size_t x = order[i];
|
||||
std::size_t xs = sig[x];
|
||||
uint64_t xs = sig[x];
|
||||
std::size_t j = i;
|
||||
while (j > 0 && sig[order[j - 1]] > xs) {
|
||||
order[j] = order[j - 1];
|
||||
@@ -386,7 +380,7 @@ consteval bool try_generate_gperf(
|
||||
bool collision = false;
|
||||
std::size_t ci = 0;
|
||||
while (ci < N && !collision) {
|
||||
std::size_t class_sig = sig[order[ci]];
|
||||
uint64_t class_sig = sig[order[ci]];
|
||||
std::size_t cj = ci;
|
||||
while (cj < N && sig[order[cj]] == class_sig) { ++cj; }
|
||||
if (cj - ci > 1) {
|
||||
@@ -762,7 +756,9 @@ simdjson_really_inline std::size_t scan_key_length(const char* p) noexcept {
|
||||
template <std::size_t MaxKeyLen>
|
||||
simdjson_really_inline bool compare_key_bytes(
|
||||
const char* p, const char* stored, std::size_t len) noexcept {
|
||||
alignas(16) static constexpr uint8_t idx16[16] =
|
||||
// [[maybe_unused]]: only the NEON/SSE2 branches read these; the scalar
|
||||
// fallback build (no SIMD) leaves them unused, which is an error under -Werror.
|
||||
[[maybe_unused]] alignas(16) static constexpr uint8_t idx16[16] =
|
||||
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
|
||||
if constexpr (MaxKeyLen <= 16) {
|
||||
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
|
||||
@@ -787,7 +783,7 @@ simdjson_really_inline bool compare_key_bytes(
|
||||
return true;
|
||||
#endif
|
||||
} else if constexpr (MaxKeyLen <= 32) {
|
||||
alignas(16) static constexpr uint8_t idx32_hi[16] =
|
||||
[[maybe_unused]] alignas(16) static constexpr uint8_t idx32_hi[16] =
|
||||
{16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31};
|
||||
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
|
||||
uint8x16_t vp_lo = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
|
||||
@@ -871,12 +867,16 @@ struct key_selector {
|
||||
static constexpr std::size_t size() noexcept { return N; }
|
||||
|
||||
/**
|
||||
* Look up a JSON key. rjs must point just after an opening quote in a padded
|
||||
* simdjson buffer. Returns the selector index in [0, N) on match, or N on miss.
|
||||
* Look up a JSON key whose length is already known. p must point at the first
|
||||
* key byte (just after the opening quote) in a padded simdjson buffer, and len
|
||||
* must be the number of raw key bytes (the distance to the closing quote).
|
||||
* Returns the selector index in [0, N) on match, or N on miss.
|
||||
*
|
||||
* Prefer this overload when the caller can obtain the key length cheaply (for
|
||||
* example, object::for_each derives it from the structural index rather than
|
||||
* re-scanning for the closing quote).
|
||||
*/
|
||||
static simdjson_really_inline std::size_t match_raw(raw_json_string rjs) noexcept {
|
||||
const char* p = rjs.raw();
|
||||
std::size_t len = key_selector_detail::scan_key_length<max_key_len>(p);
|
||||
static simdjson_really_inline std::size_t match_raw(const char* p, std::size_t len) noexcept {
|
||||
if (len == 0 || len > max_key_len) { return N; }
|
||||
|
||||
std::size_t slot;
|
||||
@@ -915,6 +915,17 @@ struct key_selector {
|
||||
return ki;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a JSON key. rjs must point just after an opening quote in a padded
|
||||
* simdjson buffer. Returns the selector index in [0, N) on match, or N on miss.
|
||||
* The key length is recovered with a SIMD scan for the closing quote; callers
|
||||
* that already know the length should use the (p, len) overload above.
|
||||
*/
|
||||
static simdjson_really_inline std::size_t match_raw(raw_json_string rjs) noexcept {
|
||||
const char* p = rjs.raw();
|
||||
return match_raw(p, key_selector_detail::scan_key_length<max_key_len>(p));
|
||||
}
|
||||
|
||||
/** Return the key text at selector index i (i in [0, N)). */
|
||||
static constexpr std::string_view key_at(std::size_t i) noexcept {
|
||||
return keys[i];
|
||||
|
||||
@@ -65,37 +65,56 @@ simdjson_inline simdjson_result<value> object::find_field(const std::string_view
|
||||
|
||||
#if SIMDJSON_SUPPORTS_CONCEPTS
|
||||
template <typename Selector, typename Func>
|
||||
simdjson_inline error_code object::for_each(Func&& on_match) noexcept {
|
||||
auto first = this->begin();
|
||||
if (first.error()) { return first.error(); }
|
||||
object_iterator it = first.value_unsafe();
|
||||
object_iterator last{};
|
||||
std::array<bool, Selector::size()> seen{};
|
||||
simdjson_flatten simdjson_inline for_each_result object::for_each(Func&& on_match) noexcept {
|
||||
// Single pass driven directly by the value_iterator, mirroring
|
||||
// find_field_unordered_raw + value(iter.child()). Compared to walking via
|
||||
// object_iterator/field, this avoids constructing a simdjson_result<field> and
|
||||
// a field (key + value) for every field -- and the development-check bookkeeping
|
||||
// in object_iterator -- building a value only for the fields that actually match.
|
||||
// We operate on a copy of the iterator, as object::begin() would.
|
||||
value_iterator it = iter;
|
||||
// Track which selector indices have already matched, as a compile-time bitset:
|
||||
// a single 64-bit word for up to 64 keys (the common case), more words as
|
||||
// needed. Initializing one (or a few) registers to zero is cheaper than zeroing
|
||||
// a per-key byte array on every call, and the test/set become register bit ops.
|
||||
constexpr std::size_t seen_words = (Selector::size() + 63) / 64;
|
||||
std::array<std::uint64_t, seen_words> seen{};
|
||||
std::size_t matched = 0;
|
||||
while (it != last) {
|
||||
auto field_res = *it;
|
||||
if (field_res.error()) { return field_res.error(); }
|
||||
field f = field_res.value_unsafe();
|
||||
std::size_t idx = Selector::match_raw(f.key());
|
||||
if (idx < Selector::size() && !seen[idx]) {
|
||||
seen[idx] = true;
|
||||
value matched_value = f.value();
|
||||
// The callback may return either void or an error_code. When it returns an
|
||||
// error_code, we stop at the first non-SUCCESS result and propagate it so
|
||||
// the caller can surface value-parse errors (for example, a type mismatch
|
||||
// on a matched field). A void-returning callback is responsible for
|
||||
// handling its own errors.
|
||||
if constexpr (std::is_same_v<decltype(on_match(idx, matched_value)), error_code>) {
|
||||
error_code e = on_match(idx, matched_value);
|
||||
if (e) { return e; }
|
||||
} else {
|
||||
on_match(idx, matched_value);
|
||||
while (it.is_open()) {
|
||||
raw_json_string key;
|
||||
std::size_t key_len;
|
||||
// field_key_with_length derives the key length from the structural index (the
|
||||
// following ':' token), avoiding a forward SIMD scan for the closing quote.
|
||||
error_code error = it.field_key_with_length(key, key_len);
|
||||
if (error) { it.abandon(); return {error, matched}; }
|
||||
// Advance past the ':' and descend onto the value.
|
||||
if ((error = it.field_value())) { it.abandon(); return {error, matched}; }
|
||||
std::size_t idx = Selector::match_raw(key.raw(), key_len);
|
||||
if (idx < Selector::size()) {
|
||||
const std::uint64_t seen_bit = std::uint64_t{1} << (idx & 63);
|
||||
std::uint64_t &seen_word = seen[idx >> 6];
|
||||
if (!(seen_word & seen_bit)) {
|
||||
seen_word |= seen_bit;
|
||||
value matched_value(it.child());
|
||||
// The callback may return either void or an error_code. When it returns an
|
||||
// error_code, we stop at the first non-SUCCESS result and propagate it so
|
||||
// the caller can surface value-parse errors (for example, a type mismatch
|
||||
// on a matched field). A void-returning callback is responsible for
|
||||
// handling its own errors.
|
||||
if constexpr (std::is_same_v<decltype(on_match(idx, matched_value)), error_code>) {
|
||||
if ((error = on_match(idx, matched_value))) { return {error, matched}; }
|
||||
} else {
|
||||
on_match(idx, matched_value);
|
||||
}
|
||||
if (++matched >= Selector::size()) { break; }
|
||||
}
|
||||
if (++matched >= Selector::size()) { break; }
|
||||
}
|
||||
++it;
|
||||
// Skip the value (a no-op if the callback consumed it) and step to the next
|
||||
// field; has_next_field() ends the container on '}', which closes the loop.
|
||||
if ((error = it.skip_child())) { it.abandon(); return {error, matched}; }
|
||||
if ((error = it.has_next_field().error())) { return {error, matched}; }
|
||||
}
|
||||
return SUCCESS;
|
||||
return {SUCCESS, matched};
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -16,6 +16,22 @@ namespace simdjson {
|
||||
namespace SIMDJSON_IMPLEMENTATION {
|
||||
namespace ondemand {
|
||||
|
||||
#if SIMDJSON_SUPPORTS_CONCEPTS
|
||||
/**
|
||||
* Result of object::for_each: the first error encountered (SUCCESS if none) and
|
||||
* the number of distinct selector keys that matched during the walk. A
|
||||
* matched_count equal to Selector::size() means every selected key was present
|
||||
* in the object. Implicitly converts to error_code so existing callers that only
|
||||
* care about the error (including SIMDJSON_TRY and the test ASSERT_* macros) keep
|
||||
* working unchanged.
|
||||
*/
|
||||
struct for_each_result {
|
||||
error_code error{SUCCESS};
|
||||
std::size_t matched_count{0};
|
||||
constexpr operator error_code() const noexcept { return error; }
|
||||
};
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A forward-only JSON object field iterator.
|
||||
*/
|
||||
@@ -148,11 +164,14 @@ public:
|
||||
* error_code, the walk stops at the first non-SUCCESS result and that error is
|
||||
* returned, which lets the callback surface value-parse errors.
|
||||
*
|
||||
* @returns SUCCESS, or the first error encountered while walking the object
|
||||
* (including any error returned by the callback).
|
||||
* @returns a for_each_result holding the first error encountered while walking
|
||||
* the object (including any error returned by the callback, SUCCESS if
|
||||
* none) and the number of distinct selector keys that matched. The
|
||||
* result converts implicitly to error_code, so callers that only need
|
||||
* the error can ignore the count.
|
||||
*/
|
||||
template <typename Selector, typename Func>
|
||||
simdjson_inline error_code for_each(Func&& on_match) noexcept;
|
||||
simdjson_inline for_each_result for_each(Func&& on_match) noexcept;
|
||||
#endif
|
||||
|
||||
/**
|
||||
|
||||
@@ -269,11 +269,12 @@ constexpr bool user_defined_type = (std::is_class_v<T>
|
||||
!concepts::appendable_containers<T>);
|
||||
|
||||
|
||||
#if defined(SIMDJSON_USE_KEY_SELECTOR_REFLECTION) && SIMDJSON_USE_KEY_SELECTOR_REFLECTION
|
||||
#if !(defined(SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION) && SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION)
|
||||
|
||||
// Experimental: deserialize a reflected struct using a compile-time key_selector
|
||||
// Default: deserialize a reflected struct using a compile-time key_selector
|
||||
// and a single object::for_each pass (perfect-hash key matching) instead of one
|
||||
// obj[key] lookup per member. Enable with -DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1.
|
||||
// obj[key] lookup per member. Disable (falling back to the per-member obj[key]
|
||||
// path below) with -DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1.
|
||||
namespace key_selector_reflection_detail {
|
||||
|
||||
// A member participates if it is public, non-const, and not annotated to skip.
|
||||
@@ -309,6 +310,37 @@ template <typename T>
|
||||
using selector_for = typename [: std::meta::substitute(
|
||||
^^SIMDJSON_IMPLEMENTATION::ondemand::key_selector, selector_key_args<T>()) :];
|
||||
|
||||
// Number of members that participate in deserialization. A class can have zero
|
||||
// eligible members (e.g. std::chrono::time_point, whose only data member is
|
||||
// private): an empty key_selector cannot be built, so the tag_invoke below
|
||||
// special-cases this count.
|
||||
template <typename T>
|
||||
consteval std::size_t eligible_member_count() {
|
||||
std::size_t count = 0;
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (is_eligible_member(mem)) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// True when none of T's eligible members is an optional type, i.e. every member
|
||||
// is required. In that case presence can be checked with a single match count
|
||||
// instead of a per-member "seen" array.
|
||||
template <typename T>
|
||||
consteval bool all_eligible_members_required() {
|
||||
bool all_required = true;
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (is_eligible_member(mem)) {
|
||||
if constexpr (concepts::optional_type<typename [: std::meta::type_of(mem) :]>) {
|
||||
all_required = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return all_required;
|
||||
}
|
||||
|
||||
} // namespace key_selector_reflection_detail
|
||||
|
||||
template <typename T, typename ValT>
|
||||
@@ -320,45 +352,80 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
|
||||
} else {
|
||||
SIMDJSON_TRY(val.get_object().get(obj));
|
||||
}
|
||||
if constexpr (key_selector_reflection_detail::eligible_member_count<T>() == 0) {
|
||||
// No members to deserialize: an empty key_selector cannot be built, so just
|
||||
// validate that the input is an object (done above) and succeed. Mirrors the
|
||||
// opt-out per-member path, which iterates over zero members.
|
||||
(void)out;
|
||||
(void)obj;
|
||||
return SUCCESS;
|
||||
} else {
|
||||
using selector = key_selector_reflection_detail::selector_for<T>;
|
||||
std::array<bool, selector::size()> seen_member{};
|
||||
// Single pass over the object: each field whose key matches a member yields its
|
||||
// selector index, which we map back to the corresponding member. The callback
|
||||
// returns an error_code so that a value-parse error (e.g. a type mismatch on a
|
||||
// matched field) is propagated by for_each instead of being silently dropped.
|
||||
error_code walk_error = obj.template for_each<selector>(
|
||||
[&](std::size_t matched_index, SIMDJSON_IMPLEMENTATION::ondemand::value field_value) -> error_code {
|
||||
std::size_t counter = 0;
|
||||
error_code field_error = SUCCESS;
|
||||
if constexpr (key_selector_reflection_detail::all_eligible_members_required<T>()) {
|
||||
// Fast path: every member is required. A single for_each pass parses each
|
||||
// matched field; the returned match count then tells us whether every member
|
||||
// was present (matched_count == selector::size()) without a per-member "seen"
|
||||
// array. A value-parse error (e.g. a type mismatch) is propagated by for_each.
|
||||
auto walk = obj.template for_each<selector>(
|
||||
[&](std::size_t matched_index, SIMDJSON_IMPLEMENTATION::ondemand::value field_value) -> error_code {
|
||||
std::size_t counter = 0;
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (key_selector_reflection_detail::is_eligible_member(mem)) {
|
||||
if (matched_index == counter) { return field_value.get(out.[:mem:]); }
|
||||
++counter;
|
||||
}
|
||||
}
|
||||
return SUCCESS;
|
||||
});
|
||||
if (walk.error) { return walk.error; }
|
||||
// A missing required member shows up as a short match count and is reported as
|
||||
// NO_SUCH_FIELD, mirroring the ordered obj[key] path.
|
||||
if (walk.matched_count != selector::size()) { return NO_SUCH_FIELD; }
|
||||
return SUCCESS;
|
||||
} else {
|
||||
std::array<bool, selector::size()> seen_member{};
|
||||
// Single pass over the object: each field whose key matches a member yields its
|
||||
// selector index, which we map back to the corresponding member. The callback
|
||||
// returns an error_code so that a value-parse error (e.g. a type mismatch on a
|
||||
// matched field) is propagated by for_each instead of being silently dropped.
|
||||
error_code walk_error = obj.template for_each<selector>(
|
||||
[&](std::size_t matched_index, SIMDJSON_IMPLEMENTATION::ondemand::value field_value) -> error_code {
|
||||
std::size_t counter = 0;
|
||||
error_code field_error = SUCCESS;
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (key_selector_reflection_detail::is_eligible_member(mem)) {
|
||||
if (matched_index == counter) {
|
||||
seen_member[counter] = true;
|
||||
field_error = field_value.get(out.[:mem:]);
|
||||
}
|
||||
++counter;
|
||||
}
|
||||
}
|
||||
return field_error;
|
||||
});
|
||||
if (walk_error) { return walk_error; }
|
||||
// Required (non-optional) members must be present: a missing one is reported as
|
||||
// NO_SUCH_FIELD, mirroring the ordered obj[key] path. Optional members may be
|
||||
// absent.
|
||||
std::size_t check_counter = 0;
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (key_selector_reflection_detail::is_eligible_member(mem)) {
|
||||
if (matched_index == counter) {
|
||||
seen_member[counter] = true;
|
||||
field_error = field_value.get(out.[:mem:]);
|
||||
if constexpr (!concepts::optional_type<decltype(out.[:mem:])>) {
|
||||
if (!seen_member[check_counter]) { return NO_SUCH_FIELD; }
|
||||
}
|
||||
++counter;
|
||||
++check_counter;
|
||||
}
|
||||
}
|
||||
return field_error;
|
||||
});
|
||||
if (walk_error) { return walk_error; }
|
||||
// Required (non-optional) members must be present: a missing one is reported as
|
||||
// NO_SUCH_FIELD, mirroring the ordered obj[key] path. Optional members may be
|
||||
// absent.
|
||||
std::size_t check_counter = 0;
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (key_selector_reflection_detail::is_eligible_member(mem)) {
|
||||
if constexpr (!concepts::optional_type<decltype(out.[:mem:])>) {
|
||||
if (!seen_member[check_counter]) { return NO_SUCH_FIELD; }
|
||||
}
|
||||
++check_counter;
|
||||
}
|
||||
return SUCCESS;
|
||||
}
|
||||
}
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Opt-out path (-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1): deserialize a
|
||||
// reflected struct with one obj[key] lookup per member, instead of the default
|
||||
// single-pass object::for_each path above.
|
||||
template <typename T, typename ValT>
|
||||
requires(user_defined_type<T> && std::is_class_v<T>)
|
||||
error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
|
||||
@@ -391,7 +458,7 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
|
||||
return simdjson::SUCCESS;
|
||||
}
|
||||
|
||||
#endif // SIMDJSON_USE_KEY_SELECTOR_REFLECTION
|
||||
#endif // SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION
|
||||
|
||||
// Support for enum deserialization - deserialize from string representation using expand approach from P2996R12
|
||||
template <typename T, typename ValT>
|
||||
|
||||
@@ -423,6 +423,22 @@ simdjson_warn_unused simdjson_inline simdjson_result<raw_json_string> value_iter
|
||||
return raw_json_string(key);
|
||||
}
|
||||
|
||||
simdjson_warn_unused simdjson_inline error_code value_iterator::field_key_with_length(raw_json_string &key, std::size_t &len) noexcept {
|
||||
assert_at_next();
|
||||
|
||||
const uint8_t *k = _json_iter->return_current_and_advance();
|
||||
if (*(k++) != '"') { return report_error(TAPE_ERROR, "Object key is not a string"); }
|
||||
// After return_current_and_advance(), the current token is the ':' that follows
|
||||
// the key. The closing quote sits just before it (only JSON whitespace may
|
||||
// intervene), so step back from the ':' to the closing quote to get the length.
|
||||
// In minified JSON this is a single back-step.
|
||||
const char *q = reinterpret_cast<const char *>(_json_iter->peek());
|
||||
do { --q; } while (*q != '"');
|
||||
key = raw_json_string(k);
|
||||
len = static_cast<std::size_t>(q - reinterpret_cast<const char *>(k));
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
simdjson_warn_unused simdjson_inline error_code value_iterator::field_value() noexcept {
|
||||
assert_at_next();
|
||||
|
||||
|
||||
@@ -158,6 +158,17 @@ public:
|
||||
*/
|
||||
simdjson_warn_unused simdjson_inline simdjson_result<raw_json_string> field_key() noexcept;
|
||||
|
||||
/**
|
||||
* Get the current field's key together with its raw byte length.
|
||||
*
|
||||
* Like field_key(), but also returns the number of raw key bytes (the distance
|
||||
* from the first key byte to the closing quote). The length is recovered from
|
||||
* the structural index -- the next structural token is the ':' -- by stepping
|
||||
* back over any whitespace to the closing quote, avoiding a forward SIMD scan
|
||||
* for the closing quote. Leaves the iterator positioned exactly as field_key().
|
||||
*/
|
||||
simdjson_warn_unused simdjson_inline error_code field_key_with_length(raw_json_string &key, std::size_t &len) noexcept;
|
||||
|
||||
/**
|
||||
* Pass the : in the field and move to its value.
|
||||
*/
|
||||
|
||||
@@ -38,9 +38,9 @@ add_cpp_test(ondemand_wrong_type_error_tests LABELS ondemand acceptance
|
||||
add_cpp_test(ondemand_iterate_many_csv LABELS ondemand acceptance per_implementation)
|
||||
add_cpp_test(ondemand_custom_types_tests LABELS ondemand acceptance per_implementation)
|
||||
add_cpp_test(ondemand_custom_types_document_tests LABELS ondemand acceptance per_implementation)
|
||||
# Reflective deserialization through the experimental key-selector path. The
|
||||
# SIMDJSON_USE_KEY_SELECTOR_REFLECTION macro is defined at the top of the test
|
||||
# source itself, so this is an ordinary test target.
|
||||
# Reflective deserialization through the (default) key-selector path. No special
|
||||
# macro is needed: the key-selector + object::for_each path is the default, so
|
||||
# this is an ordinary test target.
|
||||
add_cpp_test(ondemand_key_selector_reflection_tests LABELS ondemand acceptance per_implementation)
|
||||
add_cpp_test(ondemand_stl_types_tests LABELS ondemand acceptance per_implementation)
|
||||
add_cpp_test(ondemand_convert_tests LABELS ondemand acceptance per_implementation)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Exercises the experimental key-selector-based reflective deserializer.
|
||||
//
|
||||
// We enable the option here (before including simdjson) so the reflective
|
||||
// tag_invoke uses a compile-time key_selector + object::for_each single pass
|
||||
// instead of one obj[key] lookup per member. The point of the option is to
|
||||
// deserialize structs correctly even when the JSON keys are NOT in declaration
|
||||
// order, so the tests below deliberately scramble the key order.
|
||||
#define SIMDJSON_USE_KEY_SELECTOR_REFLECTION 1
|
||||
// Exercises the key-selector-based reflective deserializer, which is the
|
||||
// default for reflective tag_invoke: a compile-time key_selector +
|
||||
// object::for_each single pass instead of one obj[key] lookup per member.
|
||||
// (It can be disabled with -DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1.) The
|
||||
// point of this path is to deserialize structs correctly even when the JSON
|
||||
// keys are NOT in declaration order, so the tests below deliberately scramble
|
||||
// the key order.
|
||||
#include "simdjson.h"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -344,6 +344,47 @@ namespace object_tests {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if SIMDJSON_SUPPORTS_CONCEPTS
|
||||
// The key_selector's matcher (match_raw, the perfect hash) must return the
|
||||
// right selector index for every key, including tricky cases: keys that are
|
||||
// prefixes of one another, keys that extend a real key, a long key, and misses.
|
||||
// This guards the prefix/length disambiguation against the hash.
|
||||
bool key_selector_matchers_agree() {
|
||||
TEST_START();
|
||||
// Probe builds "<key>\"" in a padded buffer and checks match_raw returns the
|
||||
// expected selector index (or N for a miss).
|
||||
auto probe = [](auto sel_tag, std::string_view key, std::size_t expected) -> bool {
|
||||
using sel = decltype(sel_tag);
|
||||
char buf[64] = {};
|
||||
for (size_t i = 0; i < key.size(); ++i) { buf[i] = key[i]; }
|
||||
buf[key.size()] = '"';
|
||||
ondemand::raw_json_string r(reinterpret_cast<const uint8_t*>(buf));
|
||||
ASSERT_EQUAL(sel::match_raw(r), expected);
|
||||
return true;
|
||||
};
|
||||
// Selector with prefix keys and a 30-character key.
|
||||
using small_sel = ondemand::key_selector<"a", "ab", "abc", "id", "name",
|
||||
"abcdefghijklmnopqrstuvwxyz1234">;
|
||||
std::size_t i = 0;
|
||||
for (auto k : {"a", "ab", "abc", "id", "name", "abcdefghijklmnopqrstuvwxyz1234"}) {
|
||||
if (!probe(small_sel{}, k, i++)) { return false; }
|
||||
}
|
||||
for (auto k : {"x", "abcd", "nam", "names", "i", "ids", "zzzzz", ""}) {
|
||||
if (!probe(small_sel{}, k, small_sel::size())) { return false; }
|
||||
}
|
||||
// Larger selector exercising the same matcher.
|
||||
using big_sel = ondemand::key_selector<"k00","k01","k02","k03","k04","k05",
|
||||
"k06","k07","k08","k09","k10","k11">;
|
||||
if (!probe(big_sel{}, "k00", 0)) { return false; }
|
||||
if (!probe(big_sel{}, "k05", 5)) { return false; }
|
||||
if (!probe(big_sel{}, "k11", 11)) { return false; }
|
||||
for (auto k : {"k12","nope",""}) {
|
||||
if (!probe(big_sel{}, k, big_sel::size())) { return false; }
|
||||
}
|
||||
TEST_SUCCEED();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool run() {
|
||||
return
|
||||
object_find_field_unordered() &&
|
||||
@@ -355,6 +396,7 @@ namespace object_tests {
|
||||
#if SIMDJSON_SUPPORTS_CONCEPTS
|
||||
object_find_field_key_selector() &&
|
||||
object_for_each_callback_error() &&
|
||||
key_selector_matchers_agree() &&
|
||||
#endif
|
||||
#if SIMDJSON_EXCEPTIONS && SIMDJSON_SUPPORTS_CONCEPTS
|
||||
key_selector_example_toplevel() &&
|
||||
|
||||
Reference in New Issue
Block a user