diff --git a/DisplayCommands/DisplayCommands.csproj b/DisplayCommands/DisplayCommands.csproj index f621f74..eb5123d 100644 --- a/DisplayCommands/DisplayCommands.csproj +++ b/DisplayCommands/DisplayCommands.csproj @@ -22,6 +22,10 @@ + + diff --git a/DisplayCommands/Internals/DisplayConnection.cs b/DisplayCommands/Internals/DisplayConnection.cs index 4505d95..972a5db 100644 --- a/DisplayCommands/Internals/DisplayConnection.cs +++ b/DisplayCommands/Internals/DisplayConnection.cs @@ -14,7 +14,7 @@ internal sealed class DisplayConnection(IOptions options) public ValueTask SendClearAsync() { - var header = new HeaderWindow { Command = DisplayCommand.Clear }; + var header = new HeaderWindow { Command = (ushort)DisplayCommand.Clear }; return SendAsync(header, Memory.Empty); } @@ -23,7 +23,7 @@ internal sealed class DisplayConnection(IOptions options) { var header = new HeaderWindow { - Command = DisplayCommand.Cp437Data, + Command = (ushort)DisplayCommand.Cp437Data, Height = grid.Height, Width = grid.Width, PosX = x, @@ -37,7 +37,7 @@ internal sealed class DisplayConnection(IOptions options) { var header = new HeaderWindow { - Command = DisplayCommand.CharBrightness, + Command = (ushort)DisplayCommand.CharBrightness, PosX = x, PosY = y, Height = luma.Height, @@ -49,7 +49,7 @@ internal sealed class DisplayConnection(IOptions options) public async ValueTask SendBrightnessAsync(byte brightness) { - var header = new HeaderWindow { Command = DisplayCommand.Brightness }; + var header = new HeaderWindow { Command = (ushort)DisplayCommand.Brightness }; var payloadBuffer = _arrayPool.Rent(1); var payload = payloadBuffer.AsMemory(0, 1); @@ -61,13 +61,13 @@ internal sealed class DisplayConnection(IOptions options) public ValueTask SendHardResetAsync() { - var header = new HeaderWindow { Command = DisplayCommand.HardReset }; + var header = new HeaderWindow { Command = (ushort)DisplayCommand.HardReset }; return SendAsync(header, Memory.Empty); } public async ValueTask SendFadeOutAsync(byte loops) { - var header = new HeaderWindow { Command = DisplayCommand.FadeOut }; + var header = new HeaderWindow { Command = (ushort)DisplayCommand.FadeOut }; var payloadBuffer = _arrayPool.Rent(1); var payload = payloadBuffer.AsMemory(0, 1); @@ -81,8 +81,9 @@ internal sealed class DisplayConnection(IOptions options) { var header = new HeaderWindow { - Command = DisplayCommand.BitmapLinearWin, - PosX = x, PosY = y, + Command = (ushort)DisplayCommand.BitmapLinearWin, + PosX = x, + PosY = y, Width = (ushort)(pixels.Width / 8), Height = pixels.Height }; @@ -113,7 +114,6 @@ internal sealed class DisplayConnection(IOptions options) var buffer = _arrayPool.Rent(messageSize); var message = buffer.AsMemory(0, messageSize); - header.ChangeToNetworkOrder(); MemoryMarshal.Write(message.Span, header); payload.CopyTo(message[headerSize..]); diff --git a/DisplayCommands/Internals/HeaderBitmap.cs b/DisplayCommands/Internals/HeaderBitmap.cs index e3bdd43..b733995 100644 --- a/DisplayCommands/Internals/HeaderBitmap.cs +++ b/DisplayCommands/Internals/HeaderBitmap.cs @@ -1,17 +1,19 @@ using System.Runtime.InteropServices; +using EndiannessSourceGenerator; namespace DisplayCommands.Internals; +[StructEndianness(IsLittleEndian = false)] [StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)] -internal struct HeaderBitmap +internal partial struct HeaderBitmap { - public DisplayCommand Command; + private ushort _command; - public ushort Offset; + private ushort _offset; - public ushort Length; + private ushort _length; - public DisplaySubCommand SubCommand; + private ushort _subCommand; - public ushort Reserved; -} \ No newline at end of file + private ushort _reserved; +} diff --git a/DisplayCommands/Internals/HeaderWindow.cs b/DisplayCommands/Internals/HeaderWindow.cs index fcb9176..2be4916 100644 --- a/DisplayCommands/Internals/HeaderWindow.cs +++ b/DisplayCommands/Internals/HeaderWindow.cs @@ -1,29 +1,19 @@ -using System.Buffers.Binary; using System.Runtime.InteropServices; +using EndiannessSourceGenerator; namespace DisplayCommands.Internals; +[StructEndianness(IsLittleEndian = false)] [StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)] -internal struct HeaderWindow +internal partial struct HeaderWindow { - public DisplayCommand Command; + private ushort _command; - public ushort PosX; + private ushort _posX; - public ushort PosY; + private ushort _posY; - public ushort Width; + private ushort _width; - public ushort Height; - - public void ChangeToNetworkOrder() - { - if (!BitConverter.IsLittleEndian) - return; - Command = (DisplayCommand)BinaryPrimitives.ReverseEndianness((ushort)Command); - PosX = BinaryPrimitives.ReverseEndianness(PosX); - PosY = BinaryPrimitives.ReverseEndianness(PosY); - Width = BinaryPrimitives.ReverseEndianness(Width); - Height = BinaryPrimitives.ReverseEndianness(Height); - } + private ushort _height; } diff --git a/EndiannessSourceGenerator/EndiannessGenerator.cs b/EndiannessSourceGenerator/EndiannessGenerator.cs new file mode 100644 index 0000000..65a4be8 --- /dev/null +++ b/EndiannessSourceGenerator/EndiannessGenerator.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace EndiannessSourceGenerator; + +internal class DebugMeException(string message) : Exception(message); + +internal class InvalidUsageException(string message) : Exception(message); + +[Generator] +public class StructEndiannessSourceGenerator : ISourceGenerator +{ + private const string Namespace = "EndiannessSourceGenerator"; + private const string AttributeName = "StructEndiannessAttribute"; + private const string IsLittleEndianProperty = "IsLittleEndian"; + + private const string AttributeSourceCode = + $$""" + // + namespace {{Namespace}} + { + [System.AttributeUsage(System.AttributeTargets.Struct)] + public class {{AttributeName}}: System.Attribute + { + public required bool {{IsLittleEndianProperty}} { get; init; } + } + } + """; + + private const string UsingDeclarations = + """ + using System; + using System.Buffers.Binary; + """; + + public void Initialize(GeneratorInitializationContext context) + { + // Register the attribute source + context.RegisterForPostInitialization(i => { i.AddSource($"{AttributeName}.g.cs", AttributeSourceCode); }); + // context.RegisterForSyntaxNotifications(() => new SyntaxCon); + } + + private readonly SymbolDisplayFormat _namespacedNameFormat = + new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + + // TODO: generate syntax tree with roslyn to get rid of string wrangling and so code is properly formatted + public void Execute(GeneratorExecutionContext context) + { + var treesWithStructsWithAttributes = context.Compilation.SyntaxTrees + .Where(st => st.GetRoot().DescendantNodes() + .OfType() + .Any(p => p.DescendantNodes() + .OfType() + .Any())) + .ToList(); + + foreach (var tree in treesWithStructsWithAttributes) + { + var semanticModel = context.Compilation.GetSemanticModel(tree); + + var structsWithAttributes = tree.GetRoot().DescendantNodes() + .OfType() + .Where(cd => cd.DescendantNodes() + .OfType() + .Any()); + + foreach (var structDeclaration in structsWithAttributes) + { + var foundAttribute = GetEndiannessAttribute(structDeclaration, semanticModel); + // not my type + if (foundAttribute == null) + continue; + + HandleStruct(context, structDeclaration, semanticModel, foundAttribute); + } + } + } + + private static void HandleStruct(GeneratorExecutionContext context, StructDeclarationSyntax structDeclaration, + SemanticModel semanticModel, AttributeSyntax foundAttribute) + { + var isPartial = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); + if (!isPartial) + throw new InvalidUsageException("struct is not marked partial"); + + var accessibilityModifier = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword)) + ? "internal" + : "public"; + + var structType = semanticModel.GetDeclaredSymbol(structDeclaration); + if (structType == null) + throw new DebugMeException("struct type info is null"); + + var structNamespace = structType.ContainingNamespace?.ToDisplayString(); + if (structNamespace == null) + throw new InvalidUsageException("struct has to be contained in a namespace"); + + var structIsLittleEndian = GetStructIsLittleEndian(foundAttribute); + + var generatedCode = new StringBuilder(); + generatedCode.AppendLine(UsingDeclarations); + + generatedCode.AppendLine($"namespace {structNamespace};"); + generatedCode.AppendLine($$"""{{accessibilityModifier}} partial struct {{structType.Name}} {"""); + + var hasProperties = structDeclaration.Members + .Any(m => m.IsKind(SyntaxKind.PropertyDeclaration)); + if (hasProperties) + throw new InvalidUsageException("struct cannot have properties"); + + var fieldDeclarations = structDeclaration.Members + .Where(m => m.IsKind(SyntaxKind.FieldDeclaration)).OfType(); + GenerateStructProperties(generatedCode, fieldDeclarations, semanticModel, structIsLittleEndian); + + generatedCode.AppendLine("}"); // end of struct + + context.AddSource($"{structNamespace}.{structType.Name}.g.cs", + SourceText.From(generatedCode.ToString(), Encoding.UTF8)); + } + + private static void GenerateStructProperties(StringBuilder generatedCode, + IEnumerable fieldDeclarations, SemanticModel semanticModel, bool structIsLittleEndian) + { + foreach (var field in fieldDeclarations) + { + if (!field.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword))) + throw new InvalidUsageException("fields have to be private"); + + var variableDeclaration = field.DescendantNodes() + .OfType() + .FirstOrDefault(); + if (variableDeclaration == null) + throw new DebugMeException("variable declaration of field declaration null"); + + var variableTypeInfo = semanticModel.GetTypeInfo(variableDeclaration.Type).Type; + if (variableTypeInfo == null) + throw new DebugMeException("variable type info of field declaration null"); + + var typeName = variableTypeInfo.ToDisplayString(); + var fieldName = variableDeclaration.Variables.First().Identifier.ToString(); + var propertyName = GeneratePropertyName(fieldName); + + GenerateProperty(generatedCode, typeName, propertyName, structIsLittleEndian, fieldName); + } + } + + private static void GenerateProperty(StringBuilder generatedCode, string typeName, string propertyName, + bool structIsLittleEndian, string fieldName) + { + generatedCode.AppendLine($$"""public {{typeName}} {{propertyName}} {"""); + + var maybeNegator = structIsLittleEndian ? string.Empty : "!"; + var sameEndiannessExpression = $"{maybeNegator}BitConverter.IsLittleEndian"; + + generatedCode.AppendLine($"get => {sameEndiannessExpression}"); + generatedCode.AppendLine($" ? {fieldName}"); + generatedCode.AppendLine($" : BinaryPrimitives.ReverseEndianness({fieldName});"); + + generatedCode.AppendLine($"set => {fieldName} = {sameEndiannessExpression}"); + generatedCode.AppendLine(" ? value"); + generatedCode.AppendLine(" : BinaryPrimitives.ReverseEndianness(value);"); + + generatedCode.AppendLine("}"); // end of property + } + + private static string GeneratePropertyName(string fieldName) + { + var propertyName = fieldName; + if (propertyName.StartsWith("_")) + propertyName = propertyName.Substring(1); + if (!char.IsLetter(propertyName, 0) || char.IsUpper(propertyName, 0)) + throw new InvalidUsageException("field names have to start with a lower case letter"); + propertyName = propertyName.Substring(0, 1).ToUpperInvariant() + + propertyName.Substring(1); + return propertyName; + } + + private static AttributeSyntax? GetEndiannessAttribute(StructDeclarationSyntax structDeclaration, + SemanticModel semanticModel) + { + AttributeSyntax? foundAttribute = null; + foreach (var attributeSyntax in structDeclaration.DescendantNodes().OfType()) + { + var attributeTypeInfo = semanticModel.GetTypeInfo(attributeSyntax).Type; + if (attributeTypeInfo == null) + throw new DebugMeException("attribute type info is null"); + + if (attributeTypeInfo.ContainingNamespace?.Name != Namespace) + continue; + if (attributeTypeInfo.Name != AttributeName) + continue; + + foundAttribute = attributeSyntax; + break; + } + + return foundAttribute; + } + + private static bool GetStructIsLittleEndian(AttributeSyntax foundAttribute) + { + var endiannessArguments = foundAttribute.ArgumentList; + if (endiannessArguments == null) + throw new InvalidUsageException("endianness attribute has no arguments"); + + var isLittleEndianArgumentSyntax = endiannessArguments.Arguments + .FirstOrDefault(argumentSyntax => + argumentSyntax.NameEquals?.Name.Identifier.ToString() == IsLittleEndianProperty); + if (isLittleEndianArgumentSyntax == null) + throw new InvalidUsageException("endianness attribute argument not found"); + + bool? structIsLittleEndian = isLittleEndianArgumentSyntax.Expression.Kind() switch + { + SyntaxKind.FalseLiteralExpression => false, + SyntaxKind.TrueLiteralExpression => true, + SyntaxKind.DefaultLiteralExpression => false, + _ => throw new InvalidUsageException($"{IsLittleEndianProperty} has to be set with a literal") + }; + return structIsLittleEndian.Value; + } +} diff --git a/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj b/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj new file mode 100644 index 0000000..99670d2 --- /dev/null +++ b/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + false + enable + latest + + true + true + EndiannessSourceGenerator + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/EndiannessSourceGenerator/Readme.md b/EndiannessSourceGenerator/Readme.md new file mode 100644 index 0000000..a69efd1 --- /dev/null +++ b/EndiannessSourceGenerator/Readme.md @@ -0,0 +1,5 @@ +# Endianness Source Generator + +When annotating a struct with the `StructEndianness` attribute, this code generator will generate properties for the declared fields. + +Each time a property is read or written, the endianness is converted from runtime endianness to struct endianness or vice-versa. diff --git a/TanksServer.sln b/TanksServer.sln index 0580f66..644ced9 100644 --- a/TanksServer.sln +++ b/TanksServer.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TanksServer", "TanksServer\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisplayCommands", "DisplayCommands\DisplayCommands.csproj", "{B4B43561-7A2C-486B-99F7-E58A67BC370A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EndiannessSourceGenerator", "EndiannessSourceGenerator\EndiannessSourceGenerator.csproj", "{D77FE880-F2B8-43B6-8B33-B6FA089CC337}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {B4B43561-7A2C-486B-99F7-E58A67BC370A}.Debug|Any CPU.Build.0 = Debug|Any CPU {B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.Build.0 = Release|Any CPU + {D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal