Crunch
A Message Definition Language for Getting Things Right
Loading...
Searching...
No Matches
crunch_static_layout.hpp
1#pragma once
2
3#include <algorithm>
4#include <concepts>
5#include <crunch/core/crunch_endian.hpp>
6#include <crunch/core/crunch_types.hpp>
7#include <crunch/fields/crunch_string.hpp>
8#include <crunch/messages/crunch_field.hpp>
9#include <crunch/messages/crunch_messages.hpp>
10#include <cstddef>
11#include <cstdint>
12#include <cstring>
13#include <expected>
14#include <optional>
15#include <span>
16#include <tuple>
17
18namespace Crunch::serdes {
19
23template <std::size_t Alignment = 1>
25 static_assert(Alignment == 1 || Alignment == 4 || Alignment == 8,
26 "StaticLayout only supports 1, 4, or 8 byte alignment.");
27
32 [[nodiscard]] static constexpr Crunch::Format GetFormat() noexcept {
33 if constexpr (Alignment == 1) {
35 } else if constexpr (Alignment == 4) {
37 } else {
39 }
40 }
41
47 template <typename Message>
48 [[nodiscard]] static constexpr std::size_t Size() noexcept {
49 return PayloadStartOffset + calculate_payload_size(Message{});
50 }
51
59 template <typename Message>
60 static constexpr std::size_t Serialize(
61 const Message& msg, std::span<std::byte> output) noexcept {
62 // Header (including MessageId) is written by top-level serializer
63 // Zero-fill any alignment padding between header and payload start
64 if (PayloadStartOffset > StandardHeaderSize) {
65 std::memset(output.data() + StandardHeaderSize, 0,
66 PayloadStartOffset - StandardHeaderSize);
67 }
68 std::size_t offset = PayloadStartOffset;
69
70 std::apply(
71 [&](const auto&... fields) {
72 ((offset = serialize_field(fields, output, offset)), ...);
73 },
74 msg.get_fields());
75 return offset;
76 }
77
85 template <typename Message>
86 [[nodiscard]] static constexpr auto Deserialize(
87 std::span<const std::byte> input, Message& msg) noexcept
88 -> std::optional<Error> {
89 // Header (including MessageId) validated by top-level deserializer
90 std::size_t offset = PayloadStartOffset;
91
92 std::optional<Error> err = std::nullopt;
93 std::apply(
94 [&](auto&... fields) {
95 ((err.has_value()
96 ? void()
97 : [&] {
98 auto result =
99 deserialize_field(fields, input, offset);
100 if (result.has_value()) {
101 offset = result.value();
102 } else {
103 err = result.error();
104 }
105 }()),
106 ...);
107 },
108 msg.get_fields());
109 return err;
110 }
111
112 private:
119 [[nodiscard]] static constexpr std::size_t align_up(
120 std::size_t value, std::size_t alignment) noexcept {
121 return (value + alignment - 1) & ~(alignment - 1);
122 }
123
131 template <typename T>
132 [[nodiscard]] static constexpr std::size_t calculate_padding(
133 std::size_t offset) noexcept {
134 const std::size_t align = std::min(sizeof(T), Alignment);
135 return (align - (offset % align)) % align;
136 }
137
138 static constexpr std::size_t PayloadStartOffset =
139 align_up(StandardHeaderSize, Alignment);
140
147 template <typename Message>
148 [[nodiscard]] static constexpr std::size_t calculate_payload_size(
149 const Message& msg) noexcept {
150 std::size_t offset = 0;
151 std::apply(
152 [&](const auto&... fields) {
153 ((offset = calculate_field_end_offset(fields, offset)), ...);
154 },
155 msg.get_fields());
156 return offset;
157 }
158
165 template <typename T>
166 [[nodiscard]] static constexpr std::size_t calculate_value_end_offset(
167 std::size_t offset) noexcept {
168 if constexpr (fields::is_string_v<T>) {
169 return calculate_string_end_offset<T>(offset);
170 } else if constexpr (messages::CrunchMessage<T>) {
171 return calculate_message_end_offset<T>(offset);
172 } else if constexpr (messages::is_array_field_v<T>) {
173 return calculate_array_end_offset<T>(offset);
174 } else if constexpr (messages::is_map_field_v<T>) {
175 return calculate_map_end_offset<T>(offset);
176 } else {
177 return calculate_scalar_end_offset<T>(offset);
178 }
179 }
180
187 template <typename Field>
188 [[nodiscard]] static constexpr std::size_t calculate_field_end_offset(
189 const Field&, std::size_t offset) noexcept {
190 using ValueType = typename Field::FieldType;
191 // ArrayField and MapField don't have is_set byte in wire format
192 if constexpr (!messages::is_array_field_v<Field> &&
193 !messages::is_map_field_v<Field>) {
194 offset += 1;
195 }
196 return calculate_value_end_offset<ValueType>(offset);
197 }
198
205 template <typename T>
206 [[nodiscard]] static constexpr std::size_t calculate_message_end_offset(
207 std::size_t offset) noexcept {
208 const std::size_t padding =
209 calculate_padding<std::byte[Alignment]>(offset);
210 offset += padding;
211 offset += sizeof(MessageId) + calculate_payload_size(T{});
212 return offset;
213 }
214
221 template <typename T>
222 [[nodiscard]] static constexpr std::size_t calculate_scalar_end_offset(
223 std::size_t offset) noexcept {
224 using ValT = typename T::ValueType;
225 offset += calculate_padding<ValT>(offset) + sizeof(ValT);
226 return offset;
227 }
228
235 template <typename T>
236 [[nodiscard]] static constexpr std::size_t calculate_string_end_offset(
237 std::size_t offset) noexcept {
238 offset += calculate_padding<uint32_t>(offset) + sizeof(uint32_t) +
239 T::max_size;
240 return offset;
241 }
242
249 template <typename T>
250 [[nodiscard]] static constexpr std::size_t calculate_array_end_offset(
251 std::size_t offset) noexcept {
252 using ValT = typename T::ValueType;
253 // Align for Length (uint32_t)
254 offset += calculate_padding<uint32_t>(offset) + sizeof(uint32_t);
255
256 // Calculate size of MaxSize elements
257 // Since element locations depend on alignment which depends on offset,
258 // we must iterate.
259 for (std::size_t i = 0; i < T::max_size; ++i) {
260 offset = calculate_value_end_offset<ValT>(offset);
261 }
262 return offset;
263 }
264
271 template <typename T>
272 [[nodiscard]] static constexpr std::size_t calculate_map_end_offset(
273 std::size_t offset) noexcept {
274 using KeyField = typename T::PairType::first_type;
275 using ValueField = typename T::PairType::second_type;
276
277 // Align for Length (uint32_t)
278 offset += calculate_padding<uint32_t>(offset) + sizeof(uint32_t);
279
280 // Calculate size of MaxSize elements (Key + Value pairs)
281 for (std::size_t i = 0; i < T::max_size; ++i) {
282 offset = calculate_value_end_offset<KeyField>(offset);
283 offset = calculate_value_end_offset<ValueField>(offset);
284 }
285 return offset;
286 }
287
296 template <typename T>
297 [[nodiscard]] static constexpr std::size_t serialize_value(
298 const T& value, std::span<std::byte> output,
299 std::size_t offset) noexcept {
300 if constexpr (fields::is_string_v<T>) {
301 return serialize_string(value, output, offset);
302 } else if constexpr (messages::CrunchMessage<T>) {
303 return serialize_message(value, output, offset);
304 } else if constexpr (messages::is_array_field_v<T>) {
305 return serialize_array(value, output, offset);
306 } else if constexpr (messages::is_map_field_v<T>) {
307 return serialize_map(value, output, offset);
308 } else {
309 return serialize_scalar(value, output, offset);
310 }
311 }
312
321 template <typename Field>
322 [[nodiscard]] static constexpr std::size_t serialize_field(
323 const Field& field, std::span<std::byte> output,
324 std::size_t offset) noexcept {
325 using ValueType = typename Field::FieldType;
326
327 // ArrayField doesn't have set byte - serialize directly
328 if constexpr (messages::is_array_field_v<Field>) {
329 return serialize_array(field, output, offset);
330 } else if constexpr (messages::is_map_field_v<Field>) {
331 return serialize_map(field, output, offset);
332 } else {
333 const bool set = field.set_;
334 output[offset++] = static_cast<std::byte>(set ? 1 : 0);
335
336 if (set) {
337 return serialize_value(field.value_, output, offset);
338 } else {
339 // Zero fill unset fields
340 const std::size_t end_offset =
341 calculate_value_end_offset<ValueType>(offset);
342 const std::size_t size = end_offset - offset;
343 std::memset(output.data() + offset, 0, size);
344 return end_offset;
345 }
346 }
347 }
348
357 template <typename T>
358 [[nodiscard]] static constexpr std::size_t serialize_string(
359 const T& value, std::span<std::byte> output,
360 std::size_t offset) noexcept {
361 const std::size_t padding = calculate_padding<uint32_t>(offset);
362 if (padding > 0) {
363 std::memset(output.data() + offset, 0, padding);
364 offset += padding;
365 }
366
367 const uint32_t len = static_cast<uint32_t>(value.current_len_);
368 const uint32_t le_len = Crunch::LittleEndian(len);
369 std::memcpy(output.data() + offset, &le_len, sizeof(len));
370 offset += sizeof(len);
371
372 std::memcpy(output.data() + offset, value.buffer_.data(), T::max_size);
373 offset += T::max_size;
374 return offset;
375 }
376
385 template <typename T>
386 [[nodiscard]] static constexpr std::size_t serialize_message(
387 const T& value, std::span<std::byte> output,
388 std::size_t offset) noexcept {
389 const std::size_t padding =
390 calculate_padding<std::byte[Alignment]>(offset);
391 if (padding > 0) {
392 std::memset(output.data() + offset, 0, padding);
393 offset += padding;
394 }
395
396 const MessageId msgId = T::message_id;
397 const MessageId le_msgId = Crunch::LittleEndian(msgId);
398 std::memcpy(output.data() + offset, &le_msgId, sizeof(msgId));
399 offset += sizeof(msgId);
400
401 std::apply(
402 [&](const auto&... fields) {
403 ((offset = serialize_field(fields, output, offset)), ...);
404 },
405 value.get_fields());
406 return offset;
407 }
408
417 template <typename T>
418 [[nodiscard]] static constexpr std::size_t serialize_scalar(
419 const T& value, std::span<std::byte> output,
420 std::size_t offset) noexcept {
421 using ValT = typename T::ValueType;
422 const std::size_t padding = calculate_padding<ValT>(offset);
423 if (padding > 0) {
424 std::memset(output.data() + offset, 0, padding);
425 offset += padding;
426 }
427
428 const auto le_value = Crunch::LittleEndian(value.get());
429 std::memcpy(output.data() + offset, &le_value, sizeof(le_value));
430 offset += sizeof(le_value);
431 return offset;
432 }
433
442 template <typename T>
443 [[nodiscard]] static constexpr std::size_t serialize_array(
444 const T& value, std::span<std::byte> output,
445 std::size_t offset) noexcept {
446 using ValT = typename T::ValueType;
447 const std::size_t padding = calculate_padding<uint32_t>(offset);
448 if (padding > 0) {
449 std::memset(output.data() + offset, 0, padding);
450 offset += padding;
451 }
452
453 const uint32_t len = static_cast<uint32_t>(value.current_len_);
454 const uint32_t le_len = Crunch::LittleEndian(len);
455 std::memcpy(output.data() + offset, &le_len, sizeof(len));
456 offset += sizeof(len);
457
458 // Serialize elements
459 for (std::size_t i = 0; i < value.current_len_; ++i) {
460 offset = serialize_value(value.items_[i], output, offset);
461 }
462
463 // Zero fill remaining slots
464 for (std::size_t i = value.current_len_; i < T::max_size; ++i) {
465 const std::size_t end = calculate_value_end_offset<ValT>(offset);
466 std::memset(output.data() + offset, 0, end - offset);
467 offset = end;
468 }
469 return offset;
470 }
471
480 template <typename T>
481 [[nodiscard]] static constexpr std::size_t serialize_map(
482 const T& value, std::span<std::byte> output,
483 std::size_t offset) noexcept {
484 using KeyField = typename T::PairType::first_type;
485 using ValueField = typename T::PairType::second_type;
486
487 const std::size_t padding = calculate_padding<uint32_t>(offset);
488 if (padding > 0) {
489 std::memset(output.data() + offset, 0, padding);
490 offset += padding;
491 }
492
493 const uint32_t len = static_cast<uint32_t>(value.current_len_);
494 const uint32_t le_len = Crunch::LittleEndian(len);
495 std::memcpy(output.data() + offset, &le_len, sizeof(len));
496 offset += sizeof(len);
497
498 // Serialize elements
499 for (std::size_t i = 0; i < value.current_len_; ++i) {
500 const auto& pair = value.items_[i];
501 offset = serialize_value(pair.first, output, offset);
502 offset = serialize_value(pair.second, output, offset);
503 }
504
505 // Zero fill remaining slots
506 for (std::size_t i = value.current_len_; i < T::max_size; ++i) {
507 const std::size_t key_end =
508 calculate_value_end_offset<KeyField>(offset);
509 std::memset(output.data() + offset, 0, key_end - offset);
510 offset = key_end;
511
512 const std::size_t val_end =
513 calculate_value_end_offset<ValueField>(offset);
514 std::memset(output.data() + offset, 0, val_end - offset);
515 offset = val_end;
516 }
517 return offset;
518 }
519
529 template <typename T>
530 [[nodiscard]] static constexpr auto deserialize_value(
531 T& value, bool set, std::span<const std::byte> input,
532 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
533 if constexpr (fields::is_string_v<T>) {
534 return deserialize_string(value, set, input, offset);
535 } else if constexpr (messages::CrunchMessage<T>) {
536 return deserialize_message(value, set, input, offset);
537 } else if constexpr (messages::is_array_field_v<T>) {
538 return deserialize_array(value, set, input, offset);
539 } else if constexpr (messages::is_map_field_v<T>) {
540 return deserialize_map(value, set, input, offset);
541 } else {
542 return deserialize_scalar(value, set, input, offset);
543 }
544 }
545
554 template <typename Field>
555 [[nodiscard]] static constexpr auto deserialize_field(
556 Field& field, std::span<const std::byte> input,
557 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
558 // ArrayField and MapField don't have set byte
559 if constexpr (messages::is_array_field_v<Field>) {
560 return deserialize_array(field, true, input, offset);
561 } else if constexpr (messages::is_map_field_v<Field>) {
562 return deserialize_map(field, true, input, offset);
563 } else {
564 const bool set = static_cast<bool>(input[offset++]);
565 field.set_ = set;
566 return deserialize_value(field.value_, set, input, offset);
567 }
568 }
569
579 template <typename T>
580 [[nodiscard]] static constexpr auto deserialize_message(
581 T& value, bool set, std::span<const std::byte> input,
582 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
583 const std::size_t padding =
584 calculate_padding<std::byte[Alignment]>(offset);
585 offset += padding;
586
587 MessageId msg_id;
588 std::memcpy(&msg_id, input.data() + offset, sizeof(MessageId));
589 msg_id = Crunch::LittleEndian(msg_id);
590
591 if (set && msg_id != T::message_id) {
592 return std::unexpected(Error::invalid_message_id());
593 }
594 offset += sizeof(MessageId);
595
596 if (set) {
597 std::optional<Error> err = std::nullopt;
598 std::apply(
599 [&](auto&... sub_fields) {
600 ((err.has_value()
601 ? void()
602 : [&] {
603 auto result =
604 deserialize_field(sub_fields, input, offset);
605 if (result.has_value()) {
606 offset = result.value();
607 } else {
608 err = result.error();
609 }
610 }()),
611 ...);
612 },
613 value.get_fields());
614 if (err) {
615 return std::unexpected(err.value());
616 }
617 } else {
618 // Skip over submessage
619 offset += calculate_payload_size(T{});
620 }
621 return offset;
622 }
623
633 template <typename T>
634 [[nodiscard]] static constexpr auto deserialize_scalar(
635 T& value, bool set, std::span<const std::byte> input,
636 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
637 using ValT = typename T::ValueType;
638 offset += calculate_padding<ValT>(offset);
639
640 if (set) {
641 ValT le_value;
642 std::memcpy(&le_value, input.data() + offset, sizeof(ValT));
643 value.set_without_validation(Crunch::LittleEndian(le_value));
644 } else {
645 // Default initialization handled by caller
646 }
647 offset += sizeof(ValT);
648 return offset;
649 }
650
660 template <typename T>
661 [[nodiscard]] static constexpr auto deserialize_string(
662 T& value, bool set, std::span<const std::byte> input,
663 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
664 offset += calculate_padding<uint32_t>(offset);
665
666 uint32_t le_len;
667 std::memcpy(&le_len, input.data() + offset, sizeof(le_len));
668 offset += sizeof(le_len);
669 uint32_t len = Crunch::LittleEndian(le_len);
670
671 if (set) {
672 if (len > T::max_size) {
673 return std::unexpected(Error::capacity_exceeded(
674 0,
675 "deserialized string too long")); // No ID available here
676 }
677 std::memcpy(value.buffer_.data(), input.data() + offset,
678 T::max_size);
679 value.current_len_ = len;
680 } else {
681 value.clear();
682 }
683 offset += T::max_size;
684 return offset;
685 }
686
696 template <typename T>
697 [[nodiscard]] static constexpr auto deserialize_array(
698 T& value, bool set, std::span<const std::byte> input,
699 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
700 const auto array_end = calculate_array_end_offset<T>(offset);
701
702 if (!set) {
703 value.clear();
704 return array_end;
705 }
706
707 offset += calculate_padding<uint32_t>(offset);
708
709 uint32_t le_len;
710 std::memcpy(&le_len, input.data() + offset, sizeof(le_len));
711 offset += sizeof(le_len);
712 uint32_t len = Crunch::LittleEndian(le_len);
713
714 if (len > T::max_size) {
715 return std::unexpected(
716 Error::capacity_exceeded(0, "array capacity exceeded"));
717 }
718 value.current_len_ = len;
719
720 // Deserialize active elements
721 for (size_t i = 0; i < len; ++i) {
722 auto res = deserialize_value(value.items_[i], true, input, offset);
723 if (!res) {
724 return std::unexpected(res.error());
725 }
726 offset = res.value();
727 }
728
729 return array_end;
730 }
731
741 template <typename T>
742 [[nodiscard]] static constexpr auto deserialize_map(
743 T& value, bool set, std::span<const std::byte> input,
744 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
745 const auto map_end = calculate_map_end_offset<T>(offset);
746
747 if (!set) {
748 value.clear();
749 return map_end;
750 }
751
752 offset += calculate_padding<uint32_t>(offset);
753
754 // Deserialize length
755 uint32_t le_len;
756 std::memcpy(&le_len, input.data() + offset, sizeof(le_len));
757 offset += sizeof(le_len);
758 uint32_t len = Crunch::LittleEndian(le_len);
759
760 if (len > T::max_size) {
761 return std::unexpected(
762 Error::capacity_exceeded(0, "map capacity exceeded"));
763 }
764 value.current_len_ = len;
765
766 // Deserialize key-value pairs
767 for (size_t i = 0; i < len; ++i) {
768 auto& pair = value.items_[i];
769
770 const auto res_key =
771 deserialize_value(pair.first, true, input, offset);
772 if (!res_key) {
773 return std::unexpected(res_key.error());
774 }
775 offset = res_key.value();
776
777 const auto res_val =
778 deserialize_value(pair.second, true, input, offset);
779 if (!res_val) {
780 return std::unexpected(res_val.error());
781 }
782 offset = res_val.value();
783 }
784
785 return map_end;
786 }
787};
788
789using PackedLayout = StaticLayout<1>;
790using Aligned32Layout = StaticLayout<4>;
791using Aligned64Layout = StaticLayout<8>;
792
793} // namespace Crunch::serdes
constexpr T LittleEndian(T value) noexcept
Converts a value to/from Little Endian byte order.
Definition: crunch_endian.hpp:21
int32_t MessageId
Unique identifier for a message type.
Definition: crunch_types.hpp:25
Format
Serialization format identifier stored in the message header.
Definition: crunch_types.hpp:30
@ Aligned4
4-byte alignment padding.
@ Packed
No alignment padding (Alignment = 1).
@ Aligned8
8-byte alignment padding.
static constexpr Error invalid_message_id() noexcept
Creates an error representing an invalid message ID.
Definition: crunch_types.hpp:112
static constexpr Error capacity_exceeded(FieldId id, const char(&msg)[N]) noexcept
Creates an error representing capacity exceeded. Used for strings and aggregated fields.
Definition: crunch_types.hpp:131
A deterministic, fixed-size binary serialization policy.
Definition: crunch_static_layout.hpp:24
static constexpr std::size_t Size() noexcept
Calculates the serialized size of a message.
Definition: crunch_static_layout.hpp:48
static constexpr Crunch::Format GetFormat() noexcept
Gets the Crunch format corresponding to the alignment.
Definition: crunch_static_layout.hpp:32
static constexpr auto Deserialize(std::span< const std::byte > input, Message &msg) noexcept -> std::optional< Error >
Deserializes a message from the input buffer.
Definition: crunch_static_layout.hpp:86
static constexpr std::size_t Serialize(const Message &msg, std::span< std::byte > output) noexcept
Serializes a message into the output buffer.
Definition: crunch_static_layout.hpp:60