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 + sizeof(MessageId) +
50 calculate_payload_size(Message{});
51 }
52
60 template <typename Message>
61 static constexpr std::size_t Serialize(
62 const Message& msg, std::span<std::byte> output) noexcept {
63 std::size_t offset = StandardHeaderSize;
64 if (PayloadStartOffset > StandardHeaderSize) {
65 std::memset(output.data() + offset, 0,
66 PayloadStartOffset - StandardHeaderSize);
67 }
68 offset = PayloadStartOffset;
69
70 const MessageId msgId = Message::message_id;
71 const MessageId le_msgId = Crunch::LittleEndian(msgId);
72 std::memcpy(output.data() + offset, &le_msgId, sizeof(msgId));
73 offset += sizeof(msgId);
74
75 std::apply(
76 [&](const auto&... fields) {
77 ((offset = serialize_field(fields, output, offset)), ...);
78 },
79 msg.get_fields());
80 return offset;
81 }
82
90 template <typename Message>
91 [[nodiscard]] static constexpr auto Deserialize(
92 std::span<const std::byte> input, Message& msg) noexcept
93 -> std::optional<Error> {
94 std::size_t offset = PayloadStartOffset;
95
96 MessageId msg_id;
97 std::memcpy(&msg_id, input.data() + offset, sizeof(MessageId));
98 msg_id = Crunch::LittleEndian(msg_id);
99 if (msg_id != Message::message_id) {
101 }
102 offset += sizeof(MessageId);
103
104 std::optional<Error> err = std::nullopt;
105 std::apply(
106 [&](auto&... fields) {
107 ((err.has_value()
108 ? void()
109 : [&] {
110 auto result =
111 deserialize_field(fields, input, offset);
112 if (result.has_value()) {
113 offset = result.value();
114 } else {
115 err = result.error();
116 }
117 }()),
118 ...);
119 },
120 msg.get_fields());
121 return err;
122 }
123
124 private:
131 [[nodiscard]] static constexpr std::size_t align_up(
132 std::size_t value, std::size_t alignment) noexcept {
133 return (value + alignment - 1) & ~(alignment - 1);
134 }
135
143 template <typename T>
144 [[nodiscard]] static constexpr std::size_t calculate_padding(
145 std::size_t offset) noexcept {
146 const std::size_t align = std::min(sizeof(T), Alignment);
147 return (align - (offset % align)) % align;
148 }
149
150 static constexpr std::size_t PayloadStartOffset =
151 align_up(StandardHeaderSize, Alignment);
152
159 template <typename Message>
160 [[nodiscard]] static constexpr std::size_t calculate_payload_size(
161 const Message& msg) noexcept {
162 std::size_t offset = 0;
163 std::apply(
164 [&](const auto&... fields) {
165 ((offset = calculate_field_end_offset(fields, offset)), ...);
166 },
167 msg.get_fields());
168 return offset;
169 }
170
177 template <typename T>
178 [[nodiscard]] static constexpr std::size_t calculate_value_end_offset(
179 std::size_t offset) noexcept {
180 if constexpr (fields::is_string_v<T>) {
181 return calculate_string_end_offset<T>(offset);
182 } else if constexpr (messages::CrunchMessage<T>) {
183 return calculate_message_end_offset<T>(offset);
184 } else if constexpr (messages::is_array_field_v<T>) {
185 return calculate_array_end_offset<T>(offset);
186 } else if constexpr (messages::is_map_field_v<T>) {
187 return calculate_map_end_offset<T>(offset);
188 } else {
189 return calculate_scalar_end_offset<T>(offset);
190 }
191 }
192
199 template <typename Field>
200 [[nodiscard]] static constexpr std::size_t calculate_field_end_offset(
201 const Field&, std::size_t offset) noexcept {
202 using ValueType = typename Field::FieldType;
203 // ArrayField and MapField don't have is_set byte in wire format
204 if constexpr (!messages::is_array_field_v<Field> &&
205 !messages::is_map_field_v<Field>) {
206 offset += 1;
207 }
208 return calculate_value_end_offset<ValueType>(offset);
209 }
210
217 template <typename T>
218 [[nodiscard]] static constexpr std::size_t calculate_message_end_offset(
219 std::size_t offset) noexcept {
220 const std::size_t padding =
221 calculate_padding<std::byte[Alignment]>(offset);
222 offset += padding;
223 offset += sizeof(MessageId) + calculate_payload_size(T{});
224 return offset;
225 }
226
233 template <typename T>
234 [[nodiscard]] static constexpr std::size_t calculate_scalar_end_offset(
235 std::size_t offset) noexcept {
236 using ValT = typename T::ValueType;
237 offset += calculate_padding<ValT>(offset) + sizeof(ValT);
238 return offset;
239 }
240
247 template <typename T>
248 [[nodiscard]] static constexpr std::size_t calculate_string_end_offset(
249 std::size_t offset) noexcept {
250 offset += calculate_padding<uint32_t>(offset) + sizeof(uint32_t) +
251 T::max_size;
252 return offset;
253 }
254
261 template <typename T>
262 [[nodiscard]] static constexpr std::size_t calculate_array_end_offset(
263 std::size_t offset) noexcept {
264 using ValT = typename T::ValueType;
265 // Align for Length (uint32_t)
266 offset += calculate_padding<uint32_t>(offset) + sizeof(uint32_t);
267
268 // Calculate size of MaxSize elements
269 // Since element locations depend on alignment which depends on offset,
270 // we must iterate.
271 for (std::size_t i = 0; i < T::max_size; ++i) {
272 offset = calculate_value_end_offset<ValT>(offset);
273 }
274 return offset;
275 }
276
283 template <typename T>
284 [[nodiscard]] static constexpr std::size_t calculate_map_end_offset(
285 std::size_t offset) noexcept {
286 using KeyField = typename T::PairType::first_type;
287 using ValueField = typename T::PairType::second_type;
288
289 // Align for Length (uint32_t)
290 offset += calculate_padding<uint32_t>(offset) + sizeof(uint32_t);
291
292 // Calculate size of MaxSize elements (Key + Value pairs)
293 for (std::size_t i = 0; i < T::max_size; ++i) {
294 offset = calculate_value_end_offset<KeyField>(offset);
295 offset = calculate_value_end_offset<ValueField>(offset);
296 }
297 return offset;
298 }
299
308 template <typename T>
309 [[nodiscard]] static constexpr std::size_t serialize_value(
310 const T& value, std::span<std::byte> output,
311 std::size_t offset) noexcept {
312 if constexpr (fields::is_string_v<T>) {
313 return serialize_string(value, output, offset);
314 } else if constexpr (messages::CrunchMessage<T>) {
315 return serialize_message(value, output, offset);
316 } else if constexpr (messages::is_array_field_v<T>) {
317 return serialize_array(value, output, offset);
318 } else if constexpr (messages::is_map_field_v<T>) {
319 return serialize_map(value, output, offset);
320 } else {
321 return serialize_scalar(value, output, offset);
322 }
323 }
324
333 template <typename Field>
334 [[nodiscard]] static constexpr std::size_t serialize_field(
335 const Field& field, std::span<std::byte> output,
336 std::size_t offset) noexcept {
337 using ValueType = typename Field::FieldType;
338
339 // ArrayField doesn't have set byte - serialize directly
340 if constexpr (messages::is_array_field_v<Field>) {
341 return serialize_array(field, output, offset);
342 } else if constexpr (messages::is_map_field_v<Field>) {
343 return serialize_map(field, output, offset);
344 } else {
345 const bool set = field.set_;
346 output[offset++] = static_cast<std::byte>(set ? 1 : 0);
347
348 if (set) {
349 return serialize_value(field.value_, output, offset);
350 } else {
351 // Zero fill unset fields
352 const std::size_t end_offset =
353 calculate_value_end_offset<ValueType>(offset);
354 const std::size_t size = end_offset - offset;
355 std::memset(output.data() + offset, 0, size);
356 return end_offset;
357 }
358 }
359 }
360
369 template <typename T>
370 [[nodiscard]] static constexpr std::size_t serialize_string(
371 const T& value, std::span<std::byte> output,
372 std::size_t offset) noexcept {
373 const std::size_t padding = calculate_padding<uint32_t>(offset);
374 if (padding > 0) {
375 std::memset(output.data() + offset, 0, padding);
376 offset += padding;
377 }
378
379 const uint32_t len = static_cast<uint32_t>(value.current_len_);
380 const uint32_t le_len = Crunch::LittleEndian(len);
381 std::memcpy(output.data() + offset, &le_len, sizeof(len));
382 offset += sizeof(len);
383
384 std::memcpy(output.data() + offset, value.buffer_.data(), T::max_size);
385 offset += T::max_size;
386 return offset;
387 }
388
397 template <typename T>
398 [[nodiscard]] static constexpr std::size_t serialize_message(
399 const T& value, std::span<std::byte> output,
400 std::size_t offset) noexcept {
401 const std::size_t padding =
402 calculate_padding<std::byte[Alignment]>(offset);
403 if (padding > 0) {
404 std::memset(output.data() + offset, 0, padding);
405 offset += padding;
406 }
407
408 const MessageId msgId = T::message_id;
409 const MessageId le_msgId = Crunch::LittleEndian(msgId);
410 std::memcpy(output.data() + offset, &le_msgId, sizeof(msgId));
411 offset += sizeof(msgId);
412
413 std::apply(
414 [&](const auto&... fields) {
415 ((offset = serialize_field(fields, output, offset)), ...);
416 },
417 value.get_fields());
418 return offset;
419 }
420
429 template <typename T>
430 [[nodiscard]] static constexpr std::size_t serialize_scalar(
431 const T& value, std::span<std::byte> output,
432 std::size_t offset) noexcept {
433 using ValT = typename T::ValueType;
434 const std::size_t padding = calculate_padding<ValT>(offset);
435 if (padding > 0) {
436 std::memset(output.data() + offset, 0, padding);
437 offset += padding;
438 }
439
440 const auto le_value = Crunch::LittleEndian(value.get());
441 std::memcpy(output.data() + offset, &le_value, sizeof(le_value));
442 offset += sizeof(le_value);
443 return offset;
444 }
445
454 template <typename T>
455 [[nodiscard]] static constexpr std::size_t serialize_array(
456 const T& value, std::span<std::byte> output,
457 std::size_t offset) noexcept {
458 using ValT = typename T::ValueType;
459 const std::size_t padding = calculate_padding<uint32_t>(offset);
460 if (padding > 0) {
461 std::memset(output.data() + offset, 0, padding);
462 offset += padding;
463 }
464
465 const uint32_t len = static_cast<uint32_t>(value.current_len_);
466 const uint32_t le_len = Crunch::LittleEndian(len);
467 std::memcpy(output.data() + offset, &le_len, sizeof(len));
468 offset += sizeof(len);
469
470 // Serialize elements
471 for (std::size_t i = 0; i < value.current_len_; ++i) {
472 offset = serialize_value(value.items_[i], output, offset);
473 }
474
475 // Zero fill remaining slots
476 for (std::size_t i = value.current_len_; i < T::max_size; ++i) {
477 const std::size_t end = calculate_value_end_offset<ValT>(offset);
478 std::memset(output.data() + offset, 0, end - offset);
479 offset = end;
480 }
481 return offset;
482 }
483
492 template <typename T>
493 [[nodiscard]] static constexpr std::size_t serialize_map(
494 const T& value, std::span<std::byte> output,
495 std::size_t offset) noexcept {
496 using KeyField = typename T::PairType::first_type;
497 using ValueField = typename T::PairType::second_type;
498
499 const std::size_t padding = calculate_padding<uint32_t>(offset);
500 if (padding > 0) {
501 std::memset(output.data() + offset, 0, padding);
502 offset += padding;
503 }
504
505 const uint32_t len = static_cast<uint32_t>(value.current_len_);
506 const uint32_t le_len = Crunch::LittleEndian(len);
507 std::memcpy(output.data() + offset, &le_len, sizeof(len));
508 offset += sizeof(len);
509
510 // Serialize elements
511 for (std::size_t i = 0; i < value.current_len_; ++i) {
512 const auto& pair = value.items_[i];
513 offset = serialize_value(pair.first, output, offset);
514 offset = serialize_value(pair.second, output, offset);
515 }
516
517 // Zero fill remaining slots
518 for (std::size_t i = value.current_len_; i < T::max_size; ++i) {
519 const std::size_t key_end =
520 calculate_value_end_offset<KeyField>(offset);
521 std::memset(output.data() + offset, 0, key_end - offset);
522 offset = key_end;
523
524 const std::size_t val_end =
525 calculate_value_end_offset<ValueField>(offset);
526 std::memset(output.data() + offset, 0, val_end - offset);
527 offset = val_end;
528 }
529 return offset;
530 }
531
541 template <typename T>
542 [[nodiscard]] static constexpr auto deserialize_value(
543 T& value, bool set, std::span<const std::byte> input,
544 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
545 if constexpr (fields::is_string_v<T>) {
546 return deserialize_string(value, set, input, offset);
547 } else if constexpr (messages::CrunchMessage<T>) {
548 return deserialize_message(value, set, input, offset);
549 } else if constexpr (messages::is_array_field_v<T>) {
550 return deserialize_array(value, set, input, offset);
551 } else if constexpr (messages::is_map_field_v<T>) {
552 return deserialize_map(value, set, input, offset);
553 } else {
554 return deserialize_scalar(value, set, input, offset);
555 }
556 }
557
566 template <typename Field>
567 [[nodiscard]] static constexpr auto deserialize_field(
568 Field& field, std::span<const std::byte> input,
569 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
570 // ArrayField and MapField don't have set byte
571 if constexpr (messages::is_array_field_v<Field>) {
572 return deserialize_array(field, true, input, offset);
573 } else if constexpr (messages::is_map_field_v<Field>) {
574 return deserialize_map(field, true, input, offset);
575 } else {
576 const bool set = static_cast<bool>(input[offset++]);
577 field.set_ = set;
578 return deserialize_value(field.value_, set, input, offset);
579 }
580 }
581
591 template <typename T>
592 [[nodiscard]] static constexpr auto deserialize_message(
593 T& value, bool set, std::span<const std::byte> input,
594 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
595 const std::size_t padding =
596 calculate_padding<std::byte[Alignment]>(offset);
597 offset += padding;
598
599 MessageId msg_id;
600 std::memcpy(&msg_id, input.data() + offset, sizeof(MessageId));
601 msg_id = Crunch::LittleEndian(msg_id);
602
603 if (set && msg_id != T::message_id) {
604 return std::unexpected(Error::invalid_message_id());
605 }
606 offset += sizeof(MessageId);
607
608 if (set) {
609 std::optional<Error> err = std::nullopt;
610 std::apply(
611 [&](auto&... sub_fields) {
612 ((err.has_value()
613 ? void()
614 : [&] {
615 auto result =
616 deserialize_field(sub_fields, input, offset);
617 if (result.has_value()) {
618 offset = result.value();
619 } else {
620 err = result.error();
621 }
622 }()),
623 ...);
624 },
625 value.get_fields());
626 if (err) {
627 return std::unexpected(err.value());
628 }
629 } else {
630 // Skip over submessage
631 offset += calculate_payload_size(T{});
632 }
633 return offset;
634 }
635
645 template <typename T>
646 [[nodiscard]] static constexpr auto deserialize_scalar(
647 T& value, bool set, std::span<const std::byte> input,
648 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
649 using ValT = typename T::ValueType;
650 offset += calculate_padding<ValT>(offset);
651
652 if (set) {
653 ValT le_value;
654 std::memcpy(&le_value, input.data() + offset, sizeof(ValT));
655 value.set_without_validation(Crunch::LittleEndian(le_value));
656 } else {
657 // Default initialization handled by caller
658 }
659 offset += sizeof(ValT);
660 return offset;
661 }
662
672 template <typename T>
673 [[nodiscard]] static constexpr auto deserialize_string(
674 T& value, bool set, std::span<const std::byte> input,
675 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
676 offset += calculate_padding<uint32_t>(offset);
677
678 uint32_t le_len;
679 std::memcpy(&le_len, input.data() + offset, sizeof(le_len));
680 offset += sizeof(le_len);
681 uint32_t len = Crunch::LittleEndian(le_len);
682
683 if (set) {
684 if (len > T::max_size) {
685 return std::unexpected(Error::capacity_exceeded(
686 0,
687 "deserialized string too long")); // No ID available here
688 }
689 std::memcpy(value.buffer_.data(), input.data() + offset,
690 T::max_size);
691 value.current_len_ = len;
692 } else {
693 value.clear();
694 }
695 offset += T::max_size;
696 return offset;
697 }
698
708 template <typename T>
709 [[nodiscard]] static constexpr auto deserialize_array(
710 T& value, bool set, std::span<const std::byte> input,
711 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
712 const auto array_end = calculate_array_end_offset<T>(offset);
713
714 if (!set) {
715 value.clear();
716 return array_end;
717 }
718
719 offset += calculate_padding<uint32_t>(offset);
720
721 uint32_t le_len;
722 std::memcpy(&le_len, input.data() + offset, sizeof(le_len));
723 offset += sizeof(le_len);
724 uint32_t len = Crunch::LittleEndian(le_len);
725
726 if (len > T::max_size) {
727 return std::unexpected(
728 Error::capacity_exceeded(0, "array capacity exceeded"));
729 }
730 value.current_len_ = len;
731
732 // Deserialize active elements
733 for (size_t i = 0; i < len; ++i) {
734 auto res = deserialize_value(value.items_[i], true, input, offset);
735 if (!res) {
736 return std::unexpected(res.error());
737 }
738 offset = res.value();
739 }
740
741 return array_end;
742 }
743
753 template <typename T>
754 [[nodiscard]] static constexpr auto deserialize_map(
755 T& value, bool set, std::span<const std::byte> input,
756 std::size_t offset) noexcept -> std::expected<std::size_t, Error> {
757 const auto map_end = calculate_map_end_offset<T>(offset);
758
759 if (!set) {
760 value.clear();
761 return map_end;
762 }
763
764 offset += calculate_padding<uint32_t>(offset);
765
766 // Deserialize length
767 uint32_t le_len;
768 std::memcpy(&le_len, input.data() + offset, sizeof(le_len));
769 offset += sizeof(le_len);
770 uint32_t len = Crunch::LittleEndian(le_len);
771
772 if (len > T::max_size) {
773 return std::unexpected(
774 Error::capacity_exceeded(0, "map capacity exceeded"));
775 }
776 value.current_len_ = len;
777
778 // Deserialize key-value pairs
779 for (size_t i = 0; i < len; ++i) {
780 auto& pair = value.items_[i];
781
782 const auto res_key =
783 deserialize_value(pair.first, true, input, offset);
784 if (!res_key) {
785 return std::unexpected(res_key.error());
786 }
787 offset = res_key.value();
788
789 const auto res_val =
790 deserialize_value(pair.second, true, input, offset);
791 if (!res_val) {
792 return std::unexpected(res_val.error());
793 }
794 offset = res_val.value();
795 }
796
797 return map_end;
798 }
799};
800
801using PackedLayout = StaticLayout<1>;
802using Aligned32Layout = StaticLayout<4>;
803using Aligned64Layout = StaticLayout<8>;
804
805} // 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:114
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:133
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:91
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:61