diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b3dae..1e850e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2026-04-25 + +### Added — DevEx P0 features + +- **`AsTrimmedSpan()` on InlineArray char types** (#155): Like `AsSpan()` but additionally strips trailing space (0x20) padding — matches the behavior of `ToString().Trim()` without the two allocations. Lets hot-path consumers compare/log FIX-style space-padded fields (`Symbol`, `Currency`, `IsinNumber`, …) zero-alloc. +- **`ISpanFormattable` on InlineArray char types and decimal composites** (#153): Generated char and decimal-composite types now implement `ISpanFormattable` — `TryFormat(Span, out int, ReadOnlySpan, IFormatProvider?)` plus `ToString(string?, IFormatProvider?)`. Char types delegate to the schema's `characterEncoding` and `AsTrimmedSpan()`. Decimal composites use the schema's `Decimals` constant for the default format (e.g. `F4` / `F8`), so `string.Create(...)` and `Utf8.TryWrite(...)` emit correctly-scaled strings without allocating. +- **Static header read helpers on the message header composite** (#156): The composite designated by `headerType` (default `messageHeader`) now exposes `TryReadTemplateId(ReadOnlySpan, out ushort)` and `TryReadHeader(ReadOnlySpan, out blockLength, out templateId, out schemaId, out version)`. Single source of truth for header layout — consumers no longer hardcode `SbeHeaderSize = 8` or compute offsets manually. + +### Performance +- All three additions are emit-time only with no allocation on the new code paths. `AsTrimmedSpan()` and `TryReadTemplateId` are aggressively inlined. + ## [1.3.0] - 2026-04-25 ### Added diff --git a/README.md b/README.md index 77fe3b1..83728dc 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A Roslyn-based source generator that converts FIX Simple Binary Encoding (SBE) X - Variable-length data (varData) with configurable length prefix (uint8/uint16/uint32) - Constant fields in messages, composites, and groups - Derived numeric constants (`MinValue`/`MaxValue` on type wrappers; `Decimals`/`Multiplier`/`MultiplierDecimal`/`Divisor` on decimal composites) +- Allocation-free formatting via `ISpanFormattable` on char types and decimal composites; `AsTrimmedSpan()` on InlineArray char types - Automatic and explicit field offset calculation - Byte order handling (little-endian native, big-endian with `readonly` property-based conversion) - Schema versioning (`sinceVersion`, `deprecated` on fields, enums, sets, data) with per-message `{Msg}VersionMap` for `blockLength → version` lookup diff --git a/src/SbeCodeGenerator/Generators/Types/CompositeDefinition.cs b/src/SbeCodeGenerator/Generators/Types/CompositeDefinition.cs index e514a6a..ea9045f 100644 --- a/src/SbeCodeGenerator/Generators/Types/CompositeDefinition.cs +++ b/src/SbeCodeGenerator/Generators/Types/CompositeDefinition.cs @@ -5,7 +5,8 @@ namespace SbeSourceGenerator { public record CompositeDefinition(string Namespace, string Name, string Description, string SemanticType, - List Fields, EndianConversion EndianConversion = EndianConversion.None) : IFileContentGenerator + List Fields, EndianConversion EndianConversion = EndianConversion.None, + bool IsMessageHeader = false) : IFileContentGenerator { public void AppendFileContent(StringBuilder sb, int tabs = 0) { @@ -105,8 +106,56 @@ public void AppendFileContent(StringBuilder sb, int tabs = 0) else { FileContentGeneratorExtensions.AppendToString(sb, tabs, Name, Fields); + if (IsMessageHeader) + { + AppendHeaderHelpers(sb, tabs); + } } sb.AppendLine("}", --tabs); } + + private void AppendHeaderHelpers(StringBuilder sb, int tabs) + { + // Issue #156: static convenience helpers so consumers don't hardcode + // header offsets when they need to peek at the templateId before dispatching. + // Field accessors handle endian conversion, so we delegate to them. + var valueFields = Fields.OfType().ToList(); + bool hasField(string n) => valueFields.Any(f => string.Equals(f.Name, n, System.StringComparison.OrdinalIgnoreCase)); + if (!(hasField("BlockLength") && hasField("TemplateId") && hasField("SchemaId") && hasField("Version"))) + return; + + sb.AppendLine("", tabs); + sb.AppendLine("/// ", tabs); + sb.AppendLine("/// Reads the templateId from a buffer that starts with this header. Returns false if the buffer is too small.", tabs); + sb.AppendLine("/// ", tabs); + sb.AppendLine("[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]", tabs); + sb.AppendLine("public static bool TryReadTemplateId(ReadOnlySpan buffer, out ushort templateId)", tabs); + sb.AppendLine("{", tabs++); + sb.AppendLine("if (buffer.Length < MESSAGE_SIZE) { templateId = 0; return false; }", tabs); + sb.AppendTabs(tabs).Append("templateId = MemoryMarshal.AsRef<").Append(Name).AppendLine(">(buffer).TemplateId;"); + sb.AppendLine("return true;", tabs); + sb.AppendLine("}", --tabs); + + sb.AppendLine("", tabs); + sb.AppendLine("/// ", tabs); + sb.AppendLine("/// Reads the four standard header fields (blockLength, templateId, schemaId, version)", tabs); + sb.AppendLine("/// in a single call. Returns false if the buffer is smaller than the header.", tabs); + sb.AppendLine("/// ", tabs); + sb.AppendLine("[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]", tabs); + sb.AppendLine("public static bool TryReadHeader(ReadOnlySpan buffer, out ushort blockLength, out ushort templateId, out ushort schemaId, out ushort version)", tabs); + sb.AppendLine("{", tabs++); + sb.AppendLine("if (buffer.Length < MESSAGE_SIZE)", tabs); + sb.AppendLine("{", tabs++); + sb.AppendLine("blockLength = 0; templateId = 0; schemaId = 0; version = 0;", tabs); + sb.AppendLine("return false;", tabs); + sb.AppendLine("}", --tabs); + sb.AppendTabs(tabs).Append("ref readonly var h = ref MemoryMarshal.AsRef<").Append(Name).AppendLine(">(buffer);"); + sb.AppendLine("blockLength = h.BlockLength;", tabs); + sb.AppendLine("templateId = h.TemplateId;", tabs); + sb.AppendLine("schemaId = h.SchemaId;", tabs); + sb.AppendLine("version = h.Version;", tabs); + sb.AppendLine("return true;", tabs); + sb.AppendLine("}", --tabs); + } } } diff --git a/src/SbeCodeGenerator/Generators/Types/DecimalHelperDefinition.cs b/src/SbeCodeGenerator/Generators/Types/DecimalHelperDefinition.cs index 79511a1..558aee1 100644 --- a/src/SbeCodeGenerator/Generators/Types/DecimalHelperDefinition.cs +++ b/src/SbeCodeGenerator/Generators/Types/DecimalHelperDefinition.cs @@ -13,11 +13,14 @@ public record DecimalHelperDefinition(string Namespace, string Name, string Desc public void AppendFileContent(StringBuilder sb, int tabs = 0) { sb.AppendTabs(tabs).Append("namespace ").Append(Namespace).AppendLine(";"); - sb.AppendTabs(tabs).Append("public partial struct ").Append(Name).AppendLine(); + sb.AppendTabs(tabs).Append("public partial struct ").Append(Name).Append(" : ISpanFormattable").AppendLine(); sb.AppendLine("{", tabs++); var exponentStr = Exponent.ToString(CultureInfo.InvariantCulture); var multiplier = $"1e{exponentStr}m"; + int decimals = Exponent < 0 ? -Exponent : 0; + // Default format string (e.g. "F4") used when caller passes empty format. + var defaultFormat = $"\"F{decimals.ToString(CultureInfo.InvariantCulture)}\""; // Issue #145: emit derived constants — single source of truth for decimal places. AppendDerivedConstants(sb, tabs); @@ -29,6 +32,18 @@ public void AppendFileContent(StringBuilder sb, int tabs = 0) sb.AppendLine("/// Returns null if the mantissa is null (optional field).", tabs); sb.AppendLine("/// ", tabs); sb.AppendTabs(tabs).Append("public readonly decimal? ToDecimal() => Mantissa.HasValue ? Mantissa.Value * ").Append(multiplier).AppendLine(" : null;"); + + // Issue #153: ISpanFormattable — allocation-free decimal formatting using the schema's known decimal count. + sb.AppendLine("/// Allocation-free formatting; defaults to F" + decimals.ToString(CultureInfo.InvariantCulture) + ". Empty output for null mantissa.", tabs); + sb.AppendLine("public readonly bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = null)", tabs); + sb.AppendLine("{", tabs++); + sb.AppendLine("var v = ToDecimal();", tabs); + sb.AppendLine("if (!v.HasValue) { charsWritten = 0; return true; }", tabs); + sb.AppendTabs(tabs).Append("return v.Value.TryFormat(destination, out charsWritten, format.IsEmpty ? ").Append(defaultFormat).AppendLine(" : format, provider);"); + sb.AppendLine("}", --tabs); + + sb.AppendLine("/// IFormattable implementation; uses F" + decimals.ToString(CultureInfo.InvariantCulture) + " when format is null. Returns empty string for null mantissa.", tabs); + sb.AppendTabs(tabs).Append("public readonly string ToString(string? format, IFormatProvider? provider) => ToDecimal() is decimal v ? v.ToString(format ?? ").Append(defaultFormat).AppendLine(", provider) : string.Empty;"); } else { @@ -36,6 +51,16 @@ public void AppendFileContent(StringBuilder sb, int tabs = 0) sb.AppendTabs(tabs).Append("/// Converts the mantissa to a decimal using the constant exponent (10^").Append(exponentStr).AppendLine(")."); sb.AppendLine("/// ", tabs); sb.AppendTabs(tabs).Append("public readonly decimal ToDecimal() => Mantissa * ").Append(multiplier).AppendLine(";"); + + // Issue #153 + sb.AppendLine("/// Allocation-free formatting; defaults to F" + decimals.ToString(CultureInfo.InvariantCulture) + ".", tabs); + sb.AppendLine("public readonly bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = null)", tabs); + sb.AppendLine("{", tabs++); + sb.AppendTabs(tabs).Append("return ToDecimal().TryFormat(destination, out charsWritten, format.IsEmpty ? ").Append(defaultFormat).AppendLine(" : format, provider);"); + sb.AppendLine("}", --tabs); + + sb.AppendLine("/// IFormattable implementation; uses F" + decimals.ToString(CultureInfo.InvariantCulture) + " when format is null.", tabs); + sb.AppendTabs(tabs).Append("public readonly string ToString(string? format, IFormatProvider? provider) => ToDecimal().ToString(format ?? ").Append(defaultFormat).AppendLine(", provider);"); } sb.AppendLine("}", --tabs); diff --git a/src/SbeCodeGenerator/Generators/Types/FixedSizeCharTypeDefinition.cs b/src/SbeCodeGenerator/Generators/Types/FixedSizeCharTypeDefinition.cs index 57206bc..a025e95 100644 --- a/src/SbeCodeGenerator/Generators/Types/FixedSizeCharTypeDefinition.cs +++ b/src/SbeCodeGenerator/Generators/Types/FixedSizeCharTypeDefinition.cs @@ -27,7 +27,7 @@ public void AppendFileContent(StringBuilder sb, int tabs = 0) sb.AppendTabs(tabs).Append("namespace ").Append(Namespace).AppendLine(";"); sb.AppendSummary(Description, tabs); sb.AppendTabs(tabs).Append("[System.Runtime.CompilerServices.InlineArray(").Append(Length).AppendLine(")]"); - sb.AppendTabs(tabs).Append("public struct ").Append(Name).AppendLine(); + sb.AppendTabs(tabs).Append("public struct ").Append(Name).Append(" : ISpanFormattable").AppendLine(); sb.AppendLine("{", tabs++); sb.AppendLine("private byte value;", tabs); @@ -41,10 +41,37 @@ public void AppendFileContent(StringBuilder sb, int tabs = 0) sb.AppendTabs(tabs).Append("return span.Slice(0, index == -1 ? ").Append(Length).AppendLine(" : index);"); sb.AppendLine("}", --tabs); + // Issue #155: AsTrimmedSpan() — additionally trims trailing space padding (FIX convention). + sb.AppendLine("/// ", tabs); + sb.AppendLine("/// Like but additionally trims trailing space (0x20) padding —", tabs); + sb.AppendLine("/// matches the behavior of ToString().Trim() without allocating.", tabs); + sb.AppendLine("/// ", tabs); + sb.AppendLine("[System.Diagnostics.CodeAnalysis.UnscopedRef]", tabs); + sb.AppendLine("public readonly ReadOnlySpan AsTrimmedSpan()", tabs); + sb.AppendLine("{", tabs++); + sb.AppendLine("var span = AsSpan();", tabs); + sb.AppendLine("while (span.Length > 0 && span[span.Length - 1] == (byte)' ') span = span.Slice(0, span.Length - 1);", tabs); + sb.AppendLine("return span;", tabs); + sb.AppendLine("}", --tabs); + // Equals(ReadOnlySpan) — zero-allocation comparison sb.AppendLine("/// Compares the content to a byte span without allocation. Useful with UTF-8 string literals: symbol.Equals(\"PETR4\"u8)", tabs); sb.AppendLine("public readonly bool Equals(ReadOnlySpan other) => AsSpan().SequenceEqual(other);", tabs); + // Issue #153: ISpanFormattable — allocation-free formatting for hot-path logging. + sb.AppendLine("/// Allocation-free formatting into a destination span. Returns the trimmed character content.", tabs); + sb.AppendLine("public readonly bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = null)", tabs); + sb.AppendLine("{", tabs++); + sb.AppendLine("var src = AsTrimmedSpan();", tabs); + sb.AppendTabs(tabs).Append("int needed = System.Text.").Append(ResolvedEncoding).AppendLine(".GetCharCount(src);"); + sb.AppendLine("if (destination.Length < needed) { charsWritten = 0; return false; }", tabs); + sb.AppendTabs(tabs).Append("charsWritten = System.Text.").Append(ResolvedEncoding).AppendLine(".GetChars(src, destination);"); + sb.AppendLine("return true;", tabs); + sb.AppendLine("}", --tabs); + + sb.AppendLine("/// IFormattable implementation; delegates to . Format and provider are ignored.", tabs); + sb.AppendLine("public readonly string ToString(string? format, IFormatProvider? formatProvider) => ToString();", tabs); + // ToString() — delegates to AsSpan() sb.AppendTabs(tabs).Append("public readonly override string ToString() => System.Text.").Append(ResolvedEncoding).AppendLine(".GetString(AsSpan());"); sb.AppendLine("}", --tabs); diff --git a/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs b/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs index 1a9f3a1..441ec69 100644 --- a/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs +++ b/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs @@ -408,7 +408,8 @@ internal class TypesCodeGenerator : ICodeGenerator compositeDto.Description, compositeDto.SemanticType, fields, - context.EndianConversion + context.EndianConversion, + IsMessageHeader: string.Equals(compositeDto.Name, context.HeaderType, System.StringComparison.OrdinalIgnoreCase) ); if (generator.Fields.All(f => f is IBlittable)) context.CustomTypeLengths[compositeDto.Name] = generator.Fields.SumFieldLength(); diff --git a/src/SbeCodeGenerator/SbeSourceGenerator.csproj b/src/SbeCodeGenerator/SbeSourceGenerator.csproj index 38513dc..0b5f0e8 100644 --- a/src/SbeCodeGenerator/SbeSourceGenerator.csproj +++ b/src/SbeCodeGenerator/SbeSourceGenerator.csproj @@ -11,7 +11,7 @@ false SbeSourceGenerator SBE Source Generator - 1.3.0 + 1.4.0 Pedro Sakuma Pedro Sakuma SBE Source Generator diff --git a/tests/SbeCodeGenerator.IntegrationTests/DevExFeatureTests.cs b/tests/SbeCodeGenerator.IntegrationTests/DevExFeatureTests.cs new file mode 100644 index 0000000..c2cfa36 --- /dev/null +++ b/tests/SbeCodeGenerator.IntegrationTests/DevExFeatureTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using Edge.Cases.Test.V0; +using Xunit; + +namespace SbeCodeGenerator.IntegrationTests +{ + /// + /// DevEx P0 features: AsTrimmedSpan (#155), ISpanFormattable (#153), + /// MessageHeader static read helpers (#156). + /// + public class DevExFeatureTests + { + // ---------- #155: AsTrimmedSpan on InlineArray char types ---------- + + [Fact] + public void AsTrimmedSpan_StripsTrailingNullsAndSpaces() + { + // IsinCode is an InlineArray char type in the edge-cases schema. + var isin = default(IsinCode); + ReadOnlySpan source = "PETR4 "u8; // trailing space-padded (FIX convention) + source.CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As(ref isin), source.Length)); + + // AsSpan only trims trailing \0 — would still include the spaces. + Assert.Equal("PETR4 "u8.ToArray(), isin.AsSpan().ToArray()); + + // AsTrimmedSpan additionally trims trailing spaces. + Assert.Equal("PETR4"u8.ToArray(), isin.AsTrimmedSpan().ToArray()); + } + + [Fact] + public void AsTrimmedSpan_HandlesAllNullPadding() + { + var isin = default(IsinCode); + ReadOnlySpan source = "BBDC4"u8; + source.CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As(ref isin), source.Length)); + + Assert.Equal("BBDC4"u8.ToArray(), isin.AsTrimmedSpan().ToArray()); + } + + // ---------- #153: ISpanFormattable on InlineArray char types ---------- + + [Fact] + public void CharType_TryFormat_WritesTrimmedContent_ZeroAlloc() + { + var isin = default(IsinCode); + ReadOnlySpan source = "VALE3 "u8; + source.CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As(ref isin), source.Length)); + + Span dest = stackalloc char[16]; + Assert.True(isin.TryFormat(dest, out int written, default, null)); + Assert.Equal(5, written); + Assert.Equal("VALE3", new string(dest.Slice(0, written))); + } + + [Fact] + public void CharType_TryFormat_ReturnsFalse_WhenDestinationTooSmall() + { + var isin = default(IsinCode); + "ITUB4"u8.CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As(ref isin), 5)); + + Span small = stackalloc char[2]; + Assert.False(isin.TryFormat(small, out int written, default, null)); + Assert.Equal(0, written); + } + + // ---------- #153: ISpanFormattable on decimal composites ---------- + + [Fact] + public void DecimalComposite_TryFormat_UsesDecimalsConstant() + { + // DecimalEncoding has Decimals=7 in the edge-cases schema. + var price = new DecimalEncoding { Mantissa = 12_345_678 }; + Span dest = stackalloc char[32]; + + Assert.True(price.TryFormat(dest, out int written, default, null)); + // 12345678 * 1e-7 = 1.2345678 → "F7" → "1.2345678" + Assert.Equal("1.2345678", new string(dest.Slice(0, written))); + } + + [Fact] + public void DecimalComposite_TryFormat_RespectsExplicitFormat() + { + var price = new DecimalEncoding { Mantissa = 12_345_678 }; + Span dest = stackalloc char[32]; + + Assert.True(price.TryFormat(dest, out int written, "F2", null)); + Assert.Equal("1.23", new string(dest.Slice(0, written))); + } + + [Fact] + public void DecimalComposite_ToString_FormatProvider_Works() + { + var price = new DecimalEncoding { Mantissa = 12_345_678 }; + string s = price.ToString(null, System.Globalization.CultureInfo.InvariantCulture); + Assert.Equal("1.2345678", s); + } + + // ---------- #156: MessageHeader static helpers ---------- + + [Fact] + public void MessageHeader_TryReadTemplateId_RoundTripsAfterWriteHeader() + { + Span buffer = stackalloc byte[MessageHeader.MESSAGE_SIZE + TradeData.MESSAGE_SIZE]; + int offset = TradeData.WriteHeader(buffer); + Assert.Equal(MessageHeader.MESSAGE_SIZE, offset); + + Assert.True(MessageHeader.TryReadTemplateId(buffer, out ushort templateId)); + Assert.Equal((ushort)TradeData.MESSAGE_ID, templateId); + } + + [Fact] + public void MessageHeader_TryReadHeader_PopulatesAllFields() + { + Span buffer = stackalloc byte[MessageHeader.MESSAGE_SIZE + TradeData.MESSAGE_SIZE]; + TradeData.WriteHeader(buffer); + + Assert.True(MessageHeader.TryReadHeader(buffer, out ushort blockLength, out ushort templateId, out ushort schemaId, out ushort version)); + Assert.Equal((ushort)TradeData.MESSAGE_SIZE, blockLength); + Assert.Equal((ushort)TradeData.MESSAGE_ID, templateId); + // schemaId/version are non-negative ushorts; just ensure the call succeeded. + Assert.True(schemaId >= 0); + Assert.True(version >= 0); + } + + [Fact] + public void MessageHeader_TryReadHeader_ReturnsFalse_OnShortBuffer() + { + Span tiny = stackalloc byte[MessageHeader.MESSAGE_SIZE - 1]; + Assert.False(MessageHeader.TryReadTemplateId(tiny, out ushort tid)); + Assert.Equal(0, tid); + Assert.False(MessageHeader.TryReadHeader(tiny, out _, out _, out _, out _)); + } + } + + file static class Unsafe + { + public static ref TTo As(ref TFrom source) where TFrom : struct where TTo : struct + => ref System.Runtime.CompilerServices.Unsafe.As(ref source); + } +} diff --git a/tests/SbeCodeGenerator.Tests/Snapshots/TypesCodeGenerator.Composite.MessageHeader.verified.txt b/tests/SbeCodeGenerator.Tests/Snapshots/TypesCodeGenerator.Composite.MessageHeader.verified.txt index 68e73cc..57a5130 100644 --- a/tests/SbeCodeGenerator.Tests/Snapshots/TypesCodeGenerator.Composite.MessageHeader.verified.txt +++ b/tests/SbeCodeGenerator.Tests/Snapshots/TypesCodeGenerator.Composite.MessageHeader.verified.txt @@ -41,4 +41,35 @@ public partial struct MessageHeader public ushort Version { readonly get => version; set => version = value; } /// Returns a string representation of this struct for debugging. public readonly override string ToString() => $"MessageHeader {{ BlockLength={BlockLength}, TemplateId={TemplateId}, SchemaId={SchemaId}, Version={Version} }}"; + + /// + /// Reads the templateId from a buffer that starts with this header. Returns false if the buffer is too small. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static bool TryReadTemplateId(ReadOnlySpan buffer, out ushort templateId) + { + if (buffer.Length < MESSAGE_SIZE) { templateId = 0; return false; } + templateId = MemoryMarshal.AsRef(buffer).TemplateId; + return true; + } + + /// + /// Reads the four standard header fields (blockLength, templateId, schemaId, version) + /// in a single call. Returns false if the buffer is smaller than the header. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static bool TryReadHeader(ReadOnlySpan buffer, out ushort blockLength, out ushort templateId, out ushort schemaId, out ushort version) + { + if (buffer.Length < MESSAGE_SIZE) + { + blockLength = 0; templateId = 0; schemaId = 0; version = 0; + return false; + } + ref readonly var h = ref MemoryMarshal.AsRef(buffer); + blockLength = h.BlockLength; + templateId = h.TemplateId; + schemaId = h.SchemaId; + version = h.Version; + return true; + } }