vendor in newer version of VersionedSerialization

this is done now to reduce the migration burden in the future when this is made into a nuget package (hopefully)
This commit is contained in:
LukeFZ
2026-03-13 17:34:07 +01:00
parent e51070e726
commit 20f90a0926
27 changed files with 1218 additions and 718 deletions

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Immutable;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
@@ -7,7 +6,9 @@ using VersionedSerialization.Generator.Utils;
namespace VersionedSerialization.Generator.Analyzer;
#pragma warning disable RS1038
[DiagnosticAnalyzer(LanguageNames.CSharp)]
#pragma warning restore RS1038
public class InvalidVersionAnalyzer : DiagnosticAnalyzer
{
private const string Identifier = "VS0001";
@@ -49,7 +50,8 @@ public class InvalidVersionAnalyzer : DiagnosticAnalyzer
foreach (var argument in attribute.NamedArguments)
{
var name = argument.Key;
if (name is Constants.LessThan or Constants.GreaterThan or Constants.EqualTo)
if (name is Constants.LessThan or Constants.GreaterThan or Constants.EqualTo
or Constants.LessThanOrEqual or Constants.GreaterThanOrEqual)
{
var value = (string)argument.Value.Value!;

View File

@@ -7,6 +7,7 @@ public sealed record ObjectSerializationInfo(
string Namespace,
string Name,
bool HasBaseType,
bool IsPublic,
SyntaxKind DefinitionType,
bool CanGenerateSizeMethod,
ImmutableEquatableArray<PropertySerializationInfo> Properties

View File

@@ -16,73 +16,62 @@ public enum PropertyType
Int32,
Int64,
String,
Custom,
NativeInteger,
UNativeInteger,
Bytes
}
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),
PropertyType.Custom => "",
PropertyType.NativeInteger => "NInt",
PropertyType.UNativeInteger => "NUInt",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
extension(PropertyType type)
{
public string GetTypeName()
=> 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),
PropertyType.NativeInteger => "NativeInt",
PropertyType.UNativeInteger => "NativeUInt",
PropertyType.Bytes => "",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
public static bool IsSeperateMethod(this PropertyType type)
=> type switch
{
PropertyType.Boolean => true,
PropertyType.String => true,
PropertyType.Custom => true,
PropertyType.NativeInteger => true,
PropertyType.UNativeInteger => true,
_ => false
};
public bool IsSeperateMethod()
=> type switch
{
PropertyType.Boolean => true,
PropertyType.String => true,
PropertyType.NativeInteger => true,
PropertyType.UNativeInteger => true,
PropertyType.Bytes => true,
_ => false
};
public static int GetTypeSize(this PropertyType type)
=> type switch
{
PropertyType.Unsupported => -1,
PropertyType.None => 0,
PropertyType.UInt8 => 1,
PropertyType.Int8 => 1,
PropertyType.Boolean => 1,
PropertyType.UInt16 => 2,
PropertyType.UInt32 => 4,
PropertyType.UInt64 => 8,
PropertyType.Int16 => 2,
PropertyType.Int32 => 4,
PropertyType.Int64 => 8,
PropertyType.String => -1,
PropertyType.Custom => -1,
PropertyType.NativeInteger => -1,
PropertyType.UNativeInteger => -1,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
public bool IsUnsignedType()
=> type switch
{
PropertyType.UInt8
or PropertyType.UInt16
or PropertyType.UInt32
or PropertyType.UInt64 => true,
_ => false
};
public static bool IsUnsignedType(this PropertyType type)
=> type switch
{
PropertyType.UInt8
or PropertyType.UInt16
or PropertyType.UInt32
or PropertyType.UInt64 => true,
_ => false
};
public bool NeedsAssignmentToMember()
=> type switch
{
PropertyType.Bytes => false,
_ => true
};
}
}

View File

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

View File

@@ -8,114 +8,102 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
using VersionedSerialization.Generator.Models;
using VersionedSerialization.Generator.Utils;
namespace VersionedSerialization.Generator
namespace VersionedSerialization.Generator;
#pragma warning disable RS1038
[Generator]
#pragma warning restore RS1038
public sealed class ObjectSerializationGenerator : IIncrementalGenerator
{
[Generator]
public sealed class ObjectSerializationGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
//Debugger.Launch();
//Debugger.Launch();
var valueProvider = context.SyntaxProvider
.ForAttributeWithMetadataName(Constants.VersionedStructAttribute,
static (node, _) => node is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax,
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));
var valueProvider = context.SyntaxProvider
.ForAttributeWithMetadataName(Constants.VersionedStructAttribute,
static (node, _) => node is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax,
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);
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("using System.Runtime.CompilerServices;");
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 (condition.LessThanOrEqual.HasValue)
versions.Add(condition.LessThanOrEqual.Value);
if (condition.GreaterThanOrEqual.HasValue)
versions.Add(condition.GreaterThanOrEqual.Value);
}
private static void EmitCode(SourceProductionContext sourceProductionContext, ObjectSerializationInfo info)
if (versions.Count > 0)
{
var generator = new CodeGenerator();
generator.AppendLine("#nullable restore");
generator.AppendLine("using VersionedSerialization;");
generator.AppendLine();
generator.EnterScope("file static class Versions");
generator.AppendLine($"namespace {info.Namespace};");
var versions = new HashSet<StructVersion>();
foreach (var condition in info.Properties.SelectMany(static x => x.VersionConditions))
foreach (var version in versions)
{
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();
}
var definitionType = info.DefinitionType switch
{
SyntaxKind.ClassDeclaration => "class",
SyntaxKind.StructDeclaration => "struct",
SyntaxKind.RecordDeclaration => "record",
SyntaxKind.RecordStructDeclaration => "record struct",
_ => throw new IndexOutOfRangeException()
};
generator.EnterScope($"public partial {definitionType} {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};");
generator.LeaveScope();
}
generator.AppendLine("return size;");
generator.AppendLine($"public static readonly StructVersion {GetVersionIdentifier(version)} = \"{version}\";");
}
generator.LeaveScope();
}
private static void GenerateReadMethod(CodeGenerator generator, ObjectSerializationInfo info)
var definitionType = info.DefinitionType switch
{
generator.EnterScope("public void Read<TReader>(ref TReader reader, in StructVersion version = default) where TReader : IReader, allows ref struct");
SyntaxKind.ClassDeclaration => "class",
SyntaxKind.StructDeclaration => "struct",
SyntaxKind.RecordDeclaration => "record",
SyntaxKind.RecordStructDeclaration => "record struct",
_ => throw new IndexOutOfRangeException()
};
var visibility = info.IsPublic ? "public" : "internal";
generator.EnterScope($"{visibility} partial {definitionType} {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("static int IReadable.Size(in StructVersion version, in ReaderConfig config)");
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("base.Read(ref reader, in version);");
generator.AppendLine("size += base.StructSize(in version, in config);");
foreach (var property in info.Properties)
{
@@ -123,245 +111,305 @@ namespace VersionedSerialization.Generator
GenerateVersionCondition(property.VersionConditions, generator);
generator.EnterScope();
generator.AppendLine($"this.{property.Name} = {property.ReadMethod}");
generator.AppendLine($"size += {property.SizeExpression};");
generator.LeaveScope();
}
generator.AppendLine("return size;");
}
generator.LeaveScope();
}
private static void GenerateReadMethod(CodeGenerator generator, ObjectSerializationInfo info)
{
generator.EnterScope("public void Read<TReader>(ref Reader<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(property.Type.NeedsAssignmentToMember()
? $"this.{property.Name} = {property.ReadMethod}"
: property.ReadMethod);
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(condition.IncludingTag == ""
? "&& version.Tag == null"
: $"&& version.Tag == \"{condition.IncludingTag}\"");
if (condition.ExcludingTag != null)
generator.AppendLine(condition.IncludingTag == ""
? "&& version.Tag != null"
: $"&& version.Tag != \"{condition.IncludingTag}\"");
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 versionedStructAttribute = compilation.GetTypeByMetadataName(Constants.VersionedStructAttribute);
var versionConditionAttribute = compilation.GetTypeByMetadataName(Constants.VersionConditionAttribute);
var customSerializationAttribute = compilation.GetTypeByMetadataName(Constants.CustomSerializationAttribute);
var nativeIntegerAttribute = compilation.GetTypeByMetadataName(Constants.NativeIntegerAttribute);
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 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);
string? readMethod = null;
string? sizeExpression = null;
foreach (var attribute in member.GetAttributes())
{
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))
{
typeInfo = (PropertyType.Custom, "", typeInfo.IsArray);
readMethod = (string)attribute.ConstructorArguments[0].Value!;
sizeExpression = (string)attribute.ConstructorArguments[1].Value!;
}
else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, nativeIntegerAttribute))
{
typeInfo = (typeInfo.Type.IsUnsignedType()
? PropertyType.UNativeInteger
: PropertyType.NativeInteger,
typeInfo.ComplexTypeName == ""
? typeInfo.Type.GetTypeName()
: typeInfo.ComplexTypeName,
typeInfo.IsArray);
}
}
canGenerateSizeMethod &= typeInfo.Type != PropertyType.String;
if (readMethod == null)
{
if (typeInfo.Type == PropertyType.None)
{
readMethod = $"reader.ReadVersionedObject<{typeInfo.ComplexTypeName}>(in version);";
}
else
{
readMethod = typeInfo.Type.IsSeperateMethod()
? $"reader.Read{typeInfo.Type.GetTypeName()}();"
: $"reader.ReadPrimitive<{typeInfo.Type.GetTypeName()}>();";
if (typeInfo.ComplexTypeName != "")
readMethod = $"({typeInfo.ComplexTypeName}){readMethod}";
}
}
sizeExpression ??= typeInfo.Type switch
{
PropertyType.None => $"{typeInfo.ComplexTypeName}.Size(in version, is32Bit)",
PropertyType.NativeInteger or PropertyType.UNativeInteger =>
"is32Bit ? sizeof(uint) : sizeof(ulong)",
_ => $"sizeof({typeInfo.Type.GetTypeName()})"
};
properties.Add(new PropertySerializationInfo(
member.Name,
readMethod,
sizeExpression,
typeInfo.Type,
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(),
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);
}
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($"&& version < Versions.{GetVersionIdentifier(condition.LessThan.Value)}");
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.LessThanOrEqual.HasValue)
generator.AppendLine($"&& version <= Versions.{GetVersionIdentifier(condition.LessThanOrEqual.Value)}");
if (condition.GreaterThanOrEqual.HasValue)
generator.AppendLine($"&& version >= Versions.{GetVersionIdentifier(condition.GreaterThanOrEqual.Value)}");
if (condition.IncludingTag != null)
generator.AppendLine(condition.IncludingTag == ""
? "&& version.Tag == null"
: $"&& version.Tag == \"{condition.IncludingTag}\"");
if (condition.ExcludingTag != null)
generator.AppendLine(condition.ExcludingTag == ""
? "&& version.Tag != null"
: $"&& 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 classIsPublic = classSymbol.DeclaredAccessibility == Accessibility.Public;
//var versionedStructAttribute = compilation.GetTypeByMetadataName(Constants.VersionedStructAttribute);
var versionConditionAttribute = compilation.GetTypeByMetadataName(Constants.VersionConditionAttribute);
var nativeIntegerAttribute = compilation.GetTypeByMetadataName(Constants.NativeIntegerAttribute);
var inlineArrayAttribute = compilation.GetTypeByMetadataName(Constants.InlineArrayAttribute);
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 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, inlineArrayAttribute);
string? readMethod = null;
string? sizeExpression = null;
foreach (var attribute in member.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, versionConditionAttribute))
{
StructVersion? lessThan = null,
moreThan = null,
equalTo = null,
lessThanOrEqual = null,
moreThanOrEqual = null;
string? includingTag = null,
excludingTag = null;
foreach (var argument in attribute.NamedArguments)
{
var stringArgument = (string)argument.Value.Value!;
switch (argument.Key)
{
case Constants.LessThan:
lessThan = stringArgument;
break;
case Constants.GreaterThan:
moreThan = stringArgument;
break;
case Constants.EqualTo:
equalTo = stringArgument;
break;
case Constants.LessThanOrEqual:
lessThanOrEqual = stringArgument;
break;
case Constants.GreaterThanOrEqual:
moreThanOrEqual = stringArgument;
break;
case Constants.IncludingTag:
includingTag = stringArgument;
break;
case Constants.ExcludingTag:
excludingTag = stringArgument;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
var condition = new VersionCondition(
lessThan,
moreThan,
equalTo,
lessThanOrEqual,
moreThanOrEqual,
includingTag,
excludingTag);
versionConditions.Add(condition);
}
else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, nativeIntegerAttribute))
{
var nativeIntegerType = typeInfo.Type.IsUnsignedType()
? PropertyType.UNativeInteger
: PropertyType.NativeInteger;
var complexTypeName = typeInfo.ComplexTypeName == ""
? typeInfo.Type.GetTypeName()
: typeInfo.ComplexTypeName;
typeInfo = (nativeIntegerType, complexTypeName, typeInfo.IsArray);
}
}
canGenerateSizeMethod &= typeInfo.Type != PropertyType.String;
if (readMethod == null)
{
if (typeInfo.Type == PropertyType.None)
{
readMethod = $"reader.ReadVersionedObject<{typeInfo.ComplexTypeName}>(in version);";
}
else if (typeInfo.Type == PropertyType.Bytes)
{
readMethod = $"reader.Read<byte>({member.Name});";
}
else
{
readMethod = typeInfo.Type.IsSeperateMethod()
? $"reader.Read{typeInfo.Type.GetTypeName()}();"
: $"reader.ReadPrimitive<{typeInfo.Type.GetTypeName()}>();";
if (typeInfo.ComplexTypeName != "")
readMethod = $"({typeInfo.ComplexTypeName}){readMethod}";
}
}
sizeExpression ??= typeInfo.Type switch
{
PropertyType.None => $"{typeInfo.ComplexTypeName}.StructSize(in version, in config)",
PropertyType.NativeInteger or PropertyType.UNativeInteger =>
"config.Is32Bit ? sizeof(uint) : sizeof(ulong)",
PropertyType.Bytes => $"Unsafe.SizeOf<{typeInfo.ComplexTypeName}>()",
_ => $"sizeof({typeInfo.Type.GetTypeName()})"
};
properties.Add(new PropertySerializationInfo(
member.Name,
readMethod,
sizeExpression,
typeInfo.Type,
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,
classIsPublic,
contextClass.Kind(),
canGenerateSizeMethod,
properties.ToImmutableEquatableArray()
);
}
private static (PropertyType Type, string ComplexTypeName, bool IsArray) ParseType(ITypeSymbol typeSymbol,
INamedTypeSymbol? inlineArraySymbol)
{
switch (typeSymbol)
{
case IArrayTypeSymbol arrayTypeSymbol:
{
var elementType = ParseType(arrayTypeSymbol.ElementType, inlineArraySymbol);
return (elementType.Type, elementType.ComplexTypeName, true);
}
case INamedTypeSymbol { EnumUnderlyingType: not null } namedTypeSymbol:
var res = ParseType(namedTypeSymbol.EnumUnderlyingType, inlineArraySymbol);
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);
foreach (var attribute in typeSymbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, inlineArraySymbol))
return (PropertyType.Bytes, complexType, false);
}
return (PropertyType.None, complexType, false);
}
}

View File

@@ -1,21 +1,11 @@
using System;
namespace VersionedSerialization;
namespace VersionedSerialization.Generator;
public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = null)
public readonly record 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);
@@ -28,15 +18,6 @@ public readonly struct StructVersion(int major = 0, int minor = 0, string? tag =
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}" : "")}";

View File

@@ -1,54 +1,55 @@
using System.Text;
namespace VersionedSerialization.Generator.Utils
namespace VersionedSerialization.Generator.Utils;
public class CodeGenerator
{
public class CodeGenerator
private const string Indent = " ";
private readonly StringBuilder _sb = new();
private string _currentIndent = "";
public void EnterScope(string? header = null)
{
private const string Indent = " ";
private readonly StringBuilder _sb = new();
private string _currentIndent = "";
public void EnterScope(string? header = null)
if (header != null)
{
if (header != null)
{
AppendLine(header);
}
AppendLine("{");
IncreaseIndentation();
AppendLine(header);
}
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();
}
AppendLine("{");
IncreaseIndentation();
}
public void LeaveScope(string suffix = "")
{
DecreaseIndentation();
_sb.Append(_currentIndent);
_sb.Append('}');
_sb.AppendLine(suffix);
}
public void IncreaseIndentation()
{
_currentIndent += Indent;
}
public void DecreaseIndentation()
{
_currentIndent = _currentIndent[..^Indent.Length];
}
public void AppendLine()
{
_sb.AppendLine();
}
public void AppendLine(string text)
{
_sb.Append(_currentIndent);
_sb.AppendLine(text);
}
public override string ToString()
=> _sb.ToString();
}

View File

@@ -6,12 +6,16 @@ public static class Constants
public const string VersionedStructAttribute = $"{AttributeNamespace}.{nameof(VersionedStructAttribute)}";
public const string VersionConditionAttribute = $"{AttributeNamespace}.{nameof(VersionConditionAttribute)}";
public const string CustomSerializationAttribute = $"{AttributeNamespace}.{nameof(CustomSerializationAttribute)}";
public const string NativeIntegerAttribute = $"{AttributeNamespace}.{nameof(NativeIntegerAttribute)}";
public const string InlineArrayAttribute = "System.Runtime.CompilerServices.InlineArrayAttribute";
public const string LessThan = nameof(LessThan);
public const string GreaterThan = nameof(GreaterThan);
public const string EqualTo = nameof(EqualTo);
public const string LessThanOrEqual = nameof(LessThanOrEqual);
public const string GreaterThanOrEqual = nameof(GreaterThanOrEqual);
public const string IncludingTag = nameof(IncludingTag);
public const string ExcludingTag = nameof(ExcludingTag);
}

View File

@@ -2,15 +2,16 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AnalyzerRoslynVersion>4.10</AnalyzerRoslynVersion>
<RoslynApiVersion>4.10.0</RoslynApiVersion>
<LangVersion>latest</LangVersion>
<AnalyzerRoslynVersion>4.14</AnalyzerRoslynVersion>
<RoslynApiVersion>4.14.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">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" PrivateAssets="all" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -1,6 +0,0 @@
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

@@ -1,3 +1,4 @@
namespace VersionedSerialization.Attributes;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class NativeIntegerAttribute : Attribute;

View File

@@ -6,6 +6,10 @@ public class VersionConditionAttribute : Attribute
public string LessThan { get; set; } = "";
public string GreaterThan { get; set; } = "";
public string EqualTo { get; set; } = "";
public string LessThanOrEqual { get; set; } = "";
public string GreaterThanOrEqual { get; set; } = "";
public string IncludingTag { get; set; } = "";
public string ExcludingTag { get; set; } = "";
}

View File

@@ -0,0 +1,3 @@
namespace VersionedSerialization;
public interface INonSeekableReader : IReader;

View File

@@ -2,6 +2,8 @@
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);
public void Read<TReader>(ref Reader<TReader> reader, in StructVersion version = default)
where TReader : IReader, allows ref struct;
public static abstract int Size(in StructVersion version = default, in ReaderConfig config = default);
}

View File

@@ -1,23 +1,12 @@
using System.Collections.Immutable;
using System.Text;
namespace VersionedSerialization;
public interface IReader
{
bool Is32Bit { get; }
string ReadString(int length = -1, Encoding? encoding = null);
ReadOnlySpan<byte> ReadBytes(long length);
bool ReadBoolean();
long ReadNInt();
ulong ReadNUInt();
string ReadString();
ReadOnlySpan<byte> ReadBytes(int length);
T ReadPrimitive<T>() where T : unmanaged;
ImmutableArray<T> ReadPrimitiveArray<T>(long count) where T : unmanaged;
T ReadVersionedObject<T>(in StructVersion version = default) where T : IReadable, new();
ImmutableArray<T> ReadVersionedObjectArray<T>(long count, in StructVersion version = default) where T : IReadable, new();
void Align(int alignment = 0);
void Skip(int count);
void Read<T>(scoped Span<T> dest) where T : unmanaged;
void ReadPrimitive<T>(scoped Span<T> dest) where T : unmanaged;
}

View File

@@ -0,0 +1,7 @@
namespace VersionedSerialization;
public interface ISeekableReader : IReader
{
int Offset { get; set; }
int Length { get; }
}

View File

@@ -0,0 +1,182 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace VersionedSerialization.Impl;
file static class EndianReader
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> src)
{
Debug.Assert(Unsafe.SizeOf<TFrom>() == Unsafe.SizeOf<TTo>());
return Unsafe.BitCast<Span<TFrom>, Span<TTo>>(src);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> src)
{
Debug.Assert(Unsafe.SizeOf<TFrom>() == Unsafe.SizeOf<TTo>());
return Unsafe.BitCast<ReadOnlySpan<TFrom>, ReadOnlySpan<TTo>>(src);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ReverseEndianness<T>(ReadOnlySpan<T> input, Span<T> output)
where T : unmanaged
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
throw new InvalidOperationException();
if (Unsafe.SizeOf<T>() == sizeof(short))
{
var src = Cast<T, short>(input);
var dst = Cast<T, short>(output);
BinaryPrimitives.ReverseEndianness(src, dst);
}
else if (Unsafe.SizeOf<T>() == sizeof(int))
{
var src = Cast<T, int>(input);
var dst = Cast<T, int>(output);
BinaryPrimitives.ReverseEndianness(src, dst);
}
else if (Unsafe.SizeOf<T>() == sizeof(long))
{
var src = Cast<T, long>(input);
var dst = Cast<T, long>(output);
BinaryPrimitives.ReverseEndianness(src, dst);
}
else if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<Int128>())
{
var src = Cast<T, Int128>(input);
var dst = Cast<T, Int128>(output);
BinaryPrimitives.ReverseEndianness(src, dst);
}
Debug.Assert(false, "Failed to reverse endianness for type");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ReadLittleEndian<TReader, TType>(ref TReader reader, scoped Span<TType> dest)
where TReader : IReader, allows ref struct
where TType : unmanaged
{
if (BitConverter.IsLittleEndian || Unsafe.SizeOf<TType>() == sizeof(byte))
{
reader.ReadPrimitive(dest);
}
else
{
var data = reader.ReadBytes(Unsafe.SizeOf<TType>() * dest.Length);
var src = MemoryMarshal.Cast<byte, TType>(data);
ReverseEndianness(src, dest);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ReadBigEndian<TReader, TType>(ref TReader reader, scoped Span<TType> dest)
where TReader : IReader, allows ref struct
where TType : unmanaged
{
if (!BitConverter.IsLittleEndian || Unsafe.SizeOf<TType>() == sizeof(byte))
{
reader.ReadPrimitive(dest);
}
else
{
var data = reader.ReadBytes(Unsafe.SizeOf<TType>() * dest.Length);
var src = MemoryMarshal.Cast<byte, TType>(data);
ReverseEndianness(src, dest);
}
}
}
public ref struct LittleEndianReader<TReader>(TReader impl) : IReader
where TReader : IReader, allows ref struct
{
private TReader _impl = impl;
public string ReadString(int length = -1, Encoding? encoding = null)
=> _impl.ReadString(length, encoding);
public ReadOnlySpan<byte> ReadBytes(long length)
=> _impl.ReadBytes(length);
public void Read<T>(scoped Span<T> dest) where T : unmanaged
=> _impl.Read(dest);
public void ReadPrimitive<T>(scoped Span<T> dest) where T : unmanaged
=> EndianReader.ReadLittleEndian(ref _impl, dest);
}
public ref struct LittleEndianSeekableReader<TReader>(TReader impl) : ISeekableReader
where TReader : ISeekableReader, allows ref struct
{
private TReader _impl = impl;
public int Offset
{
get => _impl.Offset;
set => _impl.Offset = value;
}
public int Length => _impl.Length;
public string ReadString(int length = -1, Encoding? encoding = null)
=> _impl.ReadString(length, encoding);
public ReadOnlySpan<byte> ReadBytes(long length)
=> _impl.ReadBytes(length);
public void Read<T>(scoped Span<T> dest) where T : unmanaged
=> _impl.Read(dest);
public void ReadPrimitive<T>(scoped Span<T> dest) where T : unmanaged
=> EndianReader.ReadLittleEndian(ref _impl, dest);
}
public ref struct BigEndianReader<TReader>(TReader impl) : IReader
where TReader : IReader, allows ref struct
{
private TReader _impl = impl;
public string ReadString(int length = -1, Encoding? encoding = null)
=> _impl.ReadString(length, encoding);
public ReadOnlySpan<byte> ReadBytes(long length)
=> _impl.ReadBytes(length);
public void Read<T>(scoped Span<T> dest) where T : unmanaged
=> _impl.Read(dest);
public void ReadPrimitive<T>(scoped Span<T> dest) where T : unmanaged
=> EndianReader.ReadBigEndian(ref _impl, dest);
}
public ref struct BigEndianSeekableReader<TReader>(TReader impl) : ISeekableReader
where TReader : ISeekableReader, allows ref struct
{
private TReader _impl = impl;
public int Offset
{
get => _impl.Offset;
set => _impl.Offset = value;
}
public int Length => _impl.Length;
public string ReadString(int length = -1, Encoding? encoding = null)
=> _impl.ReadString(length, encoding);
public ReadOnlySpan<byte> ReadBytes(long length)
=> _impl.ReadBytes(length);
public void Read<T>(scoped Span<T> dest) where T : unmanaged
=> _impl.Read(dest);
public void ReadPrimitive<T>(scoped Span<T> dest) where T : unmanaged
=> EndianReader.ReadBigEndian(ref _impl, dest);
}

View File

@@ -0,0 +1,69 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace VersionedSerialization.Impl;
// 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)
: ISeekableReader
{
public int Offset { get; set; } = offset;
public readonly byte Peek => _data[Offset];
public readonly int Length => _data.Length;
private readonly ReadOnlySpan<byte> _data = data;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Read<T>(scoped Span<T> dest) where T : unmanaged
{
var offset = Offset;
var data = MemoryMarshal.Cast<T, byte>(dest);
_data.Slice(offset, data.Length).CopyTo(data);
Offset = offset + data.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReadPrimitive<T>(scoped Span<T> dest) where T : unmanaged
=> Read(dest);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> ReadBytes(long length)
{
var intLength = checked((int)length);
var offset = Offset;
var val = _data.Slice(offset, intLength);
Offset = offset + intLength;
return val;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadString(int length = -1, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
if (encoding is not (UTF8Encoding or ASCIIEncoding))
ThrowUnsupportedEncodingException();
var offset = Offset;
if (length == -1)
{
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);
var str = encoding.GetString(val);
Offset = offset + length + 1; // Skip null terminator
return str;
}
[DoesNotReturn]
private static void ThrowUnsupportedEncodingException() =>
throw new InvalidOperationException("Unsupported encoding: Only ASCII/UTF8 is currently supported.");
}

View File

@@ -0,0 +1,26 @@
namespace VersionedSerialization;
public static class ReadableExtensions
{
extension<TReadable>(TReadable)
where TReadable : IReadable, new()
{
public static int StructSize(in StructVersion version = default, in ReaderConfig config = default)
=> TReadable.Size(version, config);
public static TReadable FromBytes(ReadOnlySpan<byte> data, bool littleEndian = true, ReaderConfig config = default,
in StructVersion version = default)
{
if (littleEndian)
{
var reader = Reader.LittleEndian(data, config: config);
return reader.ReadVersionedObject<TReadable>(version);
}
else
{
var reader = Reader.BigEndian(data, config: config);
return reader.ReadVersionedObject<TReadable>(version);
}
}
}
}

View File

@@ -0,0 +1,99 @@
using System.Collections.Immutable;
using VersionedSerialization.Impl;
namespace VersionedSerialization;
public static class Reader
{
public static Reader<T> From<T>(T reader, ReaderConfig config = default) where T : IReader, allows ref struct
=> new(reader, config);
public static Reader<LittleEndianSeekableReader<SpanReader>> LittleEndian(ReadOnlySpan<byte> data, int offset = 0,
ReaderConfig config = default)
=> new(new SpanReader(data, offset).AsLittleEndian(), config);
public static Reader<BigEndianSeekableReader<SpanReader>> BigEndian(ReadOnlySpan<byte> data, int offset = 0,
ReaderConfig config = default)
=> new(new SpanReader(data, offset).AsBigEndian(), config);
public static T Read<T>(ReadOnlySpan<byte> data, int offset = 0, bool littleEndian = true,
ReaderConfig config = default)
where T : unmanaged
{
if (littleEndian)
{
var reader = LittleEndian(data, offset, config);
return reader.Read<T>();
}
else
{
var reader = BigEndian(data, offset, config);
return reader.Read<T>();
}
}
public static T ReadPrimitive<T>(ReadOnlySpan<byte> data, int offset = 0, bool littleEndian = true,
ReaderConfig config = default)
where T : unmanaged
{
if (littleEndian)
{
var reader = LittleEndian(data, offset, config);
return reader.ReadPrimitive<T>();
}
else
{
var reader = BigEndian(data, offset, config);
return reader.ReadPrimitive<T>();
}
}
public static ImmutableArray<T> ReadPrimitiveArray<T>(ReadOnlySpan<byte> data, long count, int offset = 0, bool littleEndian = true,
ReaderConfig config = default)
where T : unmanaged
{
if (littleEndian)
{
var reader = LittleEndian(data, offset, config);
return reader.ReadPrimitiveArray<T>(count);
}
else
{
var reader = BigEndian(data, offset, config);
return reader.ReadPrimitiveArray<T>(count);
}
}
public static T ReadVersionedObject<T>(ReadOnlySpan<byte> data, StructVersion version = default, int offset = 0, bool littleEndian = true,
ReaderConfig config = default)
where T : IReadable, new()
{
if (littleEndian)
{
var reader = LittleEndian(data, offset, config);
return reader.ReadVersionedObject<T>(version);
}
else
{
var reader = BigEndian(data, offset, config);
return reader.ReadVersionedObject<T>(version);
}
}
public static ImmutableArray<T> ReadVersionedObjectArray<T>(ReadOnlySpan<byte> data, long count,
StructVersion version = default, int offset = 0, bool littleEndian = true,
ReaderConfig config = default)
where T : IReadable, new()
{
if (littleEndian)
{
var reader = LittleEndian(data, offset, config);
return reader.ReadVersionedObjectArray<T>(count, version);
}
else
{
var reader = BigEndian(data, offset, config);
return reader.ReadVersionedObjectArray<T>(count, version);
}
}
}

View File

@@ -0,0 +1,3 @@
namespace VersionedSerialization;
public readonly record struct ReaderConfig(bool Is32Bit);

View File

@@ -1,62 +1,77 @@
using System.Runtime.CompilerServices;
using VersionedSerialization.Impl;
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
extension<TReader>(ref Reader<TReader> reader)
where TReader : struct, IReader, allows ref struct
{
var first = reader.ReadPrimitive<byte>();
if ((first & 0b10000000) == 0b00000000)
return first;
if ((first & 0b11000000) == 0b10000000)
return (uint)(((first & ~0b10000000) << 8) | reader.ReadPrimitive<byte>());
if ((first & 0b11100000) == 0b11000000)
return (uint)(((first & ~0b11000000) << 24) | (reader.ReadPrimitive<byte>() << 16) | (reader.ReadPrimitive<byte>() << 8) | reader.ReadPrimitive<byte>());
return first switch
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint ReadCompressedUInt()
{
0b11110000 => reader.ReadPrimitive<uint>(),
0b11111110 => uint.MaxValue - 1,
0b11111111 => uint.MaxValue,
_ => throw new InvalidDataException("Invalid compressed uint")
};
var first = reader.ReadPrimitive<byte>();
if ((first & 0b10000000) == 0b00000000)
return first;
if ((first & 0b11000000) == 0b10000000)
return (uint)(((first & ~0b10000000) << 8) | reader.ReadPrimitive<byte>());
if ((first & 0b11100000) == 0b11000000)
return (uint)(((first & ~0b11000000) << 24) | (reader.ReadPrimitive<byte>() << 16) | (reader.ReadPrimitive<byte>() << 8) | reader.ReadPrimitive<byte>());
return first switch
{
0b11110000 => reader.ReadPrimitive<uint>(),
0b11111110 => uint.MaxValue - 1,
0b11111111 => uint.MaxValue,
_ => throw new InvalidDataException("Invalid compressed uint")
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int ReadCompressedInt()
{
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 ulong ReadSLEB128()
{
var value = 0uL;
var shift = 0;
byte current;
do
{
current = reader.ReadPrimitive<byte>();
value |= (current & 0x7FuL) << shift;
shift += 7;
} while ((current & 0x80) != 0);
if (64 >= shift && (current & 0x40) != 0)
value |= ulong.MaxValue << shift;
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ReadBoolean()
=> reader.ReadPrimitive<byte>() != 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ReadCompressedInt<T>(this ref T reader) where T : struct, IReader, allows ref struct
extension<TReader>(TReader reader) where TReader : INonSeekableReader, 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.ReadPrimitive<byte>();
value |= (current & 0x7FuL) << shift;
shift += 7;
} while ((current & 0x80) != 0);
if (64 >= shift && (current & 0x40) != 0)
value |= ulong.MaxValue << shift;
return value;
public LittleEndianReader<TReader> AsLittleEndian() => new(reader);
public BigEndianReader<TReader> AsBigEndian() => new(reader);
}
}

View File

@@ -0,0 +1,107 @@
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace VersionedSerialization;
public ref struct Reader<TReader>(TReader impl, ReaderConfig config = default)
where TReader : IReader, allows ref struct
{
private TReader _impl = impl;
public ReaderConfig Config { get; } = config;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadString(int length = -1, Encoding? encoding = null)
=> _impl.ReadString(length, encoding);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> ReadBytes(long length)
=> _impl.ReadBytes(length);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Read<T>() where T : unmanaged
{
Unsafe.SkipInit(out T value);
Read(ref value);
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Read<T>(scoped ref T value) where T : unmanaged
=> Read(new Span<T>(ref value));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Read<T>(scoped Span<T> dest) where T : unmanaged
=> _impl.Read(dest);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T ReadPrimitive<T>() where T : unmanaged
{
Unsafe.SkipInit(out T value);
ReadPrimitive(ref value);
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReadPrimitive<T>(scoped ref T value) where T : unmanaged
=> ReadPrimitive(new Span<T>(ref value));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReadPrimitive<T>(scoped Span<T> dest) where T : unmanaged
=> _impl.ReadPrimitive(dest);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> ReadPrimitiveArray<T>(long count) where T : unmanaged
{
var data = GC.AllocateUninitializedArray<T>((int)count);
ReadPrimitive(data);
return ImmutableCollectionsMarshal.AsImmutableArray(data);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T ReadVersionedObject<T>(in StructVersion version = default) where T : IReadable, new()
{
var obj = new T();
ReadVersionedObject(ref obj, version);
return obj;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReadVersionedObject<T>(scoped ref T dest, in StructVersion version = default)
where T : IReadable
{
dest.Read(ref this, in version);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReadVersionedObject<T>(scoped Span<T> dest, in StructVersion version = default)
where T : IReadable
{
for (int i = 0; i < dest.Length; i++)
{
ReadVersionedObject(ref dest[i], version);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> ReadVersionedObjectArray<T>(long count, in StructVersion version = default)
where T : IReadable, new()
{
var array = GC.AllocateUninitializedArray<T>((int)count);
ReadVersionedObject(array, in version);
return ImmutableCollectionsMarshal.AsImmutableArray(array);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong ReadNativeUInt()
=> Config.Is32Bit
? ReadPrimitive<uint>()
: ReadPrimitive<ulong>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long ReadNativeInt()
=> Config.Is32Bit
? ReadPrimitive<int>()
: ReadPrimitive<long>();
}

View File

@@ -0,0 +1,63 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using VersionedSerialization.Impl;
namespace VersionedSerialization;
file static class ReaderAccessors<TReader>
where TReader : IReader, allows ref struct
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_impl")]
public static extern ref TReader GetImpl(ref Reader<TReader> obj);
}
public static class SeekableReaderExtensions
{
extension<TReader>(ref Reader<TReader> reader)
where TReader : ISeekableReader, allows ref struct
{
public int Offset
{
get => ReaderAccessors<TReader>.GetImpl(ref reader).Offset;
set => ReaderAccessors<TReader>.GetImpl(ref reader).Offset = value;
}
public int Length => ReaderAccessors<TReader>.GetImpl(ref reader).Length;
public void Align(int alignment = 0)
{
if (alignment == 0)
{
alignment = reader.Config.Is32Bit
? sizeof(int)
: sizeof(long);
}
if (BitOperations.IsPow2(alignment))
{
reader.Offset = (reader.Offset + (alignment - 1)) & ~(alignment - 1);
}
else
{
var offset = reader.Offset;
var rem = offset % alignment;
if (rem != 0)
{
reader.Offset += alignment - rem;
}
}
}
public void Skip(int count)
{
reader.Offset += count;
}
}
extension<TReader>(TReader reader) where TReader : ISeekableReader, allows ref struct
{
public LittleEndianSeekableReader<TReader> AsLittleEndian() => new(reader);
public BigEndianSeekableReader<TReader> AsBigEndian() => new(reader);
}
}

View File

@@ -1,151 +0,0 @@
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 ReadPrimitive<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> ReadPrimitiveArray<T>(long count) where T : unmanaged
{
var array = ImmutableArray.CreateBuilder<T>(checked((int)count));
for (long i = 0; i < count; i++)
array.Add(ReadPrimitive<T>());
return array.MoveToImmutable();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T ReadVersionedObject<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> ReadVersionedObjectArray<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(ReadVersionedObject<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() => ReadPrimitive<byte>() != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong ReadNUInt() => _is32Bit ? ReadPrimitive<uint>() : ReadPrimitive<ulong>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long ReadNInt() => _is32Bit ? ReadPrimitive<int>() : ReadPrimitive<long>();
public void Align(int alignment = 0)
{
if (alignment == 0)
alignment = Is32Bit ? 4 : 8;
var rem = Offset % alignment;
if (rem != 0)
Offset += alignment - rem;
}
public void Skip(int count)
{
Offset += count;
}
}

View File

@@ -1,6 +1,10 @@
namespace VersionedSerialization;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = null) : IEquatable<StructVersion>
namespace VersionedSerialization;
public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = null)
: IEquatable<StructVersion>, IParsable<StructVersion>
{
public readonly int Major = major;
public readonly int Minor = minor;
@@ -8,6 +12,63 @@ public readonly struct StructVersion(int major = 0, int minor = 0, string? tag =
public double AsDouble => Major + Minor / 10.0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasTag(string tag)
=> Tag != null && Tag.Contains(tag, StringComparison.Ordinal);
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out StructVersion result)
{
if (s == null)
{
result = default;
return false;
}
var versionParts = s.Split('.');
if (versionParts.Length > 2)
{
result = default;
return false;
}
if (versionParts.Length == 1)
{
if (!int.TryParse(versionParts[0], out var version))
{
result = default;
return false;
}
result = new StructVersion(version);
return true;
}
var tagParts = versionParts[1].Split("-");
if (tagParts.Length > 2)
{
result = default;
return false;
}
var major = int.Parse(versionParts[0]);
var minor = int.Parse(tagParts[0]);
var tag = tagParts.ElementAtOrDefault(1);
result = new StructVersion(major, minor, tag);
return true;
}
public static StructVersion Parse(string s, IFormatProvider? provider = null) =>
TryParse(s, provider, out var version)
? version
: throw new InvalidOperationException($"Failed to parse {s} as a StructVersion.");
public static implicit operator StructVersion(string value)
=> Parse(value);
public static implicit operator StructVersion(double value)
=> new((int)value, (int)((value - (int)value) * 10.0));
#region Equality operators
public static bool operator ==(StructVersion left, StructVersion right)
@@ -40,29 +101,4 @@ public readonly struct StructVersion(int major = 0, int minor = 0, string? tag =
#endregion
public override string ToString() => $"{Major}.{Minor}{(Tag != null ? $"-{Tag}" : "")}";
public static implicit operator StructVersion(string value)
{
var versionParts = value.Split('.');
if (versionParts.Length > 2)
throw new InvalidOperationException("Invalid version string.");
if (versionParts.Length == 1)
{
if (!int.TryParse(versionParts[0], out var version))
throw new InvalidOperationException("Invalid single-number version string.");
return new StructVersion(version);
}
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

@@ -1,16 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Authors>LukeFZ</Authors>
<Description>Library and source generator to allow fast reading of structs with version-specific layouts.</Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/LukeFZ/VersionedSerialization</PackageProjectUrl>
<RepositoryUrl>https://github.com/LukeFZ/VersionedSerialization</RepositoryUrl>
<RepositoryType></RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<VersionPrefix>0.0.12</VersionPrefix>
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<None Include="$(OutputPath)\VersionedSerialization.Generator.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Any'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\VersionedSerialization.Generator\VersionedSerialization.Generator.csproj" Pack="false">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</ProjectReference>
</ItemGroup>
</Project>