diff --git a/.github/workflows/big-endian.yml b/.github/workflows/big-endian.yml index 826668ccd0..f4ca69404e 100644 --- a/.github/workflows/big-endian.yml +++ b/.github/workflows/big-endian.yml @@ -81,6 +81,7 @@ jobs: -DCMAKE_CXX_STANDARD=23 \ -Dglaze_BUILD_NETWORKING_TESTS=OFF \ -Dglaze_BUILD_PERFORMANCE_TESTS=OFF \ + -Dglaze_BUILD_ZMEM_TESTS=OFF \ -Dglaze_ENABLE_FUZZING=OFF - name: Build diff --git a/docs/zmem.md b/docs/zmem.md new file mode 100644 index 0000000000..b15cddb8e1 --- /dev/null +++ b/docs/zmem.md @@ -0,0 +1,350 @@ +# ZMEM (Zero-Copy Memory Format) + +ZMEM is a high-performance binary serialization format designed for maximum performance with **zero overhead for fixed structs** and efficient handling of variable types with vectors and strings. + +## Key Features + +| Feature | Description | +|---------|-------------| +| **Zero overhead** | Fixed structs serialize with exact `sizeof()` bytes - no headers, no tags | +| **Zero-copy reads** | Fixed types can be read directly from the buffer without copying | +| **Deterministic** | Same logical value always produces identical bytes | +| **Little-endian** | Explicit wire format specification for cross-platform compatibility | +| **Pre-allocation** | Compute exact serialized size before writing to eliminate resize checks | + +## When to Use ZMEM + +ZMEM is ideal for: + +- **High-frequency data**: Game physics, sensor data, network packets +- **Embedded systems**: Memory-constrained environments where every byte counts +- **Real-time systems**: Predictable performance with no allocations during serialization +- **Scientific/engineering data**: Structured numerical data with minimal overhead +- **Shared memory IPC**: Direct memory mapping with known layouts + +For self-describing formats with schema evolution, consider [BEVE](./binary.md) or [MessagePack](./msgpack.md) instead. + +## Basic Usage + +```cpp +#include "glaze/zmem.hpp" + +// Fixed struct - trivially copyable (zero overhead) +struct Point { + float x; + float y; +}; + +Point p{1.0f, 2.0f}; +std::string buffer; + +// Write ZMEM +auto ec = glz::write_zmem(p, buffer); +if (!ec) { + // Success: buffer.size() == sizeof(Point) == 8 bytes +} + +// Read ZMEM +Point result{}; +ec = glz::read_zmem(result, buffer); +if (!ec) { + // Success: result.x == 1.0f, result.y == 2.0f +} +``` + +## Fixed vs Variable Types + +ZMEM distinguishes between two categories of types: + +### Fixed Types (Zero Overhead) + +Fixed types are trivially copyable and serialize as their raw bytes. Fixed structs are padded to an 8-byte boundary on the wire for safe zero-copy access: + +- Primitives: `bool`, `int8_t` through `int64_t`, `uint8_t` through `uint64_t`, `float`, `double` +- Enums +- Fixed-size arrays: `T[N]`, `std::array` +- Structs containing only fixed types + +```cpp +struct Transform { + float position[3]; + float rotation[3]; + float scale[3]; +}; +static_assert(sizeof(Transform) == 36); + +Transform t{{1,2,3}, {4,5,6}, {7,8,9}}; +std::string buffer; +glz::write_zmem(t, buffer); +// buffer.size() == 40 (padded to 8-byte boundary) +``` + +### Variable Types (With Headers) + +Variable types contain variable-length data and require headers: + +- `std::vector`: 8-byte count + elements (+ offset table for variable elements) +- `std::string`: 8-byte length + character data +- `std::map`: Count + entries (variable values stored in the map payload) +- Structs containing vectors or strings: 8-byte size header + inline section + variable section + +```cpp +struct Entity { + uint64_t id; + std::vector weights; + std::string name; +}; + +Entity e{42, {1.0f, 2.0f, 3.0f}, "Player"}; +std::string buffer; +glz::write_zmem(e, buffer); +// Format: [size:8][id:8][weights_ref:16][name_ref:16][weights_data][name_data] +``` + +## Size Pre-computation + +ZMEM supports computing the exact serialized size before writing, enabling pre-allocation to eliminate all resize checks during serialization: + +```cpp +#include "glaze/zmem.hpp" + +struct SensorData { + uint64_t timestamp; + std::vector readings; +}; + +SensorData data{12345, {1.0, 2.0, 3.0, 4.0, 5.0}}; + +// Compute exact size +size_t size = glz::size_zmem(data); + +// Pre-allocate buffer +std::string buffer; +buffer.resize(size); + +// Write with no resize checks +size_t bytes_written = 0; +glz::write_zmem_unchecked(data, buffer, bytes_written); +``` + +### Pre-allocated Write (Recommended for Performance) + +The `write_zmem_preallocated` function combines size computation and unchecked writing: + +```cpp +SensorData data{12345, {1.0, 2.0, 3.0}}; + +// Automatic: compute size, allocate, write unchecked +std::string buffer; +auto ec = glz::write_zmem_preallocated(data, buffer); + +// Or get the buffer directly +auto result = glz::write_zmem_preallocated(data); +if (result) { + std::string buffer = std::move(*result); +} +``` + +This is the fastest way to serialize when you don't have a reusable buffer. + +## File I/O + +```cpp +#include "glaze/zmem.hpp" + +struct Config { + int version; + std::string name; + std::vector values; +}; + +Config config{1, "settings", {1.0, 2.0, 3.0}}; +std::string buffer; + +// Write to file +auto ec = glz::write_file_zmem(config, "config.zmem", buffer); + +// Read from file +Config loaded{}; +ec = glz::read_file_zmem(loaded, "config.zmem", buffer); +``` + +## ZMEM Optional + +ZMEM provides `glz::zmem::optional` with a guaranteed memory layout for wire compatibility: + +```cpp +// Layout: [present:1][padding:alignof(T)-1][value:sizeof(T)] +glz::zmem::optional opt(42); // sizeof == 8, alignof == 4 +glz::zmem::optional opt64; // sizeof == 16, alignof == 8 + +// Serialize +std::string buffer; +glz::write_zmem(opt, buffer); + +// std::optional also supported (converted to zmem::optional on wire) +std::optional std_opt = 3.14f; +glz::write_zmem(std_opt, buffer); +``` + +## Maps + +ZMEM supports `std::map` with sorted keys (required by the format). Variable map values are stored in the map payload and referenced from each entry: + +```cpp +// Fixed value type - entries only +std::map fixed_map = {{1, 1.0f}, {2, 2.0f}, {3, 3.0f}}; +glz::write_zmem(fixed_map, buffer); + +// Variable value type - entries include refs to payload data +std::map variable_map = { + {1, "one"}, + {2, "two"}, + {3, "three"} +}; +glz::write_zmem(variable_map, buffer); +``` + +`std::unordered_map` is also supported - keys are sorted before serialization. + +## Glaze Metadata Support + +ZMEM works with both automatic reflection and explicit `glz::meta` metadata: + +```cpp +// Automatic reflection (C++20 aggregate) +struct AutoPoint { + float x; + float y; +}; + +// Explicit metadata +struct MetaPoint { + float x_coord; + float y_coord; +}; + +template <> +struct glz::meta { + using T = MetaPoint; + static constexpr auto value = object( + "x", &T::x_coord, + "y", &T::y_coord + ); +}; + +// Both work identically with ZMEM +AutoPoint ap{1.0f, 2.0f}; +MetaPoint mp{1.0f, 2.0f}; + +glz::write_zmem(ap, buffer1); // 8 bytes +glz::write_zmem(mp, buffer2); // 8 bytes +``` + +## Wire Format Specification + +### Fixed Types + +| Type | Wire Size | Format | +|------|-----------|--------| +| `bool` | 1 byte | 0x00 or 0x01 | +| `int8_t`/`uint8_t` | 1 byte | Raw byte | +| `int16_t`/`uint16_t` | 2 bytes | Little-endian | +| `int32_t`/`uint32_t`/`float` | 4 bytes | Little-endian | +| `int64_t`/`uint64_t`/`double` | 8 bytes | Little-endian | +| `T[N]` | `N * sizeof(T)` | Contiguous elements | +| Fixed struct | `align_up(sizeof(T), max(8, alignof(T)))` | Direct memory layout + padding | + +### Variable Types + +| Type | Wire Format | +|------|-------------| +| `std::vector` (fixed T) | `[count:8][elements...]` | +| `std::vector` (variable T) | `[count:8][offset_table:(count+1)*8][elements...]` | +| `std::string` | `[length:8][chars...]` | +| Variable struct | `[size:8][inline_section][variable_section]` | +| `std::map` (fixed V) | `[count:8][entries...]` | +| `std::map` (variable V) | `[count:8][entries...][variable_data...]` | + +### Variable Struct Layout + +For structs containing vectors or strings: + +``` +[size:8] # Total size of struct data (excluding this header) +[inline_base padding] # Optional padding to align the inline section +[inline section] # Fixed-size fields + references to variable data + - Simple fields: raw bytes + - Vector fields: [offset:8][count:8] + - String fields: [offset:8][length:8] + - Map fields: [offset:8][count:8] +[variable section] # Variable-length data + - Vector data + - String data + - Map payloads +``` + +Offsets are relative to inline_base (start of inline section after size header + padding). + +## Error Handling + +```cpp +struct Data { + int value; + std::string name; +}; + +Data d{42, "test"}; +std::string buffer; + +auto ec = glz::write_zmem(d, buffer); +if (ec) { + // Handle error + std::cerr << "Write error: " << glz::format_error(ec, buffer) << "\n"; +} + +Data result{}; +ec = glz::read_zmem(result, buffer); +if (ec) { + // Handle error (e.g., unexpected_end for truncated data) + std::cerr << "Read error: " << glz::format_error(ec, buffer) << "\n"; +} +``` + +## Performance Characteristics + +| Operation | Fixed Types | Variable Types | +|-----------|-------------|----------------| +| Write | Single `memcpy` | Header + inline pass + variable pass | +| Read | Single `memcpy` | Parse headers + extract fields | +| Size computation | `sizeof(T)` | Traverse structure | + +For fixed structs, ZMEM pads to an 8-byte boundary for safe zero-copy access (at most 7 bytes of padding). + +## API Reference + +### Write Functions + +| Function | Description | +|----------|-------------| +| `write_zmem(value, buffer)` | Write ZMEM to buffer, returns `error_ctx` | +| `write_zmem(value)` | Write ZMEM to new string, returns `expected` | +| `write_zmem_preallocated(value, buffer)` | Compute size, allocate, write unchecked | +| `write_zmem_preallocated(value)` | Returns `expected` | +| `write_zmem_unchecked(value, buffer, bytes_written)` | Write without resize checks (buffer must be pre-sized) | +| `write_file_zmem(value, filename, buffer)` | Write to file | + +### Read Functions + +| Function | Description | +|----------|-------------| +| `read_zmem(value, buffer)` | Read ZMEM from buffer, returns `error_ctx` | +| `read_zmem(buffer)` | Read and return value, returns `expected` | +| `read_file_zmem(value, filename, buffer)` | Read from file | + +### Utility Functions + +| Function | Description | +|----------|-------------| +| `size_zmem(value)` | Compute exact serialized size | +| `set_zmem()` | Create options with ZMEM format | diff --git a/include/glaze/zmem.hpp b/include/glaze/zmem.hpp new file mode 100644 index 0000000000..c4114f86df --- /dev/null +++ b/include/glaze/zmem.hpp @@ -0,0 +1,14 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#pragma once + +// ZMEM: Zero-Copy Memory Format +// A high-performance binary serialization format designed for maximum performance +// with zero overhead for simple structs and zero-copy deserialization. + +#include "glaze/zmem/header.hpp" +#include "glaze/zmem/size.hpp" +#include "glaze/zmem/write.hpp" +#include "glaze/zmem/read.hpp" +#include "glaze/zmem/lazy.hpp" diff --git a/include/glaze/zmem/header.hpp b/include/glaze/zmem/header.hpp new file mode 100644 index 0000000000..3650caa4d3 --- /dev/null +++ b/include/glaze/zmem/header.hpp @@ -0,0 +1,404 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "glaze/core/opts.hpp" +#include "glaze/core/context.hpp" +#include "glaze/core/common.hpp" +#include "glaze/util/inline.hpp" + +namespace glz +{ + // ZMEM format constant + // Using value 3 (next available after BEVE=1, CBOR=2) + inline constexpr uint32_t ZMEM = 3; + + // ZMEM requires little-endian byte order for zero-copy memory access + static_assert(std::endian::native == std::endian::little, + "ZMEM format requires a little-endian architecture"); + + namespace zmem + { + // ============================================================================ + // Endianness Utilities + // ============================================================================ + + // ZMEM uses little-endian byte order for all multi-byte values + template + GLZ_ALWAYS_INLINE constexpr void byteswap_le(T& value) noexcept + { + if constexpr (sizeof(T) > 1) { + if constexpr (std::endian::native == std::endian::big) { + value = std::byteswap(value); + } + } + } + + template + GLZ_ALWAYS_INLINE constexpr T to_little_endian(T value) noexcept + { + byteswap_le(value); + return value; + } + + template + GLZ_ALWAYS_INLINE constexpr T from_little_endian(T value) noexcept + { + byteswap_le(value); + return value; + } + + // ============================================================================ + // Wire Format Types + // ============================================================================ + + // Vector reference stored in inline section for variable structs + struct alignas(8) vector_ref + { + uint64_t offset; // Byte offset to array data (relative to inline_base) + uint64_t count; // Number of elements + }; + static_assert(sizeof(vector_ref) == 16); + static_assert(alignof(vector_ref) == 8); + + // String reference for variable-length strings + struct alignas(8) string_ref + { + uint64_t offset; // Byte offset to string data (relative to inline_base) + uint64_t length; // Byte length (NOT null-terminated) + }; + static_assert(sizeof(string_ref) == 16); + static_assert(alignof(string_ref) == 8); + + // Map reference stored in inline section for variable structs + struct alignas(8) map_ref + { + uint64_t offset; // Byte offset to map payload (relative to inline_base) + uint64_t count; // Number of entries + }; + static_assert(sizeof(map_ref) == 16); + static_assert(alignof(map_ref) == 8); + + // ============================================================================ + // Optional Type with Guaranteed Layout + // ============================================================================ + + // ZMEM optional with explicit layout for wire compatibility + // Layout: [present:1][padding:alignment-1][value:sizeof(T)] + // Use 8-byte alignment for 64-bit types to ensure consistent layout across 32/64-bit platforms + template + inline constexpr size_t optional_alignment_v = (sizeof(T) >= 8 ? 8 : alignof(T)); + + template + struct optional_padding_storage + { + std::array bytes{}; + }; + + template <> + struct optional_padding_storage<0> {}; + + template + struct alignas(optional_alignment_v) optional + { + uint8_t present{0}; + [[no_unique_address]] optional_padding_storage - 1> padding{}; + T value{}; + + constexpr optional() noexcept = default; + constexpr optional(std::nullopt_t) noexcept : present(0), value{} {} + constexpr optional(const T& v) noexcept : present(1), value(v) {} + constexpr optional(T&& v) noexcept : present(1), value(std::move(v)) {} + + constexpr bool has_value() const noexcept { return present != 0; } + constexpr explicit operator bool() const noexcept { return has_value(); } + + constexpr T& operator*() noexcept { return value; } + constexpr const T& operator*() const noexcept { return value; } + constexpr T* operator->() noexcept { return &value; } + constexpr const T* operator->() const noexcept { return &value; } + + constexpr T value_or(const T& default_value) const noexcept + { + return has_value() ? value : default_value; + } + + constexpr void reset() noexcept + { + present = 0; + value = T{}; + } + + constexpr optional& operator=(std::nullopt_t) noexcept + { + reset(); + return *this; + } + + constexpr optional& operator=(const T& v) noexcept + { + present = 1; + value = v; + return *this; + } + }; + + // Verify optional layout matches ZMEM spec + static_assert(sizeof(optional) == 2); + static_assert(sizeof(optional) == 4); + static_assert(sizeof(optional) == 8); + static_assert(sizeof(optional) == 16); + static_assert(alignof(optional) == 4); + static_assert(alignof(optional) == 8); + + // ============================================================================ + // Type Traits for ZMEM Categories + // ============================================================================ + + // Check if a type is a ZMEM optional + template + struct is_zmem_optional : std::false_type {}; + + template + struct is_zmem_optional> : std::true_type {}; + + template + inline constexpr bool is_zmem_optional_v = is_zmem_optional::value; + + // Check if a type is a std::vector (which makes struct variable) + template + struct is_std_vector : std::false_type {}; + + template + struct is_std_vector> : std::true_type {}; + + template + inline constexpr bool is_std_vector_v = is_std_vector::value; + + // Check if a type is a std::string (variable-length, makes struct variable) + template + struct is_std_string : std::false_type {}; + + template + struct is_std_string> : std::true_type {}; + + template + inline constexpr bool is_std_string_v = is_std_string::value; + + // Check if a type is a std::map + template + struct is_std_map : std::false_type {}; + + template + struct is_std_map> : std::true_type {}; + + template + inline constexpr bool is_std_map_v = is_std_map::value; + + // Check if a type is a std::unordered_map + template + struct is_std_unordered_map : std::false_type {}; + + template + struct is_std_unordered_map> : std::true_type {}; + + template + inline constexpr bool is_std_unordered_map_v = is_std_unordered_map::value; + + template + inline constexpr bool is_std_map_like_v = is_std_map_v || is_std_unordered_map_v; + + // Check if a type is a fixed-size array (char[N] for strings) + template + struct is_fixed_string : std::false_type {}; + + template + struct is_fixed_string : std::true_type {}; + + template + struct is_fixed_string : std::true_type {}; + + template + inline constexpr bool is_fixed_string_v = is_fixed_string::value; + + // Check if a type is a C-style array + template + struct is_c_array : std::false_type {}; + + template + struct is_c_array : std::true_type {}; + + template + inline constexpr bool is_c_array_v = is_c_array::value; + + // Check if a type is a std::array + template + struct is_std_array : std::false_type {}; + + template + struct is_std_array> : std::true_type {}; + + template + inline constexpr bool is_std_array_v = is_std_array::value; + + // ============================================================================ + // Fixed vs Variable Type Detection + // ============================================================================ + + // A type is "fixed" in ZMEM if it's trivially copyable + // (no vectors, no std::string, no variable nested types) + + // Forward declaration for recursive check + template + struct is_fixed_type; + + // Primitives are fixed + template + concept zmem_primitive = std::is_arithmetic_v || std::is_enum_v; + + // Check if type is fixed (can be memcpy'd directly) + template + struct is_fixed_type + { + static constexpr bool value = + std::is_trivially_copyable_v && + !is_std_vector_v && + !is_std_string_v; + }; + + template + struct is_fixed_type> + { + static constexpr bool value = is_fixed_type::value; + }; + + template + inline constexpr bool is_fixed_type_v = is_fixed_type::value; + + // Variable types contain vectors or variable-length strings + template + inline constexpr bool is_variable_type_v = !is_fixed_type_v; + + // ============================================================================ + // Stack Allocation Thresholds + // ============================================================================ + + // Maximum number of elements for stack-allocated offset tables + // 64 elements * 8 bytes = 512 bytes on stack (reasonable limit) + inline constexpr size_t offset_table_stack_threshold = 64; + + // ============================================================================ + // Alignment and Size Utilities + // ============================================================================ + + // Calculate padding needed to align to boundary + GLZ_ALWAYS_INLINE constexpr size_t padding_for_alignment(size_t offset, size_t alignment) noexcept + { + const size_t remainder = offset % alignment; + return remainder == 0 ? 0 : alignment - remainder; + } + + // Align offset up to boundary + GLZ_ALWAYS_INLINE constexpr size_t align_up(size_t offset, size_t alignment) noexcept + { + return offset + padding_for_alignment(offset, alignment); + } + + // Round size up to multiple of 8 (for fixed struct wire sizes) + GLZ_ALWAYS_INLINE constexpr size_t padded_size_8(size_t size) noexcept + { + return (size + 7) & ~size_t(7); + } + + // Round size up to alignment (for fixed struct wire sizes) + GLZ_ALWAYS_INLINE constexpr size_t padded_size(size_t size, size_t alignment) noexcept + { + return align_up(size, alignment); + } + + // ============================================================================ + // Buffer Operations + // ============================================================================ + + // Write a value to buffer at given index (with endian conversion) + // Template parameter `Unchecked`: if true, skip resize checks (buffer pre-allocated) + template + GLZ_ALWAYS_INLINE void write_value(T value, B& buffer, size_t& ix) noexcept + { + byteswap_le(value); + + if constexpr (!Unchecked && resizable) { + if (ix + sizeof(T) > buffer.size()) { + buffer.resize((std::max)(buffer.size() * 2, ix + sizeof(T))); + } + } + + std::memcpy(buffer.data() + ix, &value, sizeof(T)); + ix += sizeof(T); + } + + // Write raw bytes to buffer + // Template parameter `Unchecked`: if true, skip resize checks (buffer pre-allocated) + template + GLZ_ALWAYS_INLINE void write_bytes(const void* data, size_t size, B& buffer, size_t& ix) noexcept + { + if constexpr (!Unchecked && resizable) { + if (ix + size > buffer.size()) { + buffer.resize((std::max)(buffer.size() * 2, ix + size)); + } + } + + std::memcpy(buffer.data() + ix, data, size); + ix += size; + } + + // Write padding zeros + // Template parameter `Unchecked`: if true, skip resize checks (buffer pre-allocated) + template + GLZ_ALWAYS_INLINE void write_padding(size_t count, B& buffer, size_t& ix) noexcept + { + if (count == 0) return; + + if constexpr (!Unchecked && resizable) { + if (ix + count > buffer.size()) { + buffer.resize((std::max)(buffer.size() * 2, ix + count)); + } + } + + std::memset(buffer.data() + ix, 0, count); + ix += count; + } + + // Read a value from buffer (with endian conversion) + template + GLZ_ALWAYS_INLINE T read_value(const void* data) noexcept + { + T value; + std::memcpy(&value, data, sizeof(T)); + byteswap_le(value); + return value; + } + + template + GLZ_ALWAYS_INLINE T read_value(const void* data, size_t& ix) noexcept + { + T value = read_value(static_cast(data) + ix); + ix += sizeof(T); + return value; + } + + } // namespace zmem +} // namespace glz diff --git a/include/glaze/zmem/layout.hpp b/include/glaze/zmem/layout.hpp new file mode 100644 index 0000000000..37bb9c58a6 --- /dev/null +++ b/include/glaze/zmem/layout.hpp @@ -0,0 +1,197 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#pragma once + +#include "glaze/zmem/header.hpp" +#include "glaze/core/reflect.hpp" +#include "glaze/core/meta.hpp" + +#include +#include +#include + +namespace glz::zmem +{ + namespace detail + { + [[nodiscard]] consteval size_t max_size(size_t a, size_t b) noexcept + { + return a > b ? a : b; + } + } + + template + [[nodiscard]] consteval bool is_variable_member_type() noexcept + { + if constexpr (is_std_vector_v || is_std_string_v || is_std_map_like_v) { + return true; + } + else if constexpr (!is_fixed_type_v && !is_std_array_v) { + return true; // nested variable struct + } + else { + return false; + } + } + + template + [[nodiscard]] consteval size_t inline_field_alignment() noexcept + { + if constexpr (is_fixed_type_v) { + return alignof(T); + } + else { + return size_t{8}; + } + } + + template + [[nodiscard]] consteval size_t inline_field_size() noexcept + { + if constexpr (is_fixed_type_v) { + return sizeof(T); + } + else if constexpr (is_std_vector_v || is_std_string_v || is_std_map_like_v) { + return 16; // vector_ref, string_ref, map_ref + } + else { + return 8; // nested variable struct offset + } + } + + template + [[nodiscard]] consteval size_t vector_data_alignment() noexcept + { + if constexpr (is_fixed_type_v) { + return detail::max_size(size_t{8}, alignof(T)); + } + else { + return size_t{8}; + } + } + + template + [[nodiscard]] consteval size_t vector_fixed_stride() noexcept + { + if constexpr (std::is_aggregate_v && !is_std_array_v) { + const size_t alignment = detail::max_size(size_t{8}, alignof(T)); + return align_up(sizeof(T), alignment); + } + else { + return sizeof(T); + } + } + + template + [[nodiscard]] consteval size_t map_value_alignment() noexcept + { + if constexpr (is_fixed_type_v) { + return alignof(V); + } + else { + return size_t{8}; + } + } + + template + [[nodiscard]] consteval size_t map_entry_align() noexcept + { + if constexpr (is_fixed_type_v) { + return detail::max_size(alignof(K), alignof(V)); + } + else { + return detail::max_size(alignof(K), size_t{8}); + } + } + + template + [[nodiscard]] consteval size_t map_value_offset_in_entry() noexcept + { + return align_up(sizeof(K), map_value_alignment()); + } + + template + [[nodiscard]] consteval size_t map_entry_payload_size() noexcept + { + if constexpr (is_fixed_type_v) { + return align_up(sizeof(K), map_value_alignment()) + sizeof(V); + } + else if constexpr (is_std_vector_v || is_std_string_v) { + return align_up(sizeof(K), size_t{8}) + 16; + } + else { + return align_up(sizeof(K), size_t{8}) + 8; + } + } + + template + [[nodiscard]] consteval size_t map_entry_stride() noexcept + { + return align_up(map_entry_payload_size(), map_entry_align()); + } + + template + [[nodiscard]] consteval size_t map_data_alignment() noexcept + { + return detail::max_size(size_t{8}, map_entry_align()); + } + + template + struct inline_layout + { + static constexpr size_t N = reflect::size; + + template + using member_type_at = field_t; + + template + static consteval size_t field_alignment() noexcept + { + return inline_field_alignment>(); + } + + template + static consteval size_t field_size() noexcept + { + return inline_field_size>(); + } + + static consteval size_t compute_inline_align() noexcept + { + size_t alignment = size_t{8}; + [&](std::index_sequence) { + ((alignment = detail::max_size(alignment, field_alignment())), ...); + }(std::make_index_sequence{}); + return alignment; + } + + static consteval auto compute_offsets() noexcept + { + std::array offsets{}; + size_t offset = 0; + [&](std::index_sequence) { + ((offset = align_up(offset, field_alignment()), + offsets[Is] = offset, + offset += field_size()), ...); + }(std::make_index_sequence{}); + return offsets; + } + + static constexpr auto Offsets = compute_offsets(); + + static constexpr size_t InlineSectionSize = []() consteval { + if constexpr (N == 0) { + return size_t{0}; + } + else { + return Offsets[N - 1] + field_size(); + } + }(); + + static constexpr size_t InlineSectionAlign = compute_inline_align(); + static constexpr size_t InlineBasePadding = padding_for_alignment(8, InlineSectionAlign); + static constexpr size_t InlineBaseOffset = 8 + InlineBasePadding; + }; + +} // namespace glz::zmem diff --git a/include/glaze/zmem/lazy.hpp b/include/glaze/zmem/lazy.hpp new file mode 100644 index 0000000000..64d8f81e0f --- /dev/null +++ b/include/glaze/zmem/lazy.hpp @@ -0,0 +1,250 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#pragma once + +#include +#include + +#include "glaze/zmem/header.hpp" +#include "glaze/zmem/layout.hpp" +#include "glaze/core/reflect.hpp" +#include "glaze/reflection/to_tuple.hpp" + +namespace glz +{ + namespace zmem + { + template + struct strided_span + { + const char* data{}; + size_t count{}; + size_t stride{}; + + [[nodiscard]] size_t size() const noexcept { return count; } + + [[nodiscard]] const T& operator[](size_t index) const noexcept + { + return *reinterpret_cast(data + index * stride); + } + }; + } + + // ============================================================================ + // lazy_zmem_view - Zero-copy view into ZMEM data + // ============================================================================ + + /** + * @brief A lazy, zero-copy view into ZMEM serialized data. + * + * Unlike read_zmem which copies data into native C++ types (std::string, std::vector), + * lazy_zmem_view provides direct access to the serialized buffer: + * - Strings are accessed as std::string_view (no allocation) + * - Fixed-type vectors are accessed as std::span (no allocation) + * - Primitives are read directly from the buffer + * + * This enables zero-copy access patterns similar to FlatBuffers and Cap'n Proto. + * + * @tparam T The C++ type that describes the ZMEM structure + * + * Usage: + * @code + * std::string buffer; + * glz::write_zmem(my_obj, buffer); + * + * auto view = glz::lazy_zmem(buffer); + * std::string_view name = view.get<0>(); // Zero-copy string access + * std::span ids = view.get<1>(); // Zero-copy vector access + * @endcode + */ + template + struct lazy_zmem_view + { + private: + const char* data_{}; + size_t size_{}; + + // For variable structs, base points to the start of the inline section (after size header) + // For fixed structs, base points to the struct data directly + const char* base_{}; + + public: + lazy_zmem_view() = default; + + lazy_zmem_view(const char* data, size_t size) noexcept + : data_(data), size_(size) + { + if constexpr (zmem::is_fixed_type_v) { + base_ = data_; + } + else { + // Variable struct: skip size header and inline-base padding + base_ = data_ + zmem::inline_layout::InlineBaseOffset; + } + } + + template + requires requires(Buffer b) { b.data(); b.size(); } + explicit lazy_zmem_view(const Buffer& buffer) noexcept + : lazy_zmem_view(reinterpret_cast(buffer.data()), buffer.size()) + {} + + [[nodiscard]] bool valid() const noexcept { return data_ != nullptr && size_ > 0; } + [[nodiscard]] const char* data() const noexcept { return data_; } + [[nodiscard]] size_t size() const noexcept { return size_; } + + // ======================================================================== + // Fixed Struct Access + // ======================================================================== + + /** + * @brief Access the underlying fixed struct directly (only for fixed types) + * @return Const reference to the struct in the buffer + */ + [[nodiscard]] const T& as_fixed() const noexcept + requires zmem::is_fixed_type_v + { + return *reinterpret_cast(data_); + } + + // ======================================================================== + // Field Access by Index + // ======================================================================== + + /** + * @brief Get a field by compile-time index. + * + * For fixed types: returns the value directly + * For strings: returns std::string_view (zero-copy) + * For vectors of fixed types: returns std::span (zero-copy) + * + * @tparam I Field index (0-based) + * @return The field value or view + */ + template + [[nodiscard]] auto get() const noexcept + { + static_assert(I < reflect::size, "Field index out of bounds"); + using MemberType = field_t; + + if constexpr (zmem::is_fixed_type_v) { + // Fixed struct: direct access via reinterpret_cast + const T* ptr = reinterpret_cast(data_); + if constexpr (reflectable) { + return get_member(*ptr, glz::get(to_tie(*ptr))); + } + else { + return get_member(*ptr, glz::get(reflect::values)); + } + } + else { + // Variable struct: calculate offset at runtime and access + const char* field_ptr = compute_field_ptr(); + return read_field(field_ptr); + } + } + + private: + static constexpr auto N = reflect::size; + + // Compute field pointer at runtime, handling nested variable structs + template + [[nodiscard]] const char* compute_field_ptr() const noexcept + { + return base_ + zmem::inline_layout::Offsets[TargetIndex]; + } + + template + [[nodiscard]] auto read_field(const char* field_ptr) const noexcept + { + if constexpr (zmem::is_fixed_type_v) { + // Fixed field: read value directly + MemberType value; + std::memcpy(&value, field_ptr, sizeof(MemberType)); + return value; + } + else if constexpr (zmem::is_std_string_v) { + // String field: return string_view into buffer + zmem::string_ref ref; + std::memcpy(&ref, field_ptr, sizeof(zmem::string_ref)); + return std::string_view{base_ + ref.offset, static_cast(ref.length)}; + } + else if constexpr (zmem::is_std_vector_v) { + using ElemType = typename MemberType::value_type; + + zmem::vector_ref ref; + std::memcpy(&ref, field_ptr, sizeof(zmem::vector_ref)); + + if constexpr (zmem::is_fixed_type_v) { + constexpr size_t stride = zmem::vector_fixed_stride(); + if constexpr (stride == sizeof(ElemType)) { + const ElemType* elem_ptr = reinterpret_cast(base_ + ref.offset); + return std::span{elem_ptr, static_cast(ref.count)}; + } + else { + return zmem::strided_span{base_ + ref.offset, static_cast(ref.count), stride}; + } + } + else { + // Variable element vector: return raw data pointer and count + // A more complete implementation would return a lazy iterator + return std::pair{base_ + ref.offset, ref.count}; + } + } + else if constexpr (zmem::is_std_map_like_v) { + zmem::map_ref ref; + std::memcpy(&ref, field_ptr, sizeof(zmem::map_ref)); + return std::pair{base_ + ref.offset, ref.count}; + } + else { + // Nested variable struct: return a lazy view into it + uint64_t offset; + std::memcpy(&offset, field_ptr, sizeof(uint64_t)); + const char* nested_ptr = base_ + offset; + uint64_t nested_size; + std::memcpy(&nested_size, nested_ptr, sizeof(uint64_t)); + return lazy_zmem_view{nested_ptr, static_cast(sizeof(uint64_t) + nested_size)}; + } + } + }; + + // ============================================================================ + // Public API Functions + // ============================================================================ + + /** + * @brief Create a lazy zero-copy view into ZMEM data. + * + * @tparam T The C++ type describing the structure + * @param buffer The serialized ZMEM data (must remain valid for view lifetime) + * @return A lazy view that provides zero-copy field access + * + * @code + * auto view = glz::lazy_zmem(buffer); + * auto name = view.get<0>(); // Returns std::string_view for string fields + * @endcode + */ + template + requires requires(Buffer b) { b.data(); b.size(); } + [[nodiscard]] GLZ_ALWAYS_INLINE lazy_zmem_view lazy_zmem(const Buffer& buffer) noexcept + { + return lazy_zmem_view{buffer}; + } + + /** + * @brief Create a lazy view from raw pointer and size. + */ + template + [[nodiscard]] GLZ_ALWAYS_INLINE lazy_zmem_view lazy_zmem(const char* data, size_t size) noexcept + { + return lazy_zmem_view{data, size}; + } + + template + [[nodiscard]] GLZ_ALWAYS_INLINE lazy_zmem_view lazy_zmem(const void* data, size_t size) noexcept + { + return lazy_zmem_view{static_cast(data), size}; + } + +} // namespace glz diff --git a/include/glaze/zmem/read.hpp b/include/glaze/zmem/read.hpp new file mode 100644 index 0000000000..b7c1cef39e --- /dev/null +++ b/include/glaze/zmem/read.hpp @@ -0,0 +1,788 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#pragma once + +#include "glaze/zmem/header.hpp" +#include "glaze/zmem/layout.hpp" +#include "glaze/core/read.hpp" +#include "glaze/core/reflect.hpp" +#include "glaze/core/meta.hpp" +#include "glaze/core/common.hpp" +#include "glaze/concepts/container_concepts.hpp" +#include "glaze/file/file_ops.hpp" +#include "glaze/util/for_each.hpp" +#include "glaze/reflection/to_tuple.hpp" + +#include + +namespace glz +{ + // ============================================================================ + // ZMEM Parse Entry Point + // ============================================================================ + + template <> + struct parse + { + template + GLZ_ALWAYS_INLINE static void op(T&& value, Ctx&& ctx, It0&& it, It1&& end) + { + using V = std::remove_cvref_t; + from::template op(std::forward(value), std::forward(ctx), + std::forward(it), std::forward(end)); + } + }; + + namespace zmem + { + template + GLZ_ALWAYS_INLINE void read_fixed_raw(T& value, const char* data) noexcept + { + if constexpr (std::is_arithmetic_v || std::is_enum_v) { + value = read_value(data); + } + else { + std::memcpy(&value, data, sizeof(T)); + } + } + + template + GLZ_ALWAYS_INLINE void read_vector_payload(std::vector& value, + uint64_t count, + const char* data, + const char* struct_end, + is_context auto&& ctx) + { + value.resize(count); + if (count == 0) { + return; + } + + if constexpr (is_fixed_type_v) { + if constexpr (std::is_aggregate_v && !is_std_array_v) { + constexpr size_t stride = vector_fixed_stride(); + const size_t data_size = stride * count; + if (data + data_size > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + + if constexpr (stride == sizeof(ElemType)) { + std::memcpy(value.data(), data, sizeof(ElemType) * count); + } + else { + for (size_t i = 0; i < count; ++i) { + std::memcpy(&value[i], data + i * stride, sizeof(ElemType)); + } + } + } + else { + const size_t data_size = sizeof(ElemType) * count; + if (data + data_size > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + std::memcpy(value.data(), data, data_size); + } + } + else { + const size_t offset_table_size = (count + 1) * sizeof(uint64_t); + if (data + offset_table_size > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + + uint64_t stack_offsets[offset_table_stack_threshold + 1]; + std::vector heap_offsets; + uint64_t* offsets; + + if (count <= offset_table_stack_threshold) { + offsets = stack_offsets; + } + else { + heap_offsets.resize(count + 1); + offsets = heap_offsets.data(); + } + + std::memcpy(offsets, data, offset_table_size); + + if constexpr (std::endian::native == std::endian::big) { + for (size_t i = 0; i <= count; ++i) { + offsets[i] = std::byteswap(offsets[i]); + } + } + + const char* data_start = data + offset_table_size; + for (size_t i = 0; i < count; ++i) { + const char* elem_it = data_start + offsets[i]; + const char* elem_end = data_start + offsets[i + 1]; + if (elem_end > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + + if constexpr (is_std_string_v) { + using CharT = typename ElemType::value_type; + const size_t length_bytes = static_cast(elem_end - elem_it); + value[i].resize(length_bytes / sizeof(CharT)); + if (length_bytes > 0) { + std::memcpy(value[i].data(), elem_it, length_bytes); + } + } + else { + auto elem_ptr = elem_it; + from::template op(value[i], ctx, elem_ptr, struct_end); + if (bool(ctx.error)) return; + } + } + } + } + + template + GLZ_ALWAYS_INLINE void read_map_payload(Map& value, + const char* entries_start, + uint64_t count, + const char* inline_base, + const char* struct_end, + is_context auto&& ctx) + { + using K = typename Map::key_type; + using V = typename Map::mapped_type; + + value.clear(); + if (count == 0) { + return; + } + + constexpr size_t entry_stride = map_entry_stride(); + constexpr size_t value_offset = map_value_offset_in_entry(); + + const char* entries_end = entries_start + entry_stride * count; + if (entries_end > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + + if constexpr (is_fixed_type_v) { + for (size_t i = 0; i < count; ++i) { + const char* entry_ptr = entries_start + i * entry_stride; + K key{}; + read_fixed_raw(key, entry_ptr); + const char* value_ptr = entry_ptr + value_offset; + V val{}; + read_fixed_raw(val, value_ptr); + value.emplace(std::move(key), std::move(val)); + } + } + else { + struct entry_ref { + K key{}; + uint64_t offset{}; + uint64_t aux{}; + }; + + std::vector refs; + refs.reserve(static_cast(count)); + + for (size_t i = 0; i < count; ++i) { + const char* entry_ptr = entries_start + i * entry_stride; + entry_ref ref{}; + read_fixed_raw(ref.key, entry_ptr); + ref.offset = read_value(entry_ptr + value_offset); + + if constexpr (is_std_vector_v || is_std_string_v) { + ref.aux = read_value(entry_ptr + value_offset + 8); + } + + refs.emplace_back(std::move(ref)); + } + + for (auto& ref : refs) { + if (inline_base + ref.offset > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + + if constexpr (is_std_vector_v) { + V vals; + read_vector_payload(vals, ref.aux, inline_base + ref.offset, struct_end, ctx); + if (bool(ctx.error)) return; + value.emplace(std::move(ref.key), std::move(vals)); + } + else if constexpr (is_std_string_v) { + V s; + const size_t length = static_cast(ref.aux); + if (inline_base + ref.offset + length > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + s.resize(length / sizeof(typename V::value_type)); + if (length > 0) { + std::memcpy(s.data(), inline_base + ref.offset, length); + } + value.emplace(std::move(ref.key), std::move(s)); + } + else { + V val{}; + auto it = inline_base + ref.offset; + from::template op(val, ctx, it, struct_end); + if (bool(ctx.error)) return; + value.emplace(std::move(ref.key), std::move(val)); + } + } + } + } + } // namespace zmem + + // ============================================================================ + // Read Specializations + // ============================================================================ + + // Primitives (bool, integers, floats, enums) + template + requires(std::is_arithmetic_v || std::is_enum_v) + struct from final + { + template + GLZ_ALWAYS_INLINE static void op(T& value, is_context auto&& ctx, auto&& it, auto&& end) + { + if (static_cast(end - it) < sizeof(T)) { + ctx.error = error_code::unexpected_end; + return; + } + + std::memcpy(&value, &(*it), sizeof(T)); + zmem::byteswap_le(value); + it += sizeof(T); + } + }; + + // Fixed-size C arrays + template + struct from final + { + template + GLZ_ALWAYS_INLINE static void op(T (&value)[N], is_context auto&& ctx, auto&& it, auto&& end) + { + if constexpr (zmem::is_fixed_type_v) { + const size_t size = sizeof(T) * N; + if (static_cast(end - it) < size) { + ctx.error = error_code::unexpected_end; + return; + } + std::memcpy(value, &(*it), size); + it += size; + } + else { + for (std::size_t i = 0; i < N; ++i) { + from::template op(value[i], ctx, it, end); + if (bool(ctx.error)) return; + } + } + } + }; + + // std::array + template + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(std::array& value, is_context auto&& ctx, auto&& it, auto&& end) + { + if constexpr (zmem::is_fixed_type_v) { + const size_t size = sizeof(T) * N; + if (static_cast(end - it) < size) { + ctx.error = error_code::unexpected_end; + return; + } + std::memcpy(value.data(), &(*it), size); + it += size; + } + else { + for (std::size_t i = 0; i < N; ++i) { + from::template op(value[i], ctx, it, end); + if (bool(ctx.error)) return; + } + } + } + }; + + // ZMEM optional + template + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(zmem::optional& value, is_context auto&& ctx, auto&& it, auto&& end) + { + constexpr size_t opt_size = sizeof(zmem::optional); + if (static_cast(end - it) < opt_size) { + ctx.error = error_code::unexpected_end; + return; + } + std::memcpy(&value, &(*it), opt_size); + // Note: We need to byteswap the value if present + if (value.has_value()) { + zmem::byteswap_le(value.value); + } + it += opt_size; + } + }; + + // std::optional <- zmem::optional conversion on read + template + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(std::optional& value, is_context auto&& ctx, auto&& it, auto&& end) + { + zmem::optional zmem_opt; + from>::template op(zmem_opt, ctx, it, end); + if (bool(ctx.error)) return; + + if (zmem_opt.has_value()) { + value = zmem_opt.value; + } + else { + value = std::nullopt; + } + } + }; + + // std::vector - fixed element types + template + requires zmem::is_fixed_type_v + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(std::vector& value, is_context auto&& ctx, auto&& it, auto&& end) + { + // Array message format: count + elements + if (static_cast(end - it) < sizeof(uint64_t)) { + ctx.error = error_code::unexpected_end; + return; + } + + uint64_t count; + std::memcpy(&count, &(*it), sizeof(uint64_t)); + zmem::byteswap_le(count); + it += sizeof(uint64_t); + + value.resize(count); + if (count > 0) { + if constexpr (std::is_aggregate_v && !zmem::is_std_array_v) { + // Fixed struct elements have padding to alignment (min 8 bytes) + constexpr size_t wire_size = zmem::vector_fixed_stride(); + const size_t data_size = wire_size * count; + if (static_cast(end - it) < data_size) { + ctx.error = error_code::unexpected_end; + return; + } + + if constexpr (wire_size == sizeof(T)) { + // No padding, can memcpy all at once + std::memcpy(value.data(), &(*it), sizeof(T) * count); + } + else { + // Has padding, copy each element + for (size_t i = 0; i < count; ++i) { + std::memcpy(&value[i], &(*it) + i * wire_size, sizeof(T)); + } + } + it += data_size; + } + else { + // Primitives and arrays: no padding + const size_t data_size = sizeof(T) * count; + if (static_cast(end - it) < data_size) { + ctx.error = error_code::unexpected_end; + return; + } + std::memcpy(value.data(), &(*it), data_size); + it += data_size; + } + } + } + }; + + // std::vector - variable element types + template + requires(!zmem::is_fixed_type_v) + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(std::vector& value, is_context auto&& ctx, auto&& it, auto&& end) + { + if (static_cast(end - it) < sizeof(uint64_t)) { + ctx.error = error_code::unexpected_end; + return; + } + + uint64_t count; + std::memcpy(&count, &(*it), sizeof(uint64_t)); + zmem::byteswap_le(count); + it += sizeof(uint64_t); + + if (count == 0) { + value.clear(); + return; + } + + // Read offset table + const size_t offset_table_size = (count + 1) * sizeof(uint64_t); + if (static_cast(end - it) < offset_table_size) { + ctx.error = error_code::unexpected_end; + return; + } + + // Use stack allocation for small arrays, heap for large + // Stack: up to 64 elements (512 bytes), avoids heap allocation in hot path + uint64_t stack_offsets[zmem::offset_table_stack_threshold + 1]; + std::vector heap_offsets; + uint64_t* offsets; + + if (count <= zmem::offset_table_stack_threshold) { + offsets = stack_offsets; + } else { + heap_offsets.resize(count + 1); + offsets = heap_offsets.data(); + } + + // Bulk copy offset table then byteswap + std::memcpy(offsets, &(*it), offset_table_size); + it += offset_table_size; + + // Byteswap all offsets (no-op on little-endian systems) + if constexpr (std::endian::native == std::endian::big) { + for (size_t i = 0; i <= count; ++i) { + offsets[i] = std::byteswap(offsets[i]); + } + } + + // Now read each element + auto data_start = it; + value.resize(count); + + for (size_t i = 0; i < count; ++i) { + auto elem_it = data_start + offsets[i]; + auto elem_end = data_start + offsets[i + 1]; + + if constexpr (zmem::is_std_string_v) { + using CharT = typename T::value_type; + const size_t length_bytes = static_cast(elem_end - elem_it); + value[i].resize(length_bytes / sizeof(CharT)); + if (length_bytes > 0) { + std::memcpy(value[i].data(), &(*elem_it), length_bytes); + } + } + else { + from::template op(value[i], ctx, elem_it, elem_end); + if (bool(ctx.error)) return; + } + } + + // Advance iterator past all data + it = data_start + offsets[count]; + } + }; + + // std::string + template + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(std::basic_string& value, + is_context auto&& ctx, auto&& it, auto&& end) + { + if (static_cast(end - it) < sizeof(uint64_t)) { + ctx.error = error_code::unexpected_end; + return; + } + + uint64_t length; + std::memcpy(&length, &(*it), sizeof(uint64_t)); + zmem::byteswap_le(length); + it += sizeof(uint64_t); + + if (static_cast(end - it) < length) { + ctx.error = error_code::unexpected_end; + return; + } + + value.resize(length / sizeof(CharT)); + if (length > 0) { + std::memcpy(value.data(), &(*it), length); + it += length; + } + } + }; + + // std::pair (for map entries) + template + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(std::pair& value, is_context auto&& ctx, auto&& it, auto&& end) + { + from::template op(value.first, ctx, it, end); + if (bool(ctx.error)) return; + from::template op(value.second, ctx, it, end); + } + }; + + // std::map + template + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(std::map& value, + is_context auto&& ctx, auto&& it, auto&& end) + { + value.clear(); + + if (static_cast(end - it) < sizeof(uint64_t)) { + ctx.error = error_code::unexpected_end; + return; + } + + const char* map_start = &(*it); + const char* map_end = map_start + static_cast(end - it); + + uint64_t count; + std::memcpy(&count, &(*it), sizeof(uint64_t)); + zmem::byteswap_le(count); + it += sizeof(uint64_t); + + if (count == 0) { + return; + } + + const size_t map_alignment = zmem::map_data_alignment(); + const size_t padding = zmem::padding_for_alignment(static_cast(&(*it) - map_start), map_alignment); + if (static_cast(end - it) < padding) { + ctx.error = error_code::unexpected_end; + return; + } + it += padding; + + const char* entries_start = &(*it); + zmem::read_map_payload(value, entries_start, count, map_start, map_end, ctx); + if (bool(ctx.error)) return; + + it = end; + } + }; + + // std::unordered_map + template + struct from> final + { + template + GLZ_ALWAYS_INLINE static void op(std::unordered_map& value, + is_context auto&& ctx, auto&& it, auto&& end) + { + value.clear(); + + if (static_cast(end - it) < sizeof(uint64_t)) { + ctx.error = error_code::unexpected_end; + return; + } + + const char* map_start = &(*it); + const char* map_end = map_start + static_cast(end - it); + + uint64_t count; + std::memcpy(&count, &(*it), sizeof(uint64_t)); + zmem::byteswap_le(count); + it += sizeof(uint64_t); + + if (count == 0) { + return; + } + + const size_t map_alignment = zmem::map_data_alignment(); + const size_t padding = zmem::padding_for_alignment(static_cast(&(*it) - map_start), map_alignment); + if (static_cast(end - it) < padding) { + ctx.error = error_code::unexpected_end; + return; + } + it += padding; + + const char* entries_start = &(*it); + zmem::read_map_payload(value, entries_start, count, map_start, map_end, ctx); + if (bool(ctx.error)) return; + + it = end; + } + }; + + // ============================================================================ + // Fixed Struct Deserialization + // ============================================================================ + + template + requires(std::is_aggregate_v && zmem::is_fixed_type_v && !zmem::is_std_array_v) + struct from final + { + template + GLZ_ALWAYS_INLINE static void op(T& value, is_context auto&& ctx, auto&& it, auto&& end) + { + // Fixed struct: direct memcpy + skip padding to alignment (min 8 bytes) + constexpr size_t alignment = alignof(T) > 8 ? alignof(T) : 8; + constexpr size_t wire_size = zmem::padded_size(sizeof(T), alignment); + if (static_cast(end - it) < wire_size) { + ctx.error = error_code::unexpected_end; + return; + } + + std::memcpy(&value, &(*it), sizeof(T)); + it += wire_size; // Skip padding bytes too + } + }; + + // ============================================================================ + // Variable Struct Deserialization + // ============================================================================ + + template + requires(!zmem::is_fixed_type_v && !zmem::is_std_array_v + && (glz::reflectable || glz::glaze_object_t)) + struct from final + { + static constexpr auto N = reflect::size; + + // Member type helper (works for both reflectable and glaze_object_t) + template + using member_type_at = field_t; + + // Member access helper (works for both reflectable and glaze_object_t) + template + GLZ_ALWAYS_INLINE static decltype(auto) access_member(auto&& value) { + if constexpr (reflectable) { + return get_member(value, get(to_tie(value))); + } + else { + return get_member(value, get(reflect::values)); + } + } + + template + GLZ_ALWAYS_INLINE static void op(T& value, is_context auto&& ctx, auto&& it, auto&& end) + { + if (static_cast(end - it) < sizeof(uint64_t)) { + ctx.error = error_code::unexpected_end; + return; + } + + uint64_t total_size; + std::memcpy(&total_size, &(*it), sizeof(uint64_t)); + zmem::byteswap_le(total_size); + it += sizeof(uint64_t); + + if (static_cast(end - it) < total_size) { + ctx.error = error_code::unexpected_end; + return; + } + + const char* struct_start = &(*it); + const char* struct_end = struct_start + total_size; + const char* inline_base = struct_start + zmem::inline_layout::InlineBasePadding; + + if (inline_base + zmem::inline_layout::InlineSectionSize > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + + for_each([&]() { + if (bool(ctx.error)) return; + + using MemberType = member_type_at; + const char* field_ptr = inline_base + zmem::inline_layout::Offsets[I]; + + decltype(auto) member = access_member(value); + + if constexpr (zmem::is_fixed_type_v) { + std::memcpy(&member, field_ptr, sizeof(MemberType)); + } + else if constexpr (zmem::is_std_vector_v) { + zmem::vector_ref ref{}; + std::memcpy(&ref, field_ptr, sizeof(zmem::vector_ref)); + zmem::byteswap_le(ref.offset); + zmem::byteswap_le(ref.count); + + if (ref.count == 0) { + member.clear(); + return; + } + + zmem::read_vector_payload(member, ref.count, inline_base + ref.offset, struct_end, ctx); + } + else if constexpr (zmem::is_std_string_v) { + zmem::string_ref ref{}; + std::memcpy(&ref, field_ptr, sizeof(zmem::string_ref)); + zmem::byteswap_le(ref.offset); + zmem::byteswap_le(ref.length); + + member.resize(ref.length / sizeof(typename MemberType::value_type)); + if (ref.length > 0) { + if (inline_base + ref.offset + ref.length > struct_end) { + ctx.error = error_code::unexpected_end; + return; + } + std::memcpy(member.data(), inline_base + ref.offset, ref.length); + } + } + else if constexpr (zmem::is_std_map_like_v) { + zmem::map_ref ref{}; + std::memcpy(&ref, field_ptr, sizeof(zmem::map_ref)); + zmem::byteswap_le(ref.offset); + zmem::byteswap_le(ref.count); + + zmem::read_map_payload(member, inline_base + ref.offset, ref.count, inline_base, struct_end, ctx); + } + else { + uint64_t offset; + std::memcpy(&offset, field_ptr, sizeof(uint64_t)); + zmem::byteswap_le(offset); + + auto nested_it = inline_base + offset; + from::template op(member, ctx, nested_it, struct_end); + } + }); + + it = struct_end; + } + }; + + // ============================================================================ + // Public API Functions + // ============================================================================ + + // Read ZMEM from buffer + template + [[nodiscard]] GLZ_ALWAYS_INLINE error_ctx read_zmem(T&& value, Buffer&& buffer) + { + return read()>(std::forward(value), std::forward(buffer)); + } + + // Read ZMEM from buffer and return value + template + [[nodiscard]] GLZ_ALWAYS_INLINE expected read_zmem(Buffer&& buffer) + { + return read()>(std::forward(buffer)); + } + + // Read ZMEM from file + template + [[nodiscard]] GLZ_ALWAYS_INLINE error_ctx read_file_zmem(T&& value, const std::string_view file_name, + auto&& buffer) + { + const auto ec = file_to_buffer(buffer, file_name); + if (bool(ec)) [[unlikely]] { + return {ec}; + } + + return read()>(std::forward(value), buffer); + } + +} // namespace glz diff --git a/include/glaze/zmem/size.hpp b/include/glaze/zmem/size.hpp new file mode 100644 index 0000000000..df17a8b8f5 --- /dev/null +++ b/include/glaze/zmem/size.hpp @@ -0,0 +1,363 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#pragma once + +#include "glaze/zmem/header.hpp" +#include "glaze/zmem/layout.hpp" +#include "glaze/core/reflect.hpp" +#include "glaze/core/meta.hpp" +#include "glaze/core/common.hpp" +#include "glaze/reflection/to_tuple.hpp" + +#include + +namespace glz +{ + // ============================================================================ + // ZMEM Size Computation + // ============================================================================ + // + // Fast size computation for ZMEM serialization. This allows pre-allocating + // the exact buffer size needed, eliminating all resize checks during write. + // + // Usage: + // auto size = glz::size_zmem(value); + // std::string buffer(size, '\0'); + // glz::write_zmem(value, buffer); + // + + namespace zmem + { + // ======================================================================== + // Size computation for primitives + // ======================================================================== + + template + requires(std::is_arithmetic_v || std::is_enum_v) + GLZ_ALWAYS_INLINE constexpr size_t compute_size(const T&) noexcept + { + return sizeof(T); + } + + // ======================================================================== + // Size computation for fixed-size arrays + // ======================================================================== + + template + requires is_fixed_type_v + GLZ_ALWAYS_INLINE constexpr size_t compute_size(const T (&)[N]) noexcept + { + return sizeof(T) * N; + } + + template + requires is_fixed_type_v + GLZ_ALWAYS_INLINE constexpr size_t compute_size(const std::array&) noexcept + { + return sizeof(T) * N; + } + + // ======================================================================== + // Size computation for zmem::optional + // ======================================================================== + + template + GLZ_ALWAYS_INLINE constexpr size_t compute_size(const optional&) noexcept + { + return sizeof(optional); + } + + // ======================================================================== + // Size computation for std::optional + // ======================================================================== + + template + GLZ_ALWAYS_INLINE constexpr size_t compute_size(const std::optional&) noexcept + { + return sizeof(optional); // Converted to zmem::optional on write + } + + // ======================================================================== + // Size computation for vectors + // ======================================================================== + + // Fixed element type: count (8) + elements (each padded to 8 bytes if struct) + template + requires is_fixed_type_v + GLZ_ALWAYS_INLINE size_t compute_size(const std::vector& v) noexcept + { + if constexpr (std::is_aggregate_v && !is_std_array_v) { + // Fixed struct elements use padded stride (min 8 bytes) + return 8 + v.size() * vector_fixed_stride(); + } + else { + // Primitives and arrays use natural size + return 8 + v.size() * sizeof(T); + } + } + + // Forward declarations for mutually recursive size computation + template + requires(!is_fixed_type_v && !is_std_array_v && (glz::reflectable || glz::glaze_object_t)) + size_t compute_size(const T& value) noexcept; + + // Variable element type: count (8) + offset table + elements + template + requires(!is_fixed_type_v) + size_t compute_size(const std::vector& v) noexcept + { + const size_t count = v.size(); + return 8 + compute_vector_payload_size(v); + } + + template + GLZ_ALWAYS_INLINE size_t compute_vector_payload_size(const Vec& v) noexcept + { + using ElemType = typename Vec::value_type; + const size_t count = v.size(); + if (count == 0) { + return 0; + } + + if constexpr (is_fixed_type_v) { + if constexpr (std::is_aggregate_v && !is_std_array_v) { + return count * vector_fixed_stride(); + } + else { + return count * sizeof(ElemType); + } + } + else { + size_t total = (count + 1) * 8; // offset table + for (const auto& elem : v) { + if constexpr (is_std_string_v) { + using CharT = typename ElemType::value_type; + total += elem.size() * sizeof(CharT); + } + else { + total += compute_size(elem); + } + } + return total; + } + } + + template + GLZ_ALWAYS_INLINE size_t compute_map_payload_size(const Map& m, size_t payload_offset) noexcept + { + const size_t count = m.size(); + if (count == 0) { + return 0; + } + + constexpr size_t entry_stride = map_entry_stride(); + size_t offset = entry_stride * count; + + if constexpr (is_fixed_type_v) { + return offset; + } + + for (const auto& [k, v] : m) { + size_t value_alignment = size_t{8}; + if constexpr (is_std_vector_v) { + using ElemType = typename V::value_type; + value_alignment = vector_data_alignment(); + } + + const size_t pad = padding_for_alignment(payload_offset + offset, value_alignment); + offset += pad; + + if constexpr (is_std_vector_v) { + offset += compute_vector_payload_size(v); + } + else if constexpr (is_std_string_v) { + using CharT = typename V::value_type; + offset += v.size() * sizeof(CharT); + } + else { + offset += compute_size(v); + } + } + + return offset; + } + + // ======================================================================== + // Size computation for strings + // ======================================================================== + + template + GLZ_ALWAYS_INLINE size_t compute_size(const std::basic_string& s) noexcept + { + return 8 + s.size() * sizeof(CharT); // length + data + } + + template + GLZ_ALWAYS_INLINE size_t compute_size(const std::basic_string_view& s) noexcept + { + return 8 + s.size() * sizeof(CharT); // length + data + } + + // ======================================================================== + // Size computation for spans + // ======================================================================== + + template + requires is_fixed_type_v + GLZ_ALWAYS_INLINE size_t compute_size(const std::span& s) noexcept + { + if constexpr (Extent == std::dynamic_extent) { + return 8 + s.size() * sizeof(T); // count + elements + } + else { + return s.size() * sizeof(T); // elements only (fixed size) + } + } + + // ======================================================================== + // Size computation for pairs + // ======================================================================== + + template + requires is_fixed_type_v + GLZ_ALWAYS_INLINE size_t compute_size(const std::pair&) noexcept + { + // Fixed pair: key alignment + key + value alignment + value + // For size computation, we use worst-case alignment + return sizeof(K) + (alignof(V) - 1) + sizeof(V); + } + + template + requires(!is_fixed_type_v) + size_t compute_size(const std::pair& p) noexcept + { + // Variable value: key + value (no alignment padding in variable value maps) + return compute_size(p.first) + compute_size(p.second); + } + + // ======================================================================== + // Size computation for maps + // ======================================================================== + + // Fixed value type + template + requires is_fixed_type_v + size_t compute_size(const std::map& m) noexcept + { + size_t total = 8; // count + if (m.empty()) { + return total; + } + + const size_t padding = padding_for_alignment(total, map_data_alignment()); + total += padding; + const size_t payload_offset = total; + total += compute_map_payload_size(m, payload_offset); + return total; + } + + // Variable value type + template + requires(!is_fixed_type_v) + size_t compute_size(const std::map& m) noexcept + { + size_t total = 8; // count + if (m.empty()) { + return total; + } + + const size_t padding = padding_for_alignment(total, map_data_alignment()); + total += padding; + const size_t payload_offset = total; + total += compute_map_payload_size(m, payload_offset); + return total; + } + + // ======================================================================== + // Size computation for fixed structs (trivially copyable) + // ======================================================================== + + template + requires(std::is_aggregate_v && is_fixed_type_v && !is_std_array_v) + GLZ_ALWAYS_INLINE constexpr size_t compute_size(const T&) noexcept + { + // Fixed struct sizes are padded to alignment (min 8 bytes) for safe zero-copy access + constexpr size_t alignment = alignof(T) > 8 ? alignof(T) : 8; + return padded_size(sizeof(T), alignment); + } + + // ======================================================================== + // Size computation for variable structs + // ======================================================================== + + // Member access helper for size computation (works for both reflectable and glaze_object_t) + template + GLZ_ALWAYS_INLINE decltype(auto) access_member_for_size(const T& value) noexcept { + if constexpr (glz::reflectable) { + return get_member(value, get(to_tie(value))); + } + else { + return get_member(value, get(reflect::values)); + } + } + + template + requires(!is_fixed_type_v && !is_std_array_v && (glz::reflectable || glz::glaze_object_t)) + size_t compute_size(const T& value) noexcept + { + constexpr auto N = reflect::size; + using Layout = inline_layout; + + size_t variable_size = 0; + for_each([&]() { + decltype(auto) member = access_member_for_size(value); + using MemberType = field_t; + + if constexpr (is_std_vector_v) { + using ElemType = typename MemberType::value_type; + const size_t alignment = vector_data_alignment(); + const size_t current_offset = Layout::InlineSectionSize + variable_size; + variable_size += padding_for_alignment(current_offset, alignment); + variable_size += compute_vector_payload_size(member); + } + else if constexpr (is_std_string_v) { + const size_t current_offset = Layout::InlineSectionSize + variable_size; + variable_size += padding_for_alignment(current_offset, 8); + using CharT = typename MemberType::value_type; + variable_size += member.size() * sizeof(CharT); + } + else if constexpr (is_std_map_like_v) { + using K = typename MemberType::key_type; + using V = typename MemberType::mapped_type; + const size_t alignment = map_data_alignment(); + const size_t current_offset = Layout::InlineSectionSize + variable_size; + variable_size += padding_for_alignment(current_offset, alignment); + const size_t payload_offset = Layout::InlineSectionSize + variable_size; + variable_size += compute_map_payload_size(member, payload_offset); + } + else if constexpr (!is_fixed_type_v) { + const size_t current_offset = Layout::InlineSectionSize + variable_size; + variable_size += padding_for_alignment(current_offset, 8); + variable_size += compute_size(member); + } + }); + + size_t content_size = Layout::InlineBasePadding + Layout::InlineSectionSize + variable_size; + content_size += padding_for_alignment(content_size, 8); + return 8 + content_size; + } + + } // namespace zmem + + // ============================================================================ + // Public API: size_zmem + // ============================================================================ + + template + [[nodiscard]] GLZ_ALWAYS_INLINE size_t size_zmem(const T& value) noexcept + { + return zmem::compute_size(value); + } + +} // namespace glz diff --git a/include/glaze/zmem/write.hpp b/include/glaze/zmem/write.hpp new file mode 100644 index 0000000000..7255edbe4e --- /dev/null +++ b/include/glaze/zmem/write.hpp @@ -0,0 +1,934 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#pragma once + +#include "glaze/zmem/header.hpp" +#include "glaze/zmem/layout.hpp" +#include "glaze/core/write.hpp" +#include "glaze/core/reflect.hpp" +#include "glaze/core/meta.hpp" +#include "glaze/core/common.hpp" +#include "glaze/concepts/container_concepts.hpp" +#include "glaze/util/for_each.hpp" +#include "glaze/reflection/to_tuple.hpp" + +#include +#include +#include + +namespace glz +{ + // ============================================================================ + // Write Unchecked Helper + // ============================================================================ + // + // Check if the write_unchecked flag is set in opts. + // When set, buffer resize checks are skipped (buffer is pre-allocated). + // + + template + constexpr bool is_write_unchecked() noexcept + { + if constexpr (requires { Opts.internal; }) { + return (Opts.internal & static_cast(opts_internal::write_unchecked)) != 0; + } + else { + return false; + } + } + + // ============================================================================ + // ZMEM Serialize Entry Point + // ============================================================================ + + template <> + struct serialize + { + template + GLZ_ALWAYS_INLINE static void op(T&& value, Ctx&& ctx, B&& b, IX&& ix) + { + using V = std::remove_cvref_t; + to::template op(std::forward(value), std::forward(ctx), + std::forward(b), std::forward(ix)); + } + }; + + namespace zmem + { + // ============================================================================ + // Compile-Time Member Type Extraction + // ============================================================================ + + // Extract member type from member pointer type (e.g., int Foo::* -> int) + template + struct member_ptr_type; + + template + struct member_ptr_type { + using type = std::remove_cvref_t; + }; + + template + using member_ptr_type_t = typename member_ptr_type>::type; + + // ============================================================================ + // Compile-Time Variable Member Detection + // ============================================================================ + + // Check if a member pointer type points to a variable member (vector or string) + template + struct is_variable_member_ptr : std::false_type {}; + + template + requires (is_std_vector_v> || is_std_string_v>) + struct is_variable_member_ptr : std::true_type {}; + + template + inline constexpr bool is_variable_member_ptr_v = is_variable_member_ptr>::value; + + + template + GLZ_ALWAYS_INLINE void write_fixed_raw(const T& value, B& b, size_t& ix) noexcept + { + if constexpr (std::is_arithmetic_v || std::is_enum_v) { + write_value(value, b, ix); + } + else { + write_bytes(&value, sizeof(T), b, ix); + } + } + + template + GLZ_ALWAYS_INLINE void write_vector_data(const Vec& value, is_context auto&& ctx, B& b, size_t& ix) + { + constexpr bool unchecked = glz::is_write_unchecked(); + using ElemType = typename Vec::value_type; + const uint64_t count = value.size(); + if (count == 0) return; + + if constexpr (zmem::is_fixed_type_v) { + if constexpr (std::is_aggregate_v && !zmem::is_std_array_v) { + constexpr size_t stride = zmem::vector_fixed_stride(); + if constexpr (stride == sizeof(ElemType)) { + write_bytes(value.data(), sizeof(ElemType) * count, b, ix); + } + else { + for (const auto& elem : value) { + to::template op(elem, ctx, b, ix); + } + } + } + else { + write_bytes(value.data(), sizeof(ElemType) * count, b, ix); + } + } + else { + const size_t offset_table_start = ix; + const size_t offset_table_size = (count + 1) * sizeof(uint64_t); + + if constexpr (!unchecked && resizable>) { + if (ix + offset_table_size > b.size()) { + b.resize((std::max)(b.size() * 2, ix + offset_table_size)); + } + } + std::memset(b.data() + ix, 0, offset_table_size); + ix += offset_table_size; + + const size_t data_section_start = ix; + + uint64_t stack_offsets[zmem::offset_table_stack_threshold + 1]; + std::vector heap_offsets; + uint64_t* offsets; + + if (count <= zmem::offset_table_stack_threshold) { + offsets = stack_offsets; + } + else { + heap_offsets.resize(count + 1); + offsets = heap_offsets.data(); + } + + for (size_t i = 0; i < count; ++i) { + offsets[i] = ix - data_section_start; + if constexpr (zmem::is_std_string_v) { + using CharT = typename ElemType::value_type; + const uint64_t length = value[i].size() * sizeof(CharT); + if (length > 0) { + write_bytes(value[i].data(), length, b, ix); + } + } + else { + to::template op(value[i], ctx, b, ix); + } + } + offsets[count] = ix - data_section_start; + + if constexpr (std::endian::native == std::endian::big) { + for (size_t i = 0; i <= count; ++i) { + offsets[i] = std::byteswap(offsets[i]); + } + } + std::memcpy(b.data() + offset_table_start, offsets, offset_table_size); + } + } + + template + GLZ_ALWAYS_INLINE auto make_sorted_entries(const Map& value) + { + using K = typename Map::key_type; + using V = typename Map::mapped_type; + + std::vector> entries; + entries.reserve(value.size()); + for (const auto& [k, v] : value) { + entries.emplace_back(k, v); + } + + if constexpr (zmem::is_std_unordered_map_v) { + std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { + return a.first < b.first; + }); + } + + return entries; + } + + template + GLZ_ALWAYS_INLINE void write_map_payload_aligned(const Entries& entries, + size_t inline_base, + is_context auto&& ctx, + B& b, + size_t& ix) + { + constexpr bool unchecked = glz::is_write_unchecked(); + using Entry = std::remove_cvref_t; + using K = std::remove_cvref_t; + using V = std::remove_cvref_t; + + const uint64_t count = entries.size(); + if (count == 0) return; + + constexpr size_t entry_stride = zmem::map_entry_stride(); + constexpr size_t value_offset = zmem::map_value_offset_in_entry(); + + std::array stack_positions{}; + std::vector heap_positions; + size_t* offset_positions = nullptr; + + if constexpr (!zmem::is_fixed_type_v) { + if (count <= zmem::offset_table_stack_threshold) { + offset_positions = stack_positions.data(); + } + else { + heap_positions.resize(count); + offset_positions = heap_positions.data(); + } + } + + size_t entry_index = 0; + for (const auto& [k, v] : entries) { + const size_t entry_start = ix; + write_fixed_raw(k, b, ix); + + const size_t pad_to_value = value_offset - sizeof(K); + if (pad_to_value > 0) { + write_padding(pad_to_value, b, ix); + } + + if constexpr (zmem::is_fixed_type_v) { + write_fixed_raw(v, b, ix); + const size_t tail_pad = entry_stride - (value_offset + sizeof(V)); + if (tail_pad > 0) { + write_padding(tail_pad, b, ix); + } + } + else if constexpr (zmem::is_std_vector_v) { + write_value(0, b, ix); // offset placeholder + write_value(static_cast(v.size()), b, ix); + offset_positions[entry_index] = entry_start + value_offset; + const size_t tail_pad = entry_stride - (value_offset + 16); + if (tail_pad > 0) { + write_padding(tail_pad, b, ix); + } + } + else if constexpr (zmem::is_std_string_v) { + using CharT = typename V::value_type; + const uint64_t length = v.size() * sizeof(CharT); + write_value(0, b, ix); // offset placeholder + write_value(length, b, ix); + offset_positions[entry_index] = entry_start + value_offset; + const size_t tail_pad = entry_stride - (value_offset + 16); + if (tail_pad > 0) { + write_padding(tail_pad, b, ix); + } + } + else { + write_value(0, b, ix); // offset placeholder + offset_positions[entry_index] = entry_start + value_offset; + const size_t tail_pad = entry_stride - (value_offset + 8); + if (tail_pad > 0) { + write_padding(tail_pad, b, ix); + } + } + + ++entry_index; + } + + if constexpr (!zmem::is_fixed_type_v) { + size_t value_index = 0; + for (const auto& [k, v] : entries) { + size_t value_alignment = size_t{8}; + if constexpr (zmem::is_std_vector_v) { + using ElemType = typename V::value_type; + value_alignment = zmem::vector_data_alignment(); + } + + const size_t padding = zmem::padding_for_alignment(ix - inline_base, value_alignment); + if (padding > 0) { + write_padding(padding, b, ix); + } + + const uint64_t offset = ix - inline_base; + const uint64_t offset_le = std::endian::native == std::endian::little + ? offset + : zmem::to_little_endian(offset); + std::memcpy(b.data() + offset_positions[value_index], &offset_le, sizeof(uint64_t)); + + if constexpr (zmem::is_std_vector_v) { + write_vector_data(v, ctx, b, ix); + } + else if constexpr (zmem::is_std_string_v) { + using CharT = typename V::value_type; + const uint64_t length = v.size() * sizeof(CharT); + if (length > 0) { + write_bytes(v.data(), length, b, ix); + } + } + else { + to::template op(v, ctx, b, ix); + } + + ++value_index; + } + } + } + + } // namespace zmem + + // ============================================================================ + // Write Specializations + // ============================================================================ + + // Primitives (bool, integers, floats, enums) + template + requires(std::is_arithmetic_v || std::is_enum_v) + struct to final + { + template + GLZ_ALWAYS_INLINE static void op(const T& value, is_context auto&&, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + zmem::write_value(value, b, ix); + } + }; + + // Fixed-size C arrays (including char arrays for fixed strings) + template + struct to final + { + template + GLZ_ALWAYS_INLINE static void op(const T (&value)[N], is_context auto&& ctx, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + if constexpr (zmem::is_fixed_type_v) { + // For fixed element types, write contiguously + zmem::write_bytes(value, sizeof(T) * N, b, ix); + } + else { + // For variable element types, serialize each element + for (std::size_t i = 0; i < N; ++i) { + to::template op(value[i], ctx, b, ix); + } + } + } + }; + + // std::array + template + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::array& value, is_context auto&& ctx, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + if constexpr (zmem::is_fixed_type_v) { + zmem::write_bytes(value.data(), sizeof(T) * N, b, ix); + } + else { + for (std::size_t i = 0; i < N; ++i) { + to::template op(value[i], ctx, b, ix); + } + } + } + }; + + // ZMEM optional + template + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const zmem::optional& value, [[maybe_unused]] is_context auto&& ctx, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + // Write the entire optional struct directly (it has guaranteed layout) + zmem::write_bytes(&value, sizeof(zmem::optional), b, ix); + } + }; + + // std::optional -> zmem::optional conversion on write + template + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::optional& value, is_context auto&& ctx, B&& b, auto& ix) + { + zmem::optional zmem_opt; + if (value.has_value()) { + zmem_opt = *value; + } + to>::template op(zmem_opt, ctx, b, ix); + } + }; + + // std::vector - fixed element types (contiguous) + template + requires zmem::is_fixed_type_v + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::vector& value, is_context auto&& ctx, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + // For top-level vector serialization (Array message format): + // Write count (8 bytes) + elements + const uint64_t count = value.size(); + zmem::write_value(count, b, ix); + + if (count > 0) { + if constexpr (std::is_aggregate_v && !zmem::is_std_array_v) { + constexpr size_t stride = zmem::vector_fixed_stride(); + if constexpr (stride == sizeof(T)) { + zmem::write_bytes(value.data(), sizeof(T) * count, b, ix); + } + else { + for (const auto& elem : value) { + to::template op(elem, ctx, b, ix); + } + } + } + else { + zmem::write_bytes(value.data(), sizeof(T) * count, b, ix); + } + } + } + }; + + // std::vector - variable element types (offset table + self-contained elements) + template + requires(!zmem::is_fixed_type_v) + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::vector& value, is_context auto&& ctx, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + const uint64_t count = value.size(); + zmem::write_value(count, b, ix); + + if (count == 0) { + return; + } + + zmem::write_vector_data(value, ctx, b, ix); + } + }; + + // std::string (variable-length) + template + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::basic_string& value, + is_context auto&&, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + // For top-level string serialization: + // Write length (8 bytes) + raw bytes (NOT null-terminated) + const uint64_t length = value.size() * sizeof(CharT); + zmem::write_value(length, b, ix); + + if (length > 0) { + zmem::write_bytes(value.data(), length, b, ix); + } + } + }; + + // std::string_view + template + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::basic_string_view& value, + is_context auto&&, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + const uint64_t length = value.size() * sizeof(CharT); + zmem::write_value(length, b, ix); + + if (length > 0) { + zmem::write_bytes(value.data(), length, b, ix); + } + } + }; + + // std::span (for read-only views) + template + requires zmem::is_fixed_type_v + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::span& value, is_context auto&&, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + if constexpr (Extent == std::dynamic_extent) { + // Dynamic span: write count + elements + const uint64_t count = value.size(); + zmem::write_value(count, b, ix); + } + // Fixed extent spans write elements directly (count known at compile time) + + if (!value.empty()) { + zmem::write_bytes(value.data(), sizeof(T) * value.size(), b, ix); + } + } + }; + + // std::pair (for map entries) + template + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::pair& value, is_context auto&& ctx, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + // Align key + const size_t key_padding = zmem::padding_for_alignment(ix, alignof(K)); + zmem::write_padding(key_padding, b, ix); + to::template op(value.first, ctx, b, ix); + + // Align value + const size_t val_padding = zmem::padding_for_alignment(ix, alignof(V)); + zmem::write_padding(val_padding, b, ix); + to::template op(value.second, ctx, b, ix); + } + }; + + // std::map - ZMEM requires sorted keys (which std::map provides) + template + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::map& value, + is_context auto&& ctx, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + const uint64_t count = value.size(); + const size_t map_start = ix; + zmem::write_value(count, b, ix); + + if (count == 0) { + return; + } + + const size_t map_alignment = zmem::map_data_alignment(); + const size_t padding = zmem::padding_for_alignment(ix - map_start, map_alignment); + if (padding > 0) { + zmem::write_padding(padding, b, ix); + } + + const auto entries = zmem::make_sorted_entries(value); + zmem::write_map_payload_aligned(entries, map_start, ctx, b, ix); + } + }; + + // std::unordered_map - keys are sorted before serialization + template + struct to> final + { + template + GLZ_ALWAYS_INLINE static void op(const std::unordered_map& value, + is_context auto&& ctx, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + const uint64_t count = value.size(); + const size_t map_start = ix; + zmem::write_value(count, b, ix); + + if (count == 0) { + return; + } + + const size_t map_alignment = zmem::map_data_alignment(); + const size_t padding = zmem::padding_for_alignment(ix - map_start, map_alignment); + if (padding > 0) { + zmem::write_padding(padding, b, ix); + } + + const auto entries = zmem::make_sorted_entries(value); + zmem::write_map_payload_aligned(entries, map_start, ctx, b, ix); + } + }; + + // ============================================================================ + // Fixed Struct Serialization (Trivially Copyable) + // ============================================================================ + + // For fixed aggregate types (trivially copyable structs) + template + requires(std::is_aggregate_v && zmem::is_fixed_type_v && !zmem::is_std_array_v) + struct to final + { + template + GLZ_ALWAYS_INLINE static void op(const T& value, is_context auto&&, B&& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + // Fixed struct: direct memcpy + zmem::write_bytes(&value, sizeof(T), b, ix); + // Pad to alignment (minimum 8 bytes) for safe zero-copy access + constexpr size_t alignment = alignof(T) > 8 ? alignof(T) : 8; + constexpr size_t wire_size = zmem::padded_size(sizeof(T), alignment); + constexpr size_t padding = wire_size - sizeof(T); + if constexpr (padding > 0) { + zmem::write_padding(padding, b, ix); + } + } + }; + + // ============================================================================ + // Variable Struct Serialization (Has Vectors/Strings) + // ============================================================================ + + // Variable structs need reflection to handle inline and variable sections. + // Serialization uses the compile-time inline layout (alignment + offsets) and + // writes variable payloads with inline-base-relative offsets. + + template + requires(!zmem::is_fixed_type_v && !zmem::is_std_array_v + && (glz::reflectable || glz::glaze_object_t)) + struct to final + { + static constexpr auto N = reflect::size; + using Layout = zmem::inline_layout; + + template + using member_type_at = field_t; + + template + GLZ_ALWAYS_INLINE static decltype(auto) access_member(auto&& value) + { + if constexpr (reflectable) { + return get_member(value, get(to_tie(value))); + } + else { + return get_member(value, get(reflect::values)); + } + } + + template + GLZ_ALWAYS_INLINE static void write_variable_member_data( + const T& value, + size_t ref_pos, + size_t inline_base, + is_context auto&& ctx, + B& b, + size_t& ix) + { + constexpr bool unchecked = is_write_unchecked(); + decltype(auto) member = access_member(value); + using MemberType = member_type_at; + + size_t alignment = size_t{8}; + if constexpr (zmem::is_std_vector_v) { + using ElemType = typename MemberType::value_type; + alignment = zmem::vector_data_alignment(); + } + else if constexpr (zmem::is_std_map_like_v) { + using K = typename MemberType::key_type; + using V = typename MemberType::mapped_type; + alignment = zmem::map_data_alignment(); + } + + const size_t padding = zmem::padding_for_alignment(ix - inline_base, alignment); + if (padding > 0) { + zmem::write_padding(padding, b, ix); + } + + const uint64_t offset = ix - inline_base; + + if constexpr (zmem::is_std_vector_v) { + const uint64_t count = member.size(); + if constexpr (std::endian::native == std::endian::little) { + uint64_t ref_data[2] = {offset, count}; + std::memcpy(b.data() + ref_pos, ref_data, sizeof(ref_data)); + } + else { + uint64_t ref_data[2] = {zmem::to_little_endian(offset), zmem::to_little_endian(count)}; + std::memcpy(b.data() + ref_pos, ref_data, sizeof(ref_data)); + } + + if (count > 0) { + zmem::write_vector_data(member, ctx, b, ix); + } + } + else if constexpr (zmem::is_std_string_v) { + using CharT = typename MemberType::value_type; + const uint64_t length = member.size() * sizeof(CharT); + if constexpr (std::endian::native == std::endian::little) { + uint64_t ref_data[2] = {offset, length}; + std::memcpy(b.data() + ref_pos, ref_data, sizeof(ref_data)); + } + else { + uint64_t ref_data[2] = {zmem::to_little_endian(offset), zmem::to_little_endian(length)}; + std::memcpy(b.data() + ref_pos, ref_data, sizeof(ref_data)); + } + + if (length > 0) { + zmem::write_bytes(member.data(), length, b, ix); + } + } + else if constexpr (zmem::is_std_map_like_v) { + const auto entries = zmem::make_sorted_entries(member); + const uint64_t count = entries.size(); + if constexpr (std::endian::native == std::endian::little) { + uint64_t ref_data[2] = {offset, count}; + std::memcpy(b.data() + ref_pos, ref_data, sizeof(ref_data)); + } + else { + uint64_t ref_data[2] = {zmem::to_little_endian(offset), zmem::to_little_endian(count)}; + std::memcpy(b.data() + ref_pos, ref_data, sizeof(ref_data)); + } + + if (count > 0) { + zmem::write_map_payload_aligned(entries, inline_base, ctx, b, ix); + } + } + else { + const uint64_t offset_le = std::endian::native == std::endian::little + ? offset + : zmem::to_little_endian(offset); + std::memcpy(b.data() + ref_pos, &offset_le, sizeof(uint64_t)); + to::template op(member, ctx, b, ix); + } + } + + template + GLZ_ALWAYS_INLINE static void op(const T& value, is_context auto&& ctx, B& b, auto& ix) + { + constexpr bool unchecked = is_write_unchecked(); + + constexpr size_t header_plus_inline = 8 + Layout::InlineBasePadding + Layout::InlineSectionSize; + if constexpr (!unchecked && resizable>) { + const size_t required = ix + header_plus_inline; + if (required > b.size()) { + b.resize((std::max)(b.size() * 2, required)); + } + } + + const size_t size_pos = ix; + std::memset(b.data() + ix, 0, 8); + ix += 8; + + if constexpr (Layout::InlineBasePadding > 0) { + zmem::write_padding(Layout::InlineBasePadding, b, ix); + } + + const size_t inline_base = ix; + std::array ref_positions{}; + + for_each([&]() { + using MemberType = member_type_at; + constexpr size_t field_alignment = Layout::template field_alignment(); + const size_t pad = zmem::padding_for_alignment(ix - inline_base, field_alignment); + if (pad > 0) { + zmem::write_padding(pad, b, ix); + } + + if constexpr (zmem::is_fixed_type_v) { + decltype(auto) member = access_member(value); + std::memcpy(b.data() + ix, &member, sizeof(MemberType)); + ix += sizeof(MemberType); + } + else if constexpr (zmem::is_std_vector_v || zmem::is_std_string_v + || zmem::is_std_map_like_v) { + ref_positions[I] = ix; + std::memset(b.data() + ix, 0, 16); + ix += 16; + } + else { + ref_positions[I] = ix; + std::memset(b.data() + ix, 0, 8); + ix += 8; + } + }); + + for_each([&]() { + using MemberType = member_type_at; + if constexpr (zmem::is_variable_member_type()) { + write_variable_member_data(value, ref_positions[I], inline_base, ctx, b, ix); + } + }); + + const size_t end_padding = zmem::padding_for_alignment(ix - size_pos - 8, 8); + if (end_padding > 0) { + zmem::write_padding(end_padding, b, ix); + } + + const uint64_t total_size = zmem::to_little_endian(static_cast(ix - size_pos - 8)); + std::memcpy(b.data() + size_pos, &total_size, sizeof(uint64_t)); + } + }; + + // ============================================================================ + // Helper Functions + // ============================================================================ + + namespace detail + { + template + consteval auto set_zmem() + { + auto ret = Opts; + ret.format = ZMEM; + return ret; + } + + template + consteval auto set_zmem_unchecked() + { + auto ret = Opts; + ret.format = ZMEM; + ret.internal |= static_cast(opts_internal::write_unchecked); + return ret; + } + } + + template + consteval auto set_zmem() + { + return detail::set_zmem(); + } + + template + consteval auto set_zmem_unchecked() + { + return detail::set_zmem_unchecked(); + } + + // ============================================================================ + // Public API Functions + // ============================================================================ + + // Write ZMEM to buffer + template + [[nodiscard]] GLZ_ALWAYS_INLINE error_ctx write_zmem(T&& value, Buffer&& buffer) + { + return write()>(std::forward(value), std::forward(buffer)); + } + + // Write ZMEM to new string + template + [[nodiscard]] GLZ_ALWAYS_INLINE expected write_zmem(T&& value) + { + return write()>(std::forward(value)); + } + + // Write ZMEM to file + template + [[nodiscard]] GLZ_ALWAYS_INLINE error_ctx write_file_zmem(T&& value, const std::string_view file_name, + auto&& buffer) + { + static_assert(sizeof(decltype(*buffer.data())) == 1); + + const auto ec = write()>(std::forward(value), buffer); + if (bool(ec)) [[unlikely]] { + return ec; + } + + std::ofstream file(file_name.data(), std::ios::binary); + if (file) { + file.write(reinterpret_cast(buffer.data()), buffer.size()); + } + else { + return {.ec = error_code::file_open_failure}; + } + + return {}; + } + + // ============================================================================ + // Pre-allocated Write API + // ============================================================================ + // + // These functions pre-compute the exact serialized size, allocate once, + // then write with all resize checks disabled for maximum performance. + // + + // Write ZMEM to pre-allocated buffer (no resize checks) + // Buffer must already be sized to at least size_zmem(value) bytes + template + [[nodiscard]] GLZ_ALWAYS_INLINE error_ctx write_zmem_unchecked(T&& value, Buffer&& buffer, size_t& bytes_written) + { + bytes_written = 0; + context ctx{}; + serialize::op()>(value, ctx, buffer, bytes_written); + return {.ec = ctx.error}; + } + + // Write ZMEM with automatic pre-allocation (compute size, allocate, write unchecked) + // This is the fastest way to serialize when you don't have a reusable buffer + template + [[nodiscard]] GLZ_ALWAYS_INLINE error_ctx write_zmem_preallocated(T&& value, Buffer&& buffer) + { + // Step 1: Compute exact serialized size + const size_t required_size = size_zmem(value); + + // Step 2: Pre-allocate buffer to exact size + buffer.resize(required_size); + + // Step 3: Write with all resize checks disabled + size_t bytes_written = 0; + context ctx{}; + serialize::op()>(value, ctx, buffer, bytes_written); + + // Trim buffer to actual bytes written (should match required_size) + if (bytes_written != required_size) { + buffer.resize(bytes_written); + } + + return {.ec = ctx.error}; + } + + // Write ZMEM to new string with pre-allocation (fastest for new allocations) + template + [[nodiscard]] GLZ_ALWAYS_INLINE expected write_zmem_preallocated(T&& value) + { + std::string buffer; + const auto ec = write_zmem_preallocated(std::forward(value), buffer); + if (bool(ec)) [[unlikely]] { + return unexpected(ec); + } + return buffer; + } + +} // namespace glz diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 62f6b0c579..a3646b6a6a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -144,6 +144,13 @@ add_subdirectory(toml_test) add_subdirectory(utility_formats) add_subdirectory(yaml_test) add_subdirectory(msgpack_test) + +# ZMEM format requires little-endian architecture (uses zero-copy memory access) +# Skip on big-endian platforms (e.g., s390x) +option(glaze_BUILD_ZMEM_TESTS "Build ZMEM format tests (requires little-endian)" ON) +if(glaze_BUILD_ZMEM_TESTS) + add_subdirectory(zmem_test) +endif() add_subdirectory(ostream_buffer_test) add_subdirectory(istream_buffer_test) diff --git a/tests/zmem_test/CMakeLists.txt b/tests/zmem_test/CMakeLists.txt new file mode 100644 index 0000000000..5264b6bc68 --- /dev/null +++ b/tests/zmem_test/CMakeLists.txt @@ -0,0 +1,9 @@ +project(zmem_test) + +add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp) + +target_link_libraries(${PROJECT_NAME} PRIVATE glz_test_common) + +add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) + +target_code_coverage(${PROJECT_NAME} AUTO ALL) diff --git a/tests/zmem_test/zmem_test.cpp b/tests/zmem_test/zmem_test.cpp new file mode 100644 index 0000000000..50cdff11c3 --- /dev/null +++ b/tests/zmem_test/zmem_test.cpp @@ -0,0 +1,1411 @@ +// ZMEM Format Test Suite +// Tests the ZMEM binary serialization format implementation for Glaze + +#include +#include +#include +#include + +#include "glaze/zmem.hpp" +#include + +using namespace ut; + +// ============================================================================ +// Test Structs +// ============================================================================ + +// Fixed struct - trivially copyable (zero overhead serialization) +struct Point { + float x; + float y; +}; +static_assert(sizeof(Point) == 8); + +struct Vec3 { + float x; + float y; + float z; +}; +static_assert(sizeof(Vec3) == 12); + +struct Color { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +}; +static_assert(sizeof(Color) == 4); + +// Fixed struct with padding +struct Mixed { + uint8_t a; + // padding + uint32_t b; + uint8_t c; + // padding + uint16_t d; +}; +static_assert(sizeof(Mixed) == 12); + +// Fixed struct with fixed array +struct Matrix2x2 { + float data[4]; +}; +static_assert(sizeof(Matrix2x2) == 16); + +// Variable struct - has vector field +struct Entity { + uint64_t id; + std::vector weights; +}; + +// Variable struct with string +struct LogEntry { + uint64_t timestamp; + std::string message; +}; + +// Nested fixed struct +struct Transform { + Vec3 position; + Vec3 rotation; + Vec3 scale; +}; +static_assert(sizeof(Transform) == 36); + +// Variable struct for map value testing +struct MapVariableValue { + std::string name; + std::vector values; +}; + +// Additional structs for lazy_zmem tests +struct ByteData { + uint64_t id; + std::vector bytes; +}; + +struct BigData { + uint32_t tag; + std::vector values; +}; + +struct DoubleData { + uint64_t id; + std::vector values; +}; + +struct MultiString { + std::string first; + std::string second; + std::string third; +}; + +struct MultiVector { + std::vector a; + std::vector b; + std::vector c; +}; + +struct MixedData { + std::string prefix; + std::vector values; + std::string suffix; +}; + +struct FixedBetween { + std::string name; + uint64_t count; + std::vector data; +}; + +struct Inner { + std::string value; +}; + +struct Middle { + uint32_t id; + Inner inner; +}; + +struct Outer { + std::string name; + Middle middle; + std::vector data; +}; + +struct AllVariable { + std::string a; + std::string b; + std::vector c; +}; + +struct BinaryContainer { + uint64_t id; + std::vector binary; +}; + +// ============================================================================ +// Test Suites +// ============================================================================ + +suite primitives_tests = [] { + "integer_roundtrip"_test = [] { + std::string buffer; + uint64_t value = 0x123456789ABCDEF0ULL; + auto err = glz::write_zmem(value, buffer); + expect(!err); + expect(buffer.size() == sizeof(uint64_t)); + + uint64_t result = 0; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == value); + }; + + "float_roundtrip"_test = [] { + std::string buffer; + float value = 3.14159f; + auto err = glz::write_zmem(value, buffer); + expect(!err); + expect(buffer.size() == sizeof(float)); + + float result = 0.0f; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == value); + }; + + "double_roundtrip"_test = [] { + std::string buffer; + double value = 2.718281828459045; + auto err = glz::write_zmem(value, buffer); + expect(!err); + expect(buffer.size() == sizeof(double)); + + double result = 0.0; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == value); + }; + + "bool_true_roundtrip"_test = [] { + std::string buffer; + bool value = true; + auto err = glz::write_zmem(value, buffer); + expect(!err); + + bool result = false; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == value); + }; + + "bool_false_roundtrip"_test = [] { + std::string buffer; + bool value = false; + auto err = glz::write_zmem(value, buffer); + expect(!err); + + bool result = true; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == value); + }; + + "signed_integers"_test = [] { + std::string buffer; + int32_t value = -42; + auto err = glz::write_zmem(value, buffer); + expect(!err); + + int32_t result = 0; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == value); + }; +}; + +suite fixed_struct_tests = [] { + "point_zero_overhead"_test = [] { + std::string buffer; + Point p{1.0f, 2.0f}; + auto err = glz::write_zmem(p, buffer); + expect(!err); + expect(buffer.size() == sizeof(Point)) << "Fixed struct should have zero overhead"; + + Point result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.x == p.x); + expect(result.y == p.y); + }; + + "vec3_roundtrip"_test = [] { + std::string buffer; + Vec3 v{1.0f, 2.0f, 3.0f}; + auto err = glz::write_zmem(v, buffer); + expect(!err); + expect(buffer.size() == glz::zmem::padded_size_8(sizeof(Vec3))); // 12 -> 16 bytes + + Vec3 result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.x == v.x); + expect(result.y == v.y); + expect(result.z == v.z); + }; + + "mixed_with_padding"_test = [] { + std::string buffer; + Mixed m{1, 2, 3, 4}; + auto err = glz::write_zmem(m, buffer); + expect(!err); + expect(buffer.size() == glz::zmem::padded_size_8(sizeof(Mixed))); // 12 -> 16 bytes + + Mixed result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.a == m.a); + expect(result.b == m.b); + expect(result.c == m.c); + expect(result.d == m.d); + }; + + "nested_fixed_struct"_test = [] { + std::string buffer; + Transform t{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + auto err = glz::write_zmem(t, buffer); + expect(!err); + expect(buffer.size() == glz::zmem::padded_size_8(sizeof(Transform))); // 36 -> 40 bytes + + Transform result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.position.x == t.position.x); + expect(result.rotation.y == t.rotation.y); + expect(result.scale.z == t.scale.z); + }; + + "color_struct"_test = [] { + std::string buffer; + Color c{255, 128, 64, 32}; + auto err = glz::write_zmem(c, buffer); + expect(!err); + expect(buffer.size() == glz::zmem::padded_size_8(sizeof(Color))); // 4 -> 8 bytes + + Color result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.r == c.r); + expect(result.g == c.g); + expect(result.b == c.b); + expect(result.a == c.a); + }; +}; + +suite fixed_array_tests = [] { + "c_array_float"_test = [] { + std::string buffer; + float arr[4] = {1.0f, 2.0f, 3.0f, 4.0f}; + auto err = glz::write_zmem(arr, buffer); + expect(!err); + expect(buffer.size() == sizeof(arr)); + + float result[4] = {}; + err = glz::read_zmem(result, buffer); + expect(!err); + for (int i = 0; i < 4; ++i) { + expect(result[i] == arr[i]); + } + }; + + "std_array_int"_test = [] { + std::string buffer; + std::array arr = {10, 20, 30}; + auto err = glz::write_zmem(arr, buffer); + expect(!err); + expect(buffer.size() == sizeof(arr)); + + std::array result = {}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == arr); + }; + + "struct_with_array"_test = [] { + std::string buffer; + Matrix2x2 m{{1, 2, 3, 4}}; + auto err = glz::write_zmem(m, buffer); + expect(!err); + expect(buffer.size() == sizeof(Matrix2x2)); + + Matrix2x2 result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + for (int i = 0; i < 4; ++i) { + expect(result.data[i] == m.data[i]); + } + }; +}; + +suite vector_tests = [] { + "vector_of_floats"_test = [] { + std::string buffer; + std::vector v = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + auto err = glz::write_zmem(v, buffer); + expect(!err); + expect(buffer.size() == 8 + v.size() * sizeof(float)) << "count + data"; + + std::vector result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == v); + }; + + "empty_vector"_test = [] { + std::string buffer; + std::vector v; + auto err = glz::write_zmem(v, buffer); + expect(!err); + expect(buffer.size() == 8) << "Just count (0)"; + + std::vector result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.empty()); + }; + + "vector_of_fixed_structs"_test = [] { + std::string buffer; + std::vector v = {{1, 2}, {3, 4}, {5, 6}}; + auto err = glz::write_zmem(v, buffer); + expect(!err); + + std::vector result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.size() == v.size()); + for (size_t i = 0; i < v.size(); ++i) { + expect(result[i].x == v[i].x); + expect(result[i].y == v[i].y); + } + }; + + "large_vector"_test = [] { + std::string buffer; + std::vector v(10000); + for (size_t i = 0; i < v.size(); ++i) { + v[i] = i * 42; + } + auto err = glz::write_zmem(v, buffer); + expect(!err); + + std::vector result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == v); + }; +}; + +suite string_tests = [] { + "string_roundtrip"_test = [] { + std::string buffer; + std::string s = "Hello, ZMEM!"; + auto err = glz::write_zmem(s, buffer); + expect(!err); + expect(buffer.size() == 8 + s.size()) << "length + data"; + + std::string result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == s); + }; + + "empty_string"_test = [] { + std::string buffer; + std::string s; + auto err = glz::write_zmem(s, buffer); + expect(!err); + expect(buffer.size() == 8) << "Just length (0)"; + + std::string result = "not empty"; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.empty()); + }; + + "unicode_string"_test = [] { + std::string buffer; + std::string s = "Hello, \xe4\xb8\x96\xe7\x95\x8c! \xf0\x9f\x8c\x8d"; // UTF-8 encoded: "Hello, δΈ–η•Œ! 🌍" + auto err = glz::write_zmem(s, buffer); + expect(!err); + + std::string result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == s); + }; +}; + +suite optional_tests = [] { + "zmem_optional_present"_test = [] { + std::string buffer; + glz::zmem::optional opt(42); + auto err = glz::write_zmem(opt, buffer); + expect(!err); + expect(buffer.size() == sizeof(glz::zmem::optional)); + + glz::zmem::optional result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.has_value()); + expect(*result == 42u); + }; + + "zmem_optional_absent"_test = [] { + std::string buffer; + glz::zmem::optional opt; + auto err = glz::write_zmem(opt, buffer); + expect(!err); + + glz::zmem::optional result(123); + err = glz::read_zmem(result, buffer); + expect(!err); + expect(!result.has_value()); + }; + + "std_optional_present"_test = [] { + std::string buffer; + std::optional opt = 3.14f; + auto err = glz::write_zmem(opt, buffer); + expect(!err); + + std::optional result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.has_value()); + expect(*result == 3.14f); + }; + + "std_optional_absent"_test = [] { + std::string buffer; + std::optional opt; + auto err = glz::write_zmem(opt, buffer); + expect(!err); + + std::optional result = 999.0; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(!result.has_value()); + }; +}; + +suite map_tests = [] { + "map_fixed_values"_test = [] { + std::string buffer; + std::map m = {{1, 1.0f}, {2, 2.0f}, {3, 3.0f}}; + auto err = glz::write_zmem(m, buffer); + expect(!err); + + std::map result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == m); + }; + + "empty_map"_test = [] { + std::string buffer; + std::map m; + auto err = glz::write_zmem(m, buffer); + expect(!err); + + std::map result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.empty()); + }; + + "map_maintains_order"_test = [] { + std::string buffer; + std::map m = {{3, 30}, {1, 10}, {2, 20}}; + auto err = glz::write_zmem(m, buffer); + expect(!err); + + std::map result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == m); + + // Verify order + auto it = result.begin(); + expect(it->first == 1); + ++it; + expect(it->first == 2); + ++it; + expect(it->first == 3); + }; + + "map_variable_values_string"_test = [] { + // Map with string values (variable type) + std::string buffer; + std::map m = { + {1, "hello"}, + {2, "world"}, + {3, "test"} + }; + auto err = glz::write_zmem(m, buffer); + expect(!err); + + std::map result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == m); + expect(result[1] == "hello"); + expect(result[2] == "world"); + expect(result[3] == "test"); + }; + + "map_variable_values_vector"_test = [] { + // Map with vector values (variable type) + std::string buffer; + std::map> m = { + {1, {10, 20, 30}}, + {2, {40, 50}}, + {3, {60, 70, 80, 90}} + }; + auto err = glz::write_zmem(m, buffer); + expect(!err); + + std::map> result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == m); + expect(result[1] == std::vector{10, 20, 30}); + expect(result[2] == std::vector{40, 50}); + expect(result[3] == std::vector{60, 70, 80, 90}); + }; + + "map_variable_empty"_test = [] { + // Empty map with variable value type + std::string buffer; + std::map m; + auto err = glz::write_zmem(m, buffer); + expect(!err); + + std::map result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.empty()); + }; + + "map_variable_single_entry"_test = [] { + // Single entry map with variable value + std::string buffer; + std::map m = {{42, "answer to everything"}}; + auto err = glz::write_zmem(m, buffer); + expect(!err); + + std::map result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result == m); + expect(result[42] == "answer to everything"); + }; + + "map_variable_nested_struct"_test = [] { + // Map with struct values containing vectors + std::string buffer; + std::map m = { + {1, {"first", {1, 2, 3}}}, + {2, {"second", {4, 5}}}, + {3, {"third", {}}} + }; + auto err = glz::write_zmem(m, buffer); + expect(!err); + + std::map result; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.size() == 3u); + expect(result[1].name == "first"); + expect(result[1].values == std::vector{1, 2, 3}); + expect(result[2].name == "second"); + expect(result[2].values == std::vector{4, 5}); + expect(result[3].name == "third"); + expect(result[3].values.empty()); + }; +}; + +suite layout_tests = [] { + "optional_layout_u8"_test = [] { + expect(sizeof(glz::zmem::optional) == 2u); + }; + + "optional_layout_u16"_test = [] { + expect(sizeof(glz::zmem::optional) == 4u); + }; + + "optional_layout_u32"_test = [] { + expect(sizeof(glz::zmem::optional) == 8u); + }; + + "optional_layout_u64"_test = [] { + expect(sizeof(glz::zmem::optional) == 16u); + }; + + "optional_alignment_u32"_test = [] { + expect(alignof(glz::zmem::optional) == 4u); + }; + + "optional_alignment_u64"_test = [] { + expect(alignof(glz::zmem::optional) == 8u); + }; + + "vector_ref_size"_test = [] { + expect(sizeof(glz::zmem::vector_ref) == 16u); + }; + + "string_ref_size"_test = [] { + expect(sizeof(glz::zmem::string_ref) == 16u); + }; +}; + +suite wire_format_tests = [] { + "fixed_struct_exact_bytes"_test = [] { + Point p{1.0f, 2.0f}; + std::string buffer; + auto err = glz::write_zmem(p, buffer); + expect(!err); + + // Should be identical to memcpy + expect(buffer.size() == sizeof(Point)); + Point direct; + std::memcpy(&direct, buffer.data(), sizeof(Point)); + expect(direct.x == p.x); + expect(direct.y == p.y); + }; + + "vector_header_format"_test = [] { + std::vector v = {1, 2, 3}; + std::string buffer; + auto err = glz::write_zmem(v, buffer); + expect(!err); + + // First 8 bytes should be count + uint64_t count; + std::memcpy(&count, buffer.data(), 8); + glz::zmem::byteswap_le(count); + expect(count == 3u); + + // Total size: 8 (count) + 3 * 4 (elements) + expect(buffer.size() == 8 + 3 * sizeof(uint32_t)); + }; + + "little_endian_encoding"_test = [] { + uint32_t value = 0x12345678; + std::string buffer; + auto err = glz::write_zmem(value, buffer); + expect(!err); + + // Little-endian: least significant byte first + expect(static_cast(buffer[0]) == 0x78u); + expect(static_cast(buffer[1]) == 0x56u); + expect(static_cast(buffer[2]) == 0x34u); + expect(static_cast(buffer[3]) == 0x12u); + }; +}; + +// ============================================================================ +// glaze_object_t Tests (types with glz::meta metadata) +// ============================================================================ + +// Fixed struct with explicit glaze metadata (not reflectable) +struct MetaPoint { + float x_coord; + float y_coord; +}; + +template <> +struct glz::meta { + using T = MetaPoint; + static constexpr auto value = object( + "x", &T::x_coord, + "y", &T::y_coord + ); +}; + +// Variable struct with glaze metadata +struct MetaEntity { + uint64_t entity_id; + std::string entity_name; + std::vector entity_tags; +}; + +template <> +struct glz::meta { + using T = MetaEntity; + static constexpr auto value = object( + "id", &T::entity_id, + "name", &T::entity_name, + "tags", &T::entity_tags + ); +}; + +// Nested glaze_object_t types +struct MetaTransform { + MetaPoint position; + float rotation; + float scale; +}; + +template <> +struct glz::meta { + using T = MetaTransform; + static constexpr auto value = object( + "pos", &T::position, + "rot", &T::rotation, + "scale", &T::scale + ); +}; + +suite glaze_object_tests = [] { + "meta_fixed_struct_roundtrip"_test = [] { + std::string buffer; + MetaPoint p{3.14f, 2.71f}; + auto err = glz::write_zmem(p, buffer); + expect(!err); + // Fixed glaze_object_t should have zero overhead (same as reflectable) + expect(buffer.size() == sizeof(MetaPoint)) << "Fixed meta struct should have zero overhead"; + + MetaPoint result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.x_coord == p.x_coord); + expect(result.y_coord == p.y_coord); + }; + + "meta_variable_struct_roundtrip"_test = [] { + std::string buffer; + MetaEntity entity{42, "TestEntity", {1, 2, 3, 4, 5}}; + auto err = glz::write_zmem(entity, buffer); + expect(!err); + + MetaEntity result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.entity_id == entity.entity_id); + expect(result.entity_name == entity.entity_name); + expect(result.entity_tags == entity.entity_tags); + }; + + "meta_nested_struct_roundtrip"_test = [] { + std::string buffer; + MetaTransform xform{{1.0f, 2.0f}, 45.0f, 1.5f}; + auto err = glz::write_zmem(xform, buffer); + expect(!err); + + MetaTransform result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.position.x_coord == xform.position.x_coord); + expect(result.position.y_coord == xform.position.y_coord); + expect(result.rotation == xform.rotation); + expect(result.scale == xform.scale); + }; + + "meta_size_computation"_test = [] { + MetaEntity entity{100, "HelloWorld", {10, 20, 30}}; + + // Compute expected size and verify size_zmem + const size_t computed_size = glz::size_zmem(entity); + + std::string buffer; + auto err = glz::write_zmem(entity, buffer); + expect(!err); + expect(buffer.size() == computed_size) << "size_zmem should match actual serialized size"; + }; + + "meta_preallocated_write"_test = [] { + MetaEntity entity{999, "PreallocTest", {100, 200, 300, 400}}; + + std::string buffer; + auto err = glz::write_zmem_preallocated(entity, buffer); + expect(!err); + + MetaEntity result{}; + err = glz::read_zmem(result, buffer); + expect(!err); + expect(result.entity_id == entity.entity_id); + expect(result.entity_name == entity.entity_name); + expect(result.entity_tags == entity.entity_tags); + }; +}; + +// ============================================================================ +// lazy_zmem Tests (zero-copy views) +// ============================================================================ + +// Variable struct for lazy view testing +struct Person { + uint64_t id; + std::string name; + std::vector scores; +}; + +// Nested variable struct +struct Team { + std::string team_name; + std::vector ratings; +}; + +struct Organization { + uint64_t org_id; + Team team; + std::string description; +}; + +suite lazy_zmem_tests = [] { + "lazy_fixed_struct_as_fixed"_test = [] { + Point p{3.14f, 2.71f}; + std::string buffer; + auto err = glz::write_zmem(p, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.valid()); + expect(view.size() == buffer.size()); + + const Point& ref = view.as_fixed(); + expect(ref.x == p.x); + expect(ref.y == p.y); + }; + + "lazy_fixed_struct_get_fields"_test = [] { + Point p{1.5f, 2.5f}; + std::string buffer; + auto err = glz::write_zmem(p, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.get<0>() == p.x); + expect(view.get<1>() == p.y); + }; + + "lazy_fixed_nested_struct"_test = [] { + Transform t{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + std::string buffer; + auto err = glz::write_zmem(t, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + const Transform& ref = view.as_fixed(); + expect(ref.position.x == t.position.x); + expect(ref.rotation.y == t.rotation.y); + expect(ref.scale.z == t.scale.z); + }; + + "lazy_variable_struct_string"_test = [] { + LogEntry entry{12345, "Hello, lazy world!"}; + std::string buffer; + auto err = glz::write_zmem(entry, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.valid()); + + // Get timestamp (fixed field) + uint64_t ts = view.get<0>(); + expect(ts == entry.timestamp); + + // Get message as string_view (zero-copy) + std::string_view msg = view.get<1>(); + expect(msg == entry.message); + }; + + "lazy_variable_struct_vector"_test = [] { + Entity entity{42, {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}}; + std::string buffer; + auto err = glz::write_zmem(entity, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.valid()); + + // Get id (fixed field) + uint64_t id = view.get<0>(); + expect(id == entity.id); + + // Get weights as span (zero-copy) + std::span weights = view.get<1>(); + expect(weights.size() == entity.weights.size()); + for (size_t i = 0; i < weights.size(); ++i) { + expect(weights[i] == entity.weights[i]); + } + }; + + "lazy_variable_struct_multiple_fields"_test = [] { + Person person{100, "Alice", {95, 87, 92, 88}}; + std::string buffer; + auto err = glz::write_zmem(person, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + + uint64_t id = view.get<0>(); + expect(id == person.id); + + std::string_view name = view.get<1>(); + expect(name == person.name); + + std::span scores = view.get<2>(); + expect(scores.size() == person.scores.size()); + expect(scores[0] == 95); + expect(scores[3] == 88); + }; + + "lazy_empty_string"_test = [] { + LogEntry entry{999, ""}; + std::string buffer; + auto err = glz::write_zmem(entry, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::string_view msg = view.get<1>(); + expect(msg.empty()); + }; + + "lazy_empty_vector"_test = [] { + Entity entity{123, {}}; + std::string buffer; + auto err = glz::write_zmem(entity, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::span weights = view.get<1>(); + expect(weights.empty()); + }; + + "lazy_nested_variable_struct"_test = [] { + Organization org{ + 42, + {"Engineering", {4.5f, 4.8f, 4.2f}}, + "Software development team" + }; + std::string buffer; + auto err = glz::write_zmem(org, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + + uint64_t org_id = view.get<0>(); + expect(org_id == org.org_id); + + // Get nested Team as a lazy view + auto team_view = view.get<1>(); + std::string_view team_name = team_view.get<0>(); + expect(team_name == org.team.team_name); + + std::span ratings = team_view.get<1>(); + expect(ratings.size() == org.team.ratings.size()); + expect(ratings[0] == 4.5f); + + std::string_view desc = view.get<2>(); + expect(desc == org.description); + }; + + "lazy_from_raw_pointer"_test = [] { + Point p{10.0f, 20.0f}; + std::string buffer; + auto err = glz::write_zmem(p, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer.data(), buffer.size()); + expect(view.valid()); + expect(view.as_fixed().x == p.x); + }; + + "lazy_from_void_pointer"_test = [] { + Point p{30.0f, 40.0f}; + std::string buffer; + auto err = glz::write_zmem(p, buffer); + expect(!err); + + const void* data = buffer.data(); + auto view = glz::lazy_zmem(data, buffer.size()); + expect(view.valid()); + expect(view.as_fixed().y == p.y); + }; + + "lazy_meta_fixed_struct"_test = [] { + MetaPoint p{1.0f, 2.0f}; + std::string buffer; + auto err = glz::write_zmem(p, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + const MetaPoint& ref = view.as_fixed(); + expect(ref.x_coord == p.x_coord); + expect(ref.y_coord == p.y_coord); + }; + + "lazy_meta_variable_struct"_test = [] { + MetaEntity entity{777, "LazyMeta", {10, 20, 30}}; + std::string buffer; + auto err = glz::write_zmem(entity, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + + uint64_t id = view.get<0>(); + expect(id == entity.entity_id); + + std::string_view name = view.get<1>(); + expect(name == entity.entity_name); + + std::span tags = view.get<2>(); + expect(tags.size() == entity.entity_tags.size()); + expect(tags[0] == 10); + expect(tags[2] == 30); + }; + + "lazy_long_string"_test = [] { + std::string long_message(10000, 'X'); + LogEntry entry{1, long_message}; + std::string buffer; + auto err = glz::write_zmem(entry, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::string_view msg = view.get<1>(); + expect(msg.size() == 10000u); + expect(msg == long_message); + }; + + "lazy_large_vector"_test = [] { + Entity entity{1, {}}; + entity.weights.resize(10000); + for (size_t i = 0; i < entity.weights.size(); ++i) { + entity.weights[i] = static_cast(i); + } + std::string buffer; + auto err = glz::write_zmem(entity, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::span weights = view.get<1>(); + expect(weights.size() == 10000u); + expect(weights[0] == 0.0f); + expect(weights[9999] == 9999.0f); + }; + + "lazy_default_view_invalid"_test = [] { + glz::lazy_zmem_view view; + expect(!view.valid()); + expect(view.data() == nullptr); + expect(view.size() == 0u); + }; + + "lazy_unicode_string"_test = [] { + LogEntry entry{42, "Hello, \xe4\xb8\x96\xe7\x95\x8c! \xf0\x9f\x8c\x8d"}; // UTF-8: "Hello, δΈ–η•Œ! 🌍" + std::string buffer; + auto err = glz::write_zmem(entry, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::string_view msg = view.get<1>(); + expect(msg == entry.message); + expect(msg.size() == entry.message.size()); + }; + + "lazy_single_char_string"_test = [] { + LogEntry entry{1, "X"}; + std::string buffer; + auto err = glz::write_zmem(entry, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::string_view msg = view.get<1>(); + expect(msg == "X"); + expect(msg.size() == 1u); + }; + + "lazy_single_element_vector"_test = [] { + Entity entity{1, {42.0f}}; + std::string buffer; + auto err = glz::write_zmem(entity, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::span weights = view.get<1>(); + expect(weights.size() == 1u); + expect(weights[0] == 42.0f); + }; + + "lazy_vector_of_fixed_structs"_test = [] { + std::vector points = {{1.0f, 2.0f}, {3.0f, 4.0f}, {5.0f, 6.0f}}; + std::string buffer; + auto err = glz::write_zmem(points, buffer); + expect(!err); + + // For top-level vectors, we read count then access data + uint64_t count; + std::memcpy(&count, buffer.data(), sizeof(uint64_t)); + expect(count == 3u); + + // Data starts after count (8 bytes) + const Point* data = reinterpret_cast(buffer.data() + 8); + expect(data[0].x == 1.0f); + expect(data[1].y == 4.0f); + expect(data[2].x == 5.0f); + }; + + "lazy_consistency_with_read"_test = [] { + // Verify lazy view returns same values as regular read_zmem + Person person{12345, "Consistency Test", {10, 20, 30, 40, 50}}; + std::string buffer; + auto err = glz::write_zmem(person, buffer); + expect(!err); + + // Read via regular API + Person read_result{}; + err = glz::read_zmem(read_result, buffer); + expect(!err); + + // Read via lazy view + auto view = glz::lazy_zmem(buffer); + uint64_t lazy_id = view.get<0>(); + std::string_view lazy_name = view.get<1>(); + std::span lazy_scores = view.get<2>(); + + // Compare + expect(lazy_id == read_result.id); + expect(lazy_name == read_result.name); + expect(lazy_scores.size() == read_result.scores.size()); + for (size_t i = 0; i < lazy_scores.size(); ++i) { + expect(lazy_scores[i] == read_result.scores[i]); + } + }; + + "lazy_vector_different_types_u8"_test = [] { + ByteData data{1, {0x00, 0xFF, 0x42, 0xAB}}; + std::string buffer; + auto err = glz::write_zmem(data, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::span bytes = view.get<1>(); + expect(bytes.size() == 4u); + expect(bytes[0] == 0x00u); + expect(bytes[1] == 0xFFu); + expect(bytes[2] == 0x42u); + expect(bytes[3] == 0xABu); + }; + + "lazy_vector_different_types_u64"_test = [] { + BigData data{42, {0x123456789ABCDEF0ULL, 0xFEDCBA9876543210ULL}}; + std::string buffer; + auto err = glz::write_zmem(data, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::span values = view.get<1>(); + expect(values.size() == 2u); + expect(values[0] == 0x123456789ABCDEF0ULL); + expect(values[1] == 0xFEDCBA9876543210ULL); + }; + + "lazy_vector_doubles"_test = [] { + DoubleData data{99, {3.14159265358979, 2.71828182845904, 1.41421356237309}}; + std::string buffer; + auto err = glz::write_zmem(data, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::span values = view.get<1>(); + expect(values.size() == 3u); + expect(values[0] == 3.14159265358979); + expect(values[1] == 2.71828182845904); + expect(values[2] == 1.41421356237309); + }; + + "lazy_multiple_strings"_test = [] { + MultiString data{"Hello", "World", "!"}; + std::string buffer; + auto err = glz::write_zmem(data, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.get<0>() == "Hello"); + expect(view.get<1>() == "World"); + expect(view.get<2>() == "!"); + }; + + "lazy_multiple_vectors"_test = [] { + MultiVector data{{1, 2}, {3, 4, 5}, {6}}; + std::string buffer; + auto err = glz::write_zmem(data, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + + std::span a = view.get<0>(); + expect(a.size() == 2u); + expect(a[0] == 1); + expect(a[1] == 2); + + std::span b = view.get<1>(); + expect(b.size() == 3u); + expect(b[0] == 3); + expect(b[2] == 5); + + std::span c = view.get<2>(); + expect(c.size() == 1u); + expect(c[0] == 6); + }; + + "lazy_mixed_strings_and_vectors"_test = [] { + MixedData data{"START", {1.0f, 2.0f, 3.0f}, "END"}; + std::string buffer; + auto err = glz::write_zmem(data, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.get<0>() == "START"); + + std::span values = view.get<1>(); + expect(values.size() == 3u); + expect(values[1] == 2.0f); + + expect(view.get<2>() == "END"); + }; + + "lazy_fixed_between_variable"_test = [] { + FixedBetween fb{"test", 42, {1, 2, 3}}; + std::string buffer; + auto err = glz::write_zmem(fb, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.get<0>() == "test"); + expect(view.get<1>() == 42u); + + std::span data = view.get<2>(); + expect(data.size() == 3u); + }; + + "lazy_deeply_nested"_test = [] { + Outer outer{"outer", {123, {"inner_value"}}, {10, 20, 30}}; + std::string buffer; + auto err = glz::write_zmem(outer, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.get<0>() == "outer"); + + auto middle_view = view.get<1>(); + expect(middle_view.get<0>() == 123u); + + auto inner_view = middle_view.get<1>(); + expect(inner_view.get<0>() == "inner_value"); + + std::span data = view.get<2>(); + expect(data.size() == 3u); + expect(data[0] == 10); + }; + + "lazy_all_variable_fields"_test = [] { + AllVariable data{"first", "second", {1, 2, 3, 4, 5}}; + std::string buffer; + auto err = glz::write_zmem(data, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + expect(view.get<0>() == "first"); + expect(view.get<1>() == "second"); + + std::span c = view.get<2>(); + expect(c.size() == 5u); + expect(c[4] == 5u); + }; + + "lazy_binary_data"_test = [] { + // Test with binary data containing null bytes + std::vector binary_data = {0x00, 0x01, 0x00, 0xFF, 0x00, 0xFE}; + BinaryContainer data{1, binary_data}; + std::string buffer; + auto err = glz::write_zmem(data, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::span binary = view.get<1>(); + expect(binary.size() == 6u); + expect(binary[0] == 0x00u); + expect(binary[2] == 0x00u); + expect(binary[3] == 0xFFu); + expect(binary[5] == 0xFEu); + }; + + "lazy_span_iteration"_test = [] { + Entity entity{1, {10.0f, 20.0f, 30.0f, 40.0f, 50.0f}}; + std::string buffer; + auto err = glz::write_zmem(entity, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::span weights = view.get<1>(); + + // Test range-based for loop + float sum = 0.0f; + for (float w : weights) { + sum += w; + } + expect(sum == 150.0f); + + // Test iterator access + expect(*weights.begin() == 10.0f); + expect(*(weights.end() - 1) == 50.0f); + }; + + "lazy_string_view_operations"_test = [] { + LogEntry entry{1, "Hello, World!"}; + std::string buffer; + auto err = glz::write_zmem(entry, buffer); + expect(!err); + + auto view = glz::lazy_zmem(buffer); + std::string_view msg = view.get<1>(); + + // Test string_view operations + expect(msg.starts_with("Hello")); + expect(msg.ends_with("!")); + expect(msg.find("World") == 7u); + expect(msg.substr(0, 5) == "Hello"); + }; + + "lazy_reuse_buffer"_test = [] { + // Test that we can reuse buffer for multiple serializations + std::string buffer; + + Person p1{1, "Alice", {100}}; + auto err = glz::write_zmem(p1, buffer); + expect(!err); + + auto view1 = glz::lazy_zmem(buffer); + expect(view1.get<0>() == 1u); + expect(view1.get<1>() == "Alice"); + + // Reuse buffer + Person p2{2, "Bob", {200, 201}}; + err = glz::write_zmem(p2, buffer); + expect(!err); + + auto view2 = glz::lazy_zmem(buffer); + expect(view2.get<0>() == 2u); + expect(view2.get<1>() == "Bob"); + expect(view2.get<2>().size() == 2u); + }; +}; + +int main() {}