add VersionedSerialization + source generator
This commit is contained in:
@@ -40,6 +40,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
|
|||||||
.github\workflows\build.yml = .github\workflows\build.yml
|
.github\workflows\build.yml = .github\workflows\build.yml
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{A24D77DA-8A64-4AD3-956A-677A96F20373}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; Shipped analyzer releases
|
||||||
|
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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
|
||||||
|
);
|
||||||
48
VersionedSerialization.Generator/Models/PropertyType.cs
Normal file
48
VersionedSerialization.Generator/Models/PropertyType.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace VersionedSerialization.Generator.Models;
|
||||||
|
|
||||||
|
public sealed record VersionCondition(StructVersion? LessThan, StructVersion? GreaterThan, StructVersion? EqualTo, string? IncludingTag, string? ExcludingTag);
|
||||||
356
VersionedSerialization.Generator/ObjectSerializationGenerator.cs
Normal file
356
VersionedSerialization.Generator/ObjectSerializationGenerator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
VersionedSerialization.Generator/StructVersion.cs
Normal file
71
VersionedSerialization.Generator/StructVersion.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
VersionedSerialization.Generator/Utils/CodeGenerator.cs
Normal file
54
VersionedSerialization.Generator/Utils/CodeGenerator.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
VersionedSerialization.Generator/Utils/Constants.cs
Normal file
17
VersionedSerialization.Generator/Utils/Constants.cs
Normal 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);
|
||||||
|
}
|
||||||
454
VersionedSerialization.Generator/Utils/HashCode.cs
Normal file
454
VersionedSerialization.Generator/Utils/HashCode.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
6
VersionedSerialization/Attributes/AlignedAttribute.cs
Normal file
6
VersionedSerialization/Attributes/AlignedAttribute.cs
Normal 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.
|
||||||
@@ -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.
|
||||||
@@ -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; } = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace VersionedSerialization.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
|
||||||
|
public class VersionedStructAttribute : Attribute;
|
||||||
7
VersionedSerialization/IReadable.cs
Normal file
7
VersionedSerialization/IReadable.cs
Normal 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);
|
||||||
|
}
|
||||||
22
VersionedSerialization/IReader.cs
Normal file
22
VersionedSerialization/IReader.cs
Normal 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);
|
||||||
|
}
|
||||||
62
VersionedSerialization/ReaderExtensions.cs
Normal file
62
VersionedSerialization/ReaderExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
146
VersionedSerialization/SpanReader.cs
Normal file
146
VersionedSerialization/SpanReader.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
VersionedSerialization/StructVersion.cs
Normal file
58
VersionedSerialization/StructVersion.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
VersionedSerialization/VersionedSerialization.csproj
Normal file
10
VersionedSerialization/VersionedSerialization.csproj
Normal 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>
|
||||||
Reference in New Issue
Block a user