add VersionedSerialization + source generator

This commit is contained in:
LukeFZ
2024-08-13 04:27:23 +02:00
parent 30c019c4ef
commit 22ecdc3612
25 changed files with 1585 additions and 0 deletions

View File

@@ -40,6 +40,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\build.yml = .github\workflows\build.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VersionedSerialization", "VersionedSerialization\VersionedSerialization.csproj", "{803C3421-1907-4114-8B6B-F5E1789FD6A6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VersionedSerialization.Generator", "VersionedSerialization.Generator\VersionedSerialization.Generator.csproj", "{6FF1F0C0-374A-4B7E-B173-697605679AF6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -66,6 +70,14 @@ Global
{A24D77DA-8A64-4AD3-956A-677A96F20373}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A24D77DA-8A64-4AD3-956A-677A96F20373}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A24D77DA-8A64-4AD3-956A-677A96F20373}.Release|Any CPU.Build.0 = Release|Any CPU
{803C3421-1907-4114-8B6B-F5E1789FD6A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{803C3421-1907-4114-8B6B-F5E1789FD6A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{803C3421-1907-4114-8B6B-F5E1789FD6A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{803C3421-1907-4114-8B6B-F5E1789FD6A6}.Release|Any CPU.Build.0 = Release|Any CPU
{6FF1F0C0-374A-4B7E-B173-697605679AF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FF1F0C0-374A-4B7E-B173-697605679AF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FF1F0C0-374A-4B7E-B173-697605679AF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FF1F0C0-374A-4B7E-B173-697605679AF6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using VersionedSerialization.Generator.Utils;
namespace VersionedSerialization.Generator.Analyzer;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class InvalidVersionAnalyzer : DiagnosticAnalyzer
{
private const string Identifier = "VS0001";
private const string Category = "Usage";
private const string Title = "Invalid version string in attribute";
private const string MessageFormat = "Invalid version string";
private const string Description =
"The version needs to be specified in the following format: <major>.<minor>. Tags are not supported.";
private static readonly DiagnosticDescriptor Descriptor = new(Identifier, Title, MessageFormat,
Category, DiagnosticSeverity.Error, true, Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Descriptor];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.PropertyDeclaration, SyntaxKind.FieldDeclaration);
}
private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
if (context.ContainingSymbol == null)
return;
var compilation = context.Compilation;
var versionConditionAttribute = compilation.GetTypeByMetadataName(Constants.VersionConditionAttribute);
foreach (var attribute in context.ContainingSymbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, versionConditionAttribute))
{
if (attribute.ApplicationSyntaxReference == null)
continue;
foreach (var argument in attribute.NamedArguments)
{
var name = argument.Key;
if (name is Constants.LessThan or Constants.GreaterThan or Constants.EqualTo)
{
var value = (string)argument.Value.Value!;
if (!StructVersion.TryParse(value, out var ver) || ver.Tag != null)
{
var span = attribute.ApplicationSyntaxReference.Span;
var location = attribute.ApplicationSyntaxReference.SyntaxTree.GetLocation(span);
var diagnostic = Diagnostic.Create(Descriptor, location);
context.ReportDiagnostic(diagnostic);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

View File

@@ -0,0 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
VS0001 | Usage | Error | InvalidVersionAnalyzer

View File

@@ -0,0 +1,13 @@
using VersionedSerialization.Generator.Utils;
namespace VersionedSerialization.Generator.Models;
public sealed record ObjectSerializationInfo(
string Namespace,
string Name,
bool HasBaseType,
bool IsStruct,
bool ShouldGenerateSizeMethod,
bool CanGenerateSizeMethod,
ImmutableEquatableArray<PropertySerializationInfo> Properties
);

View File

@@ -0,0 +1,11 @@
using VersionedSerialization.Generator.Utils;
namespace VersionedSerialization.Generator.Models;
public sealed record PropertySerializationInfo(
string Name,
string ReadMethod,
string SizeExpression,
int Alignment,
ImmutableEquatableArray<VersionCondition> VersionConditions
);

View File

@@ -0,0 +1,48 @@
using System;
namespace VersionedSerialization.Generator.Models;
public enum PropertyType
{
Unsupported = -1,
None,
Boolean,
UInt8,
UInt16,
UInt32,
UInt64,
Int8,
Int16,
Int32,
Int64,
String,
}
public static class PropertyTypeExtensions
{
public static string GetTypeName(this PropertyType type)
=> type switch
{
PropertyType.Unsupported => nameof(PropertyType.Unsupported),
PropertyType.None => nameof(PropertyType.None),
PropertyType.UInt8 => nameof(Byte),
PropertyType.Int8 => nameof(SByte),
PropertyType.Boolean => nameof(PropertyType.Boolean),
PropertyType.UInt16 => nameof(PropertyType.UInt16),
PropertyType.UInt32 => nameof(PropertyType.UInt32),
PropertyType.UInt64 => nameof(PropertyType.UInt64),
PropertyType.Int16 => nameof(PropertyType.Int16),
PropertyType.Int32 => nameof(PropertyType.Int32),
PropertyType.Int64 => nameof(PropertyType.Int64),
PropertyType.String => nameof(String),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
public static bool IsSeperateMethod(this PropertyType type)
=> type switch
{
PropertyType.Boolean => true,
PropertyType.String => true,
_ => false
};
}

View File

@@ -0,0 +1,3 @@
namespace VersionedSerialization.Generator.Models;
public sealed record VersionCondition(StructVersion? LessThan, StructVersion? GreaterThan, StructVersion? EqualTo, string? IncludingTag, string? ExcludingTag);

View File

@@ -0,0 +1,356 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using VersionedSerialization.Generator.Models;
using VersionedSerialization.Generator.Utils;
namespace VersionedSerialization.Generator
{
[Generator]
public sealed class ObjectSerializationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
//Debugger.Launch();
var valueProvider = context.SyntaxProvider
.ForAttributeWithMetadataName(Constants.VersionedStructAttribute,
static (node, _) => node is ClassDeclarationSyntax or StructDeclarationSyntax,
static (context, _) => (ContextClass: (TypeDeclarationSyntax)context.TargetNode, context.SemanticModel))
.Combine(context.CompilationProvider)
.Select(static (tuple, cancellationToken) => ParseSerializationInfo(tuple.Left.ContextClass, tuple.Left.SemanticModel, tuple.Right, cancellationToken))
.WithTrackingName(nameof(ObjectSerializationGenerator));
context.RegisterSourceOutput(valueProvider, EmitCode);
}
private static void EmitCode(SourceProductionContext sourceProductionContext, ObjectSerializationInfo info)
{
var generator = new CodeGenerator();
generator.AppendLine("#nullable restore");
generator.AppendLine("using VersionedSerialization;");
generator.AppendLine();
generator.AppendLine($"namespace {info.Namespace};");
var versions = new HashSet<StructVersion>();
foreach (var condition in info.Properties.SelectMany(static x => x.VersionConditions))
{
if (condition.LessThan.HasValue)
versions.Add(condition.LessThan.Value);
if (condition.GreaterThan.HasValue)
versions.Add(condition.GreaterThan.Value);
if (condition.EqualTo.HasValue)
versions.Add(condition.EqualTo.Value);
}
if (versions.Count > 0)
{
generator.EnterScope("file static class Versions");
foreach (var version in versions)
{
generator.AppendLine($"public static readonly StructVersion {GetVersionIdentifier(version)} = \"{version}\";");
}
generator.LeaveScope();
}
generator.EnterScope($"public partial {(info.IsStruct ? "struct" : "class")} {info.Name} : IReadable");
GenerateReadMethod(generator, info);
generator.AppendLine();
GenerateSizeMethod(generator, info);
generator.LeaveScope();
sourceProductionContext.AddSource($"{info.Namespace}.{info.Name}.g.cs", generator.ToString());
}
private static void GenerateSizeMethod(CodeGenerator generator, ObjectSerializationInfo info)
{
generator.EnterScope("public static int Size(in StructVersion version = default, bool is32Bit = false)");
if (!info.CanGenerateSizeMethod)
{
generator.AppendLine("throw new InvalidOperationException(\"No size can be calculated for this struct.\");");
}
else
{
generator.AppendLine("var size = 0;");
if (info.HasBaseType)
generator.AppendLine("size += base.Size(in version, is32Bit);");
foreach (var property in info.Properties)
{
if (property.VersionConditions.Length > 0)
GenerateVersionCondition(property.VersionConditions, generator);
generator.EnterScope();
generator.AppendLine($"size += {property.SizeExpression};");
if (property.Alignment != 0)
generator.AppendLine($"size += size % {property.Alignment} == 0 ? 0 : {property.Alignment} - (size % {property.Alignment});");
generator.LeaveScope();
}
generator.AppendLine("return size;");
}
generator.LeaveScope();
}
private static void GenerateReadMethod(CodeGenerator generator, ObjectSerializationInfo info)
{
generator.EnterScope("public void Read<TReader>(ref TReader reader, in StructVersion version = default) where TReader : IReader, allows ref struct");
if (info.HasBaseType)
generator.AppendLine("base.Read(ref reader, in version);");
foreach (var property in info.Properties)
{
if (property.VersionConditions.Length > 0)
GenerateVersionCondition(property.VersionConditions, generator);
generator.EnterScope();
generator.AppendLine($"this.{property.Name} = {property.ReadMethod}");
if (property.Alignment != 0)
generator.AppendLine($"reader.Align({property.Alignment});");
generator.LeaveScope();
}
generator.LeaveScope();
}
private static string GetVersionIdentifier(StructVersion version)
=> $"V{version.Major}_{version.Minor}{(version.Tag == null ? "" : $"_{version.Tag}")}";
private static void GenerateVersionCondition(ImmutableEquatableArray<VersionCondition> conditions,
CodeGenerator generator)
{
generator.AppendLine("if (");
generator.IncreaseIndentation();
for (var i = 0; i < conditions.Length; i++)
{
generator.AppendLine("(true");
var condition = conditions[i];
if (condition.LessThan.HasValue)
generator.AppendLine($"&& Versions.{GetVersionIdentifier(condition.LessThan.Value)} >= version");
if (condition.GreaterThan.HasValue)
generator.AppendLine($"&& version >= Versions.{GetVersionIdentifier(condition.GreaterThan.Value)}");
if (condition.EqualTo.HasValue)
generator.AppendLine($"&& version == Versions.{GetVersionIdentifier(condition.EqualTo.Value)}");
if (condition.IncludingTag != null)
generator.AppendLine($"&& version.Tag == \"{condition.IncludingTag}\"");
if (condition.ExcludingTag != null)
generator.AppendLine($"&& version.Tag != \"{condition.ExcludingTag}\"");
generator.AppendLine(")");
if (i != conditions.Length - 1)
generator.AppendLine("||");
}
generator.DecreaseIndentation();
generator.AppendLine(")");
}
private static ObjectSerializationInfo ParseSerializationInfo(TypeDeclarationSyntax contextClass,
SemanticModel model, Compilation compilation,
CancellationToken cancellationToken)
{
var classSymbol = model.GetDeclaredSymbol(contextClass, cancellationToken) ?? throw new InvalidOperationException();
var alignedAttribute = compilation.GetTypeByMetadataName(Constants.AlignedAttribute);
var versionConditionAttribute = compilation.GetTypeByMetadataName(Constants.VersionConditionAttribute);
var customSerializationAttribute = compilation.GetTypeByMetadataName(Constants.CustomSerializationAttribute);
var canGenerateSizeMethod = true;
var properties = new List<PropertySerializationInfo>();
foreach (var member in classSymbol.GetMembers())
{
if (member.IsStatic
|| member is IFieldSymbol { AssociatedSymbol: not null }
|| member is IPropertySymbol { SetMethod: null })
continue;
var alignment = 0;
var versionConditions = new List<VersionCondition>();
ITypeSymbol type;
switch (member)
{
case IFieldSymbol field:
type = field.Type;
break;
case IPropertySymbol property:
type = property.Type;
break;
default:
continue;
}
var typeInfo = ParseType(type);
canGenerateSizeMethod &= typeInfo.Type != PropertyType.String;
string readMethod;
if (typeInfo.Type == PropertyType.None)
{
readMethod = $"reader.ReadObject<{typeInfo.ComplexTypeName}>(in version);";
}
else
{
readMethod = typeInfo.Type.IsSeperateMethod()
? $"reader.Read{typeInfo.Type.GetTypeName()}();"
: $"reader.Read<{typeInfo.Type.GetTypeName()}>();";
if (typeInfo.ComplexTypeName != "")
readMethod = $"({typeInfo.ComplexTypeName}){readMethod}";
}
string sizeExpression;
if (typeInfo.Type == PropertyType.None)
{
sizeExpression = $"{typeInfo.ComplexTypeName}.Size(in version, is32Bit)";
}
else
{
sizeExpression = $"sizeof({typeInfo.Type.GetTypeName()})";
}
foreach (var attribute in member.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, alignedAttribute))
{
alignment = (int)attribute.ConstructorArguments[0].Value!;
}
else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, versionConditionAttribute))
{
StructVersion? lessThan = null,
moreThan = null,
equalTo = null;
string? includingTag = null,
excludingTag = null;
foreach (var argument in attribute.NamedArguments)
{
switch (argument.Key)
{
case Constants.LessThan:
lessThan = (StructVersion)(string)argument.Value.Value!;
break;
case Constants.GreaterThan:
moreThan = (StructVersion)(string)argument.Value.Value!;
break;
case Constants.EqualTo:
equalTo = (StructVersion)(string)argument.Value.Value!;
break;
case Constants.IncludingTag:
includingTag = (string)argument.Value.Value!;
break;
case Constants.ExcludingTag:
excludingTag = (string)argument.Value.Value!;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
versionConditions.Add(new VersionCondition(lessThan, moreThan, equalTo, includingTag, excludingTag));
}
else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, customSerializationAttribute))
{
readMethod = (string)attribute.ConstructorArguments[0].Value!;
sizeExpression = (string)attribute.ConstructorArguments[1].Value!;
}
}
properties.Add(new PropertySerializationInfo(
member.Name,
readMethod,
sizeExpression,
alignment,
versionConditions.ToImmutableEquatableArray()
));
}
var hasBaseType = false;
if (classSymbol.BaseType != null)
{
var objectSymbol = compilation.GetSpecialType(SpecialType.System_Object);
var valueTypeSymbol = compilation.GetSpecialType(SpecialType.System_ValueType);
if (!SymbolEqualityComparer.Default.Equals(objectSymbol, classSymbol.BaseType)
&& !SymbolEqualityComparer.Default.Equals(valueTypeSymbol, classSymbol.BaseType))
hasBaseType = true;
}
return new ObjectSerializationInfo(
classSymbol.ContainingNamespace.ToDisplayString(),
classSymbol.Name,
hasBaseType,
contextClass.Kind() == SyntaxKind.StructDeclaration,
true,
canGenerateSizeMethod,
properties.ToImmutableEquatableArray()
);
}
private static (PropertyType Type, string ComplexTypeName, bool IsArray) ParseType(ITypeSymbol typeSymbol)
{
switch (typeSymbol)
{
case IArrayTypeSymbol arrayTypeSymbol:
{
var elementType = ParseType(arrayTypeSymbol.ElementType);
return (elementType.Type, elementType.ComplexTypeName, true);
}
case INamedTypeSymbol { EnumUnderlyingType: not null } namedTypeSymbol:
var res = ParseType(namedTypeSymbol.EnumUnderlyingType);
return (res.Type, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), false);
}
if (typeSymbol.SpecialType != SpecialType.None)
{
var type = typeSymbol.SpecialType switch
{
SpecialType.System_Boolean => PropertyType.Boolean,
SpecialType.System_Byte => PropertyType.UInt8,
SpecialType.System_UInt16 => PropertyType.UInt16,
SpecialType.System_UInt32 => PropertyType.UInt32,
SpecialType.System_UInt64 => PropertyType.UInt64,
SpecialType.System_SByte => PropertyType.Int8,
SpecialType.System_Int16 => PropertyType.Int16,
SpecialType.System_Int32 => PropertyType.Int32,
SpecialType.System_Int64 => PropertyType.Int64,
SpecialType.System_String => PropertyType.String,
_ => PropertyType.Unsupported
};
return (type, "", false);
}
var complexType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
return (PropertyType.None, complexType, false);
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
namespace VersionedSerialization;
public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = null)
{
public readonly int Major = major;
public readonly int Minor = minor;
public readonly string? Tag = tag;
#region Equality operators
public static bool operator ==(StructVersion left, StructVersion right)
=> left.Major == right.Major && left.Minor == right.Minor;
public static bool operator !=(StructVersion left, StructVersion right)
=> !(left == right);
public static bool operator >(StructVersion left, StructVersion right)
=> left.Major > right.Major || (left.Major == right.Major && left.Minor > right.Minor);
public static bool operator <(StructVersion left, StructVersion right)
=> left.Major < right.Major || (left.Major == right.Major && left.Minor < right.Minor);
public static bool operator >=(StructVersion left, StructVersion right)
=> left.Major > right.Major || (left.Major == right.Major && left.Minor >= right.Minor);
public static bool operator <=(StructVersion left, StructVersion right)
=> left.Major < right.Major || (left.Major == right.Major && left.Minor <= right.Minor);
public override bool Equals(object? obj)
=> obj is StructVersion other && Equals(other);
public bool Equals(StructVersion other)
=> Major == other.Major && Minor == other.Minor;
public override int GetHashCode()
=> HashCode.Combine(Major, Minor, Tag);
#endregion
public override string ToString() => $"{Major}.{Minor}{(Tag != null ? $"-{Tag}" : "")}";
public static bool TryParse(string version, out StructVersion parsed)
{
parsed = default;
var versionParts = version.Split('.');
if (versionParts.Length is 1 or > 2)
return false;
var tagParts = versionParts[1].Split('-');
if (tagParts.Length > 2)
return false;
var major = int.Parse(versionParts[0]);
var minor = int.Parse(tagParts[0]);
var tag = tagParts.Length == 1 ? null : tagParts[1];
parsed = new StructVersion(major, minor, tag);
return true;
}
public static implicit operator StructVersion(string value)
{
if (!TryParse(value, out var ver))
throw new InvalidOperationException("Invalid version string");
return ver;
}
}

View File

@@ -0,0 +1,54 @@
using System.Text;
namespace VersionedSerialization.Generator.Utils
{
public class CodeGenerator
{
private const string Indent = " ";
private readonly StringBuilder _sb = new();
private string _currentIndent = "";
public void EnterScope(string? header = null)
{
if (header != null)
{
AppendLine(header);
}
AppendLine("{");
IncreaseIndentation();
}
public void LeaveScope(string suffix = "")
{
DecreaseIndentation();
AppendLine($"}}{suffix}");
}
public void IncreaseIndentation()
{
_currentIndent += Indent;
}
public void DecreaseIndentation()
{
_currentIndent = _currentIndent.Substring(0, _currentIndent.Length - Indent.Length);
}
public void AppendLine()
{
_sb.AppendLine();
}
public void AppendLine(string text)
{
_sb.AppendLine(_currentIndent + text);
}
public override string ToString()
{
return _sb.ToString();
}
}
}

View File

@@ -0,0 +1,17 @@
namespace VersionedSerialization.Generator.Utils;
public static class Constants
{
private const string AttributeNamespace = "VersionedSerialization.Attributes";
public const string VersionedStructAttribute = $"{AttributeNamespace}.{nameof(VersionedStructAttribute)}";
public const string AlignedAttribute = $"{AttributeNamespace}.{nameof(AlignedAttribute)}";
public const string VersionConditionAttribute = $"{AttributeNamespace}.{nameof(VersionConditionAttribute)}";
public const string CustomSerializationAttribute = $"{AttributeNamespace}.{nameof(CustomSerializationAttribute)}";
public const string LessThan = nameof(LessThan);
public const string GreaterThan = nameof(GreaterThan);
public const string EqualTo = nameof(EqualTo);
public const string IncludingTag = nameof(IncludingTag);
public const string ExcludingTag = nameof(ExcludingTag);
}

View File

@@ -0,0 +1,454 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// NOTE: This code is derived from an implementation originally in dotnet/roslyn-analyzers
// https://github.com/dotnet/roslyn-analyzers/blob/1dc431ec828e9cc816fc69f89ad9b8286e74d707/src/Utilities/Compiler/RoslynHashCode.cs
/*
The xxHash32 implementation is based on the code published by Yann Collet:
https://raw.githubusercontent.com/Cyan4973/xxHash/5c174cfa4e45a42f94082dc0d4539b39696afea1/xxhash.c
xxHash - Fast Hash algorithm
Copyright (C) 2012-2016, Yann Collet
BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
You can contact the author at :
- xxHash homepage: http://www.xxhash.com
- xxHash source repository : https://github.com/Cyan4973/xxHash
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
[SuppressMessage("Design", "CA1066:Implement IEquatable when overriding Object.Equals", Justification = "This type is not equatable.")]
[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "This type is not equatable.")]
[SuppressMessage("Usage", "CA2231:Overload operator equals on overriding value type Equals", Justification = "This type is not equatable.")]
internal struct HashCode
{
private static readonly uint s_seed = GenerateGlobalSeed();
private const uint Prime1 = 2654435761U;
private const uint Prime2 = 2246822519U;
private const uint Prime3 = 3266489917U;
private const uint Prime4 = 668265263U;
private const uint Prime5 = 374761393U;
private uint _v1, _v2, _v3, _v4;
private uint _queue1, _queue2, _queue3;
private uint _length;
private static uint GenerateGlobalSeed()
{
using var randomNumberGenerator = RandomNumberGenerator.Create();
var array = new byte[sizeof(uint)];
randomNumberGenerator.GetBytes(array);
return BitConverter.ToUInt32(array, 0);
}
public static int Combine<T1>(T1 value1)
{
// Provide a way of diffusing bits from something with a limited
// input hash space. For example, many enums only have a few
// possible hashes, only using the bottom few bits of the code. Some
// collections are built on the assumption that hashes are spread
// over a larger space, so diffusing the bits may help the
// collection work more efficiently.
var hc1 = (uint)(value1?.GetHashCode() ?? 0);
var hash = MixEmptyState();
hash += 4;
hash = QueueRound(hash, hc1);
hash = MixFinal(hash);
return (int)hash;
}
public static int Combine<T1, T2>(T1 value1, T2 value2)
{
var hc1 = (uint)(value1?.GetHashCode() ?? 0);
var hc2 = (uint)(value2?.GetHashCode() ?? 0);
var hash = MixEmptyState();
hash += 8;
hash = QueueRound(hash, hc1);
hash = QueueRound(hash, hc2);
hash = MixFinal(hash);
return (int)hash;
}
public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3)
{
var hc1 = (uint)(value1?.GetHashCode() ?? 0);
var hc2 = (uint)(value2?.GetHashCode() ?? 0);
var hc3 = (uint)(value3?.GetHashCode() ?? 0);
var hash = MixEmptyState();
hash += 12;
hash = QueueRound(hash, hc1);
hash = QueueRound(hash, hc2);
hash = QueueRound(hash, hc3);
hash = MixFinal(hash);
return (int)hash;
}
public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4)
{
var hc1 = (uint)(value1?.GetHashCode() ?? 0);
var hc2 = (uint)(value2?.GetHashCode() ?? 0);
var hc3 = (uint)(value3?.GetHashCode() ?? 0);
var hc4 = (uint)(value4?.GetHashCode() ?? 0);
Initialize(out var v1, out var v2, out var v3, out var v4);
v1 = Round(v1, hc1);
v2 = Round(v2, hc2);
v3 = Round(v3, hc3);
v4 = Round(v4, hc4);
var hash = MixState(v1, v2, v3, v4);
hash += 16;
hash = MixFinal(hash);
return (int)hash;
}
public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5)
{
var hc1 = (uint)(value1?.GetHashCode() ?? 0);
var hc2 = (uint)(value2?.GetHashCode() ?? 0);
var hc3 = (uint)(value3?.GetHashCode() ?? 0);
var hc4 = (uint)(value4?.GetHashCode() ?? 0);
var hc5 = (uint)(value5?.GetHashCode() ?? 0);
Initialize(out var v1, out var v2, out var v3, out var v4);
v1 = Round(v1, hc1);
v2 = Round(v2, hc2);
v3 = Round(v3, hc3);
v4 = Round(v4, hc4);
var hash = MixState(v1, v2, v3, v4);
hash += 20;
hash = QueueRound(hash, hc5);
hash = MixFinal(hash);
return (int)hash;
}
public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6)
{
var hc1 = (uint)(value1?.GetHashCode() ?? 0);
var hc2 = (uint)(value2?.GetHashCode() ?? 0);
var hc3 = (uint)(value3?.GetHashCode() ?? 0);
var hc4 = (uint)(value4?.GetHashCode() ?? 0);
var hc5 = (uint)(value5?.GetHashCode() ?? 0);
var hc6 = (uint)(value6?.GetHashCode() ?? 0);
Initialize(out var v1, out var v2, out var v3, out var v4);
v1 = Round(v1, hc1);
v2 = Round(v2, hc2);
v3 = Round(v3, hc3);
v4 = Round(v4, hc4);
var hash = MixState(v1, v2, v3, v4);
hash += 24;
hash = QueueRound(hash, hc5);
hash = QueueRound(hash, hc6);
hash = MixFinal(hash);
return (int)hash;
}
public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7)
{
var hc1 = (uint)(value1?.GetHashCode() ?? 0);
var hc2 = (uint)(value2?.GetHashCode() ?? 0);
var hc3 = (uint)(value3?.GetHashCode() ?? 0);
var hc4 = (uint)(value4?.GetHashCode() ?? 0);
var hc5 = (uint)(value5?.GetHashCode() ?? 0);
var hc6 = (uint)(value6?.GetHashCode() ?? 0);
var hc7 = (uint)(value7?.GetHashCode() ?? 0);
Initialize(out var v1, out var v2, out var v3, out var v4);
v1 = Round(v1, hc1);
v2 = Round(v2, hc2);
v3 = Round(v3, hc3);
v4 = Round(v4, hc4);
var hash = MixState(v1, v2, v3, v4);
hash += 28;
hash = QueueRound(hash, hc5);
hash = QueueRound(hash, hc6);
hash = QueueRound(hash, hc7);
hash = MixFinal(hash);
return (int)hash;
}
public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8)
{
var hc1 = (uint)(value1?.GetHashCode() ?? 0);
var hc2 = (uint)(value2?.GetHashCode() ?? 0);
var hc3 = (uint)(value3?.GetHashCode() ?? 0);
var hc4 = (uint)(value4?.GetHashCode() ?? 0);
var hc5 = (uint)(value5?.GetHashCode() ?? 0);
var hc6 = (uint)(value6?.GetHashCode() ?? 0);
var hc7 = (uint)(value7?.GetHashCode() ?? 0);
var hc8 = (uint)(value8?.GetHashCode() ?? 0);
Initialize(out var v1, out var v2, out var v3, out var v4);
v1 = Round(v1, hc1);
v2 = Round(v2, hc2);
v3 = Round(v3, hc3);
v4 = Round(v4, hc4);
v1 = Round(v1, hc5);
v2 = Round(v2, hc6);
v3 = Round(v3, hc7);
v4 = Round(v4, hc8);
var hash = MixState(v1, v2, v3, v4);
hash += 32;
hash = MixFinal(hash);
return (int)hash;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4)
{
v1 = s_seed + Prime1 + Prime2;
v2 = s_seed + Prime2;
v3 = s_seed;
v4 = s_seed - Prime1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Round(uint hash, uint input)
{
return BitOperations.RotateLeft(hash + (input * Prime2), 13) * Prime1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint QueueRound(uint hash, uint queuedValue)
{
return BitOperations.RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixState(uint v1, uint v2, uint v3, uint v4)
{
return BitOperations.RotateLeft(v1, 1) + BitOperations.RotateLeft(v2, 7) + BitOperations.RotateLeft(v3, 12) + BitOperations.RotateLeft(v4, 18);
}
private static uint MixEmptyState()
{
return s_seed + Prime5;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixFinal(uint hash)
{
hash ^= hash >> 15;
hash *= Prime2;
hash ^= hash >> 13;
hash *= Prime3;
hash ^= hash >> 16;
return hash;
}
public void Add<T>(T value)
{
Add(value?.GetHashCode() ?? 0);
}
public void Add<T>(T value, IEqualityComparer<T>? comparer)
{
Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode()));
}
private void Add(int value)
{
// The original xxHash works as follows:
// 0. Initialize immediately. We can't do this in a struct (no
// default ctor).
// 1. Accumulate blocks of length 16 (4 uints) into 4 accumulators.
// 2. Accumulate remaining blocks of length 4 (1 uint) into the
// hash.
// 3. Accumulate remaining blocks of length 1 into the hash.
// There is no need for #3 as this type only accepts ints. _queue1,
// _queue2 and _queue3 are basically a buffer so that when
// ToHashCode is called we can execute #2 correctly.
// We need to initialize the xxHash32 state (_v1 to _v4) lazily (see
// #0) nd the last place that can be done if you look at the
// original code is just before the first block of 16 bytes is mixed
// in. The xxHash32 state is never used for streams containing fewer
// than 16 bytes.
// To see what's really going on here, have a look at the Combine
// methods.
var val = (uint)value;
// Storing the value of _length locally shaves of quite a few bytes
// in the resulting machine code.
var previousLength = _length++;
var position = previousLength % 4;
// Switch can't be inlined.
if (position == 0)
{
_queue1 = val;
}
else if (position == 1)
{
_queue2 = val;
}
else if (position == 2)
{
_queue3 = val;
}
else // position == 3
{
if (previousLength == 3)
{
Initialize(out _v1, out _v2, out _v3, out _v4);
}
_v1 = Round(_v1, _queue1);
_v2 = Round(_v2, _queue2);
_v3 = Round(_v3, _queue3);
_v4 = Round(_v4, val);
}
}
public int ToHashCode()
{
// Storing the value of _length locally shaves of quite a few bytes
// in the resulting machine code.
var length = _length;
// position refers to the *next* queue position in this method, so
// position == 1 means that _queue1 is populated; _queue2 would have
// been populated on the next call to Add.
var position = length % 4;
// If the length is less than 4, _v1 to _v4 don't contain anything
// yet. xxHash32 treats this differently.
var hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4);
// _length is incremented once per Add(Int32) and is therefore 4
// times too small (xxHash length is in bytes, not ints).
hash += length * 4;
// Mix what remains in the queue
// Switch can't be inlined right now, so use as few branches as
// possible by manually excluding impossible scenarios (position > 1
// is always false if position is not > 0).
if (position > 0)
{
hash = QueueRound(hash, _queue1);
if (position > 1)
{
hash = QueueRound(hash, _queue2);
if (position > 2)
{
hash = QueueRound(hash, _queue3);
}
}
}
hash = MixFinal(hash);
return (int)hash;
}
#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member
#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations
// Obsolete member 'memberA' overrides non-obsolete member 'memberB'.
// Disallowing GetHashCode and Equals is by design
// * We decided to not override GetHashCode() to produce the hash code
// as this would be weird, both naming-wise as well as from a
// behavioral standpoint (GetHashCode() should return the object's
// hash code, not the one being computed).
// * Even though ToHashCode() can be called safely multiple times on
// this implementation, it is not part of the contract. If the
// implementation has to change in the future we don't want to worry
// about people who might have incorrectly used this type.
[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => throw new NotSupportedException(SR.HashCode_HashCodeNotSupported);
[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => throw new NotSupportedException(SR.HashCode_EqualityNotSupported);
#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations
#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member
private static class SR
{
public static string HashCode_HashCodeNotSupported = "HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.";
public static string HashCode_EqualityNotSupported = "HashCode is a mutable struct and should not be compared with other HashCodes.";
}
private static class BitOperations
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint RotateLeft(uint value, int offset)
=> (value << offset) | (value >> (32 - offset));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong RotateLeft(ulong value, int offset)
=> (value << offset) | (value >> (64 - offset));
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
namespace VersionedSerialization.Generator.Utils;
[DebuggerDisplay("Length = {Length}")]
[DebuggerTypeProxy(typeof(ImmutableEquatableArray<>.DebugView))]
public sealed class ImmutableEquatableArray<T> :
IEquatable<ImmutableEquatableArray<T>>,
IReadOnlyList<T>,
IList<T>,
IList
where T : IEquatable<T>
{
public static ImmutableEquatableArray<T> Empty { get; } = new([]);
private readonly T[] _values;
public ref readonly T this[int index] => ref _values[index];
public int Length => _values.Length;
private ImmutableEquatableArray(T[] values)
=> _values = values;
public bool Equals(ImmutableEquatableArray<T>? other)
=> ReferenceEquals(this, other) || ((ReadOnlySpan<T>)_values).SequenceEqual(other?._values);
public override bool Equals(object? obj)
=> obj is ImmutableEquatableArray<T> other && Equals(other);
public override int GetHashCode()
{
var hash = 0;
foreach (var value in _values)
{
hash = HashCode.Combine(hash, value.GetHashCode());
}
return hash;
}
public Enumerator GetEnumerator() => new(_values);
public struct Enumerator
{
private readonly T[] _values;
private int _index;
internal Enumerator(T[] values)
{
_values = values;
_index = -1;
}
public bool MoveNext() => ++_index < _values.Length;
public readonly ref T Current => ref _values[_index];
}
[EditorBrowsable(EditorBrowsableState.Never)]
internal static ImmutableEquatableArray<T> UnsafeCreateFromArray(T[] values)
=> new(values);
#region Explicit interface implementations
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
bool ICollection<T>.IsReadOnly => true;
bool IList.IsFixedSize => true;
bool IList.IsReadOnly => true;
T IReadOnlyList<T>.this[int index] => _values[index];
T IList<T>.this[int index] { get => _values[index]; set => throw new InvalidOperationException(); }
object? IList.this[int index] { get => _values[index]; set => throw new InvalidOperationException(); }
void ICollection<T>.CopyTo(T[] array, int arrayIndex) => _values.CopyTo(array, arrayIndex);
void ICollection.CopyTo(Array array, int index) => _values.CopyTo(array, index);
int IList<T>.IndexOf(T item) => _values.AsSpan().IndexOf(item);
int IList.IndexOf(object? value) => ((IList)_values).IndexOf(value);
bool ICollection<T>.Contains(T item) => _values.AsSpan().IndexOf(item) >= 0;
bool IList.Contains(object? value) => ((IList)_values).Contains(value);
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
int IReadOnlyCollection<T>.Count => Length;
int ICollection<T>.Count => Length;
int ICollection.Count => Length;
void ICollection<T>.Add(T item) => throw new InvalidOperationException();
bool ICollection<T>.Remove(T item) => throw new InvalidOperationException();
void ICollection<T>.Clear() => throw new InvalidOperationException();
void IList<T>.Insert(int index, T item) => throw new InvalidOperationException();
void IList<T>.RemoveAt(int index) => throw new InvalidOperationException();
int IList.Add(object? value) => throw new InvalidOperationException();
void IList.Clear() => throw new InvalidOperationException();
void IList.Insert(int index, object? value) => throw new InvalidOperationException();
void IList.Remove(object? value) => throw new InvalidOperationException();
void IList.RemoveAt(int index) => throw new InvalidOperationException();
#endregion
private sealed class DebugView(ImmutableEquatableArray<T> array)
{
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public T[] Items => array.ToArray();
}
}
public static class ImmutableEquatableArray
{
public static ImmutableEquatableArray<T> ToImmutableEquatableArray<T>(this IEnumerable<T> values) where T : IEquatable<T>
=> values is ICollection<T> { Count: 0 } ? ImmutableEquatableArray<T>.Empty : ImmutableEquatableArray<T>.UnsafeCreateFromArray(values.ToArray());
public static ImmutableEquatableArray<T> Create<T>(ReadOnlySpan<T> values) where T : IEquatable<T>
=> values.IsEmpty ? ImmutableEquatableArray<T>.Empty : ImmutableEquatableArray<T>.UnsafeCreateFromArray(values.ToArray());
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<AnalyzerRoslynVersion>4.10</AnalyzerRoslynVersion>
<RoslynApiVersion>4.10.0</RoslynApiVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" PrivateAssets="all" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace VersionedSerialization.Attributes;
#pragma warning disable CS9113 // Parameter is unread.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class AlignedAttribute(int alignment) : Attribute;
#pragma warning restore CS9113 // Parameter is unread.

View File

@@ -0,0 +1,6 @@
namespace VersionedSerialization.Attributes;
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
#pragma warning disable CS9113 // Parameter is unread.
public class CustomSerializationAttribute(string methodName, string sizeExpression) : Attribute;
#pragma warning restore CS9113 // Parameter is unread.

View File

@@ -0,0 +1,11 @@
namespace VersionedSerialization.Attributes;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public class VersionConditionAttribute : Attribute
{
public string LessThan { get; set; } = "";
public string GreaterThan { get; set; } = "";
public string EqualTo { get; set; } = "";
public string IncludingTag { get; set; } = "";
public string ExcludingTag { get; set; } = "";
}

View File

@@ -0,0 +1,4 @@
namespace VersionedSerialization.Attributes;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class VersionedStructAttribute : Attribute;

View File

@@ -0,0 +1,7 @@
namespace VersionedSerialization;
public interface IReadable
{
public void Read<TReader>(ref TReader reader, in StructVersion version = default) where TReader : IReader, allows ref struct;
public static abstract int Size(in StructVersion version = default, bool is32Bit = false);
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Immutable;
namespace VersionedSerialization;
public interface IReader
{
bool Is32Bit { get; }
bool ReadBoolean();
long ReadNInt();
ulong ReadNUInt();
string ReadString();
ReadOnlySpan<byte> ReadBytes(int length);
T Read<T>() where T : unmanaged;
ImmutableArray<T> ReadArray<T>(long count) where T : unmanaged;
T ReadObject<T>(in StructVersion version = default) where T : IReadable, new();
ImmutableArray<T> ReadObjectArray<T>(long count, in StructVersion version = default) where T : IReadable, new();
public void Align(int alignment = 0);
}

View File

@@ -0,0 +1,62 @@
using System.Runtime.CompilerServices;
namespace VersionedSerialization;
public static class ReaderExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint ReadCompressedUInt<T>(this ref T reader) where T : struct, IReader, allows ref struct
{
var first = reader.Read<byte>();
if ((first & 0b10000000) == 0b00000000)
return first;
if ((first & 0b11000000) == 0b10000000)
return (uint)(((first & ~0b10000000) << 8) | reader.Read<byte>());
if ((first & 0b11100000) == 0b11000000)
return (uint)(((first & ~0b11000000) << 24) | (reader.Read<byte>() << 16) | (reader.Read<byte>() << 8) | reader.Read<byte>());
return first switch
{
0b11110000 => reader.Read<uint>(),
0b11111110 => uint.MaxValue - 1,
0b11111111 => uint.MaxValue,
_ => throw new InvalidDataException("Invalid compressed uint")
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ReadCompressedInt<T>(this ref T reader) where T : struct, IReader, allows ref struct
{
var value = reader.ReadCompressedUInt();
if (value == uint.MaxValue)
return int.MinValue;
var isNegative = (value & 0b1) == 1;
value >>= 1;
return (int)(isNegative ? -(value + 1) : value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong ReadSLEB128<T>(this ref T reader) where T : struct, IReader, allows ref struct
{
var value = 0uL;
var shift = 0;
byte current;
do
{
current = reader.Read<byte>();
value |= (current & 0x7FuL) << shift;
shift += 7;
} while ((current & 0x80) != 0);
if (64 >= shift && (current & 0x40) != 0)
value |= ulong.MaxValue << shift;
return value;
}
}

View File

@@ -0,0 +1,146 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace VersionedSerialization;
// ReSharper disable ReplaceSliceWithRangeIndexer | The range indexer gets compiled into .Slice(x, y) and not .Slice(x) which worsens performance
public ref struct SpanReader(ReadOnlySpan<byte> data, int offset = 0, bool littleEndian = true, bool is32Bit = false) : IReader
{
public int Offset = offset;
public readonly byte Peek => _data[Offset];
public readonly bool IsLittleEndian => _littleEndian;
public readonly bool Is32Bit => _is32Bit;
public readonly int Length => _data.Length;
public readonly int PointerSize => Is32Bit ? sizeof(uint) : sizeof(ulong);
private readonly ReadOnlySpan<byte> _data = data;
private readonly bool _littleEndian = littleEndian;
private readonly bool _is32Bit = is32Bit;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private T ReadInternal<T>() where T : unmanaged
{
var value = MemoryMarshal.Read<T>(_data.Slice(Offset));
Offset += Unsafe.SizeOf<T>();
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TTo Cast<TFrom, TTo>(in TFrom from) => Unsafe.As<TFrom, TTo>(ref Unsafe.AsRef(in from));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> ReadBytes(int length)
{
var val = _data.Slice(Offset, length);
Offset += length;
return val;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Read<T>() where T : unmanaged
{
if (typeof(T) == typeof(byte))
return Cast<byte, T>(_data[Offset++]);
var value = ReadInternal<T>();
if (!_littleEndian)
{
if (value is ulong val)
{
var converted = BinaryPrimitives.ReverseEndianness(val);
value = Cast<ulong, T>(converted);
}
else if (typeof(T) == typeof(long))
{
var converted = BinaryPrimitives.ReverseEndianness(Cast<T, long>(value));
value = Cast<long, T>(converted);
}
else if (typeof(T) == typeof(uint))
{
var converted = BinaryPrimitives.ReverseEndianness(Cast<T, uint>(value));
value = Cast<uint, T>(converted);
}
else if (typeof(T) == typeof(int))
{
var converted = BinaryPrimitives.ReverseEndianness(Cast<T, int>(value));
value = Cast<int, T>(converted);
}
else if (typeof(T) == typeof(ushort))
{
var converted = BinaryPrimitives.ReverseEndianness(Cast<T, ushort>(value));
value = Cast<ushort, T>(converted);
}
else if (typeof(T) == typeof(short))
{
var converted = BinaryPrimitives.ReverseEndianness(Cast<T, short>(value));
value = Cast<short, T>(converted);
}
}
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> ReadArray<T>(long count) where T : unmanaged
{
var array = ImmutableArray.CreateBuilder<T>(checked((int)count));
for (long i = 0; i < count; i++)
array.Add(Read<T>());
return array.MoveToImmutable();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T ReadObject<T>(in StructVersion version = default) where T : IReadable, new()
{
var obj = new T();
obj.Read(ref this, in version);
return obj;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> ReadObjectArray<T>(long count, in StructVersion version = default) where T : IReadable, new()
{
var array = ImmutableArray.CreateBuilder<T>(checked((int)count));
for (long i = 0; i < count; i++)
array.Add(ReadObject<T>(in version));
return array.MoveToImmutable();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadString()
{
var length = _data.Slice(Offset).IndexOf(byte.MinValue);
if (length == -1)
throw new InvalidDataException("Failed to find string in span.");
var val = _data.Slice(Offset, length);
Offset += length + 1; // Skip null terminator
return Encoding.UTF8.GetString(val);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ReadBoolean() => Read<byte>() != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong ReadNUInt() => _is32Bit ? Read<uint>() : Read<ulong>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long ReadNInt() => _is32Bit ? Read<int>() : Read<long>();
public void Align(int alignment = 0)
{
if (alignment == 0)
alignment = Is32Bit ? 4 : 8;
var rem = Offset % alignment;
if (rem != 0)
Offset += alignment - rem;
}
}

View File

@@ -0,0 +1,58 @@
namespace VersionedSerialization;
public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = null) : IEquatable<StructVersion>
{
public readonly int Major = major;
public readonly int Minor = minor;
public readonly string? Tag = tag;
#region Equality operators
public static bool operator ==(StructVersion left, StructVersion right)
=> left.Major == right.Major && left.Minor == right.Minor;
public static bool operator !=(StructVersion left, StructVersion right)
=> !(left == right);
public static bool operator >(StructVersion left, StructVersion right)
=> left.Major > right.Major || (left.Major == right.Major && left.Minor > right.Minor);
public static bool operator <(StructVersion left, StructVersion right)
=> left.Major < right.Major || (left.Major == right.Major && left.Minor < right.Minor);
public static bool operator >=(StructVersion left, StructVersion right)
=> left.Major > right.Major || (left.Major == right.Major && left.Minor >= right.Minor);
public static bool operator <=(StructVersion left, StructVersion right)
=> left.Major < right.Major || (left.Major == right.Major && left.Minor <= right.Minor);
public override bool Equals(object? obj)
=> obj is StructVersion other && Equals(other);
public bool Equals(StructVersion other)
=> Major == other.Major && Minor == other.Minor;
public override int GetHashCode()
=> HashCode.Combine(Major, Minor);
#endregion
public override string ToString() => $"{Major}.{Minor}{(Tag != null ? $"-{Tag}" : "")}";
public static implicit operator StructVersion(string value)
{
var versionParts = value.Split('.');
if (versionParts.Length is 1 or > 2)
throw new InvalidOperationException("Invalid version string.");
var tagParts = versionParts[1].Split("-");
if (tagParts.Length > 2)
throw new InvalidOperationException("Invalid version string.");
var major = int.Parse(versionParts[0]);
var minor = int.Parse(tagParts[0]);
var tag = tagParts.Length == 1 ? null : tagParts[1];
return new StructVersion(major, minor, tag);
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>