From c5e3181962023079a634367e575b98420fbf657d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 6 Apr 2024 14:24:33 +0200 Subject: [PATCH] Add source generator for `ReadOnlySpan` overloads of C `const char *` functions --- SDL3-CS.SourceGeneration/Changes.cs | 33 ++++ .../FriendlyOverloadGenerator.cs | 173 ++++++++++++++++++ SDL3-CS.SourceGeneration/GeneratedMethod.cs | 19 ++ SDL3-CS.SourceGeneration/Helper.cs | 77 ++++++++ .../Properties/launchSettings.json | 9 + .../SDL3-CS.SourceGeneration.csproj | 25 +++ .../UnfriendlyMethodFinder.cs | 59 ++++++ SDL3-CS.sln | 6 + SDL3-CS/SDL3-CS.csproj | 4 + 9 files changed, 405 insertions(+) create mode 100644 SDL3-CS.SourceGeneration/Changes.cs create mode 100644 SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs create mode 100644 SDL3-CS.SourceGeneration/GeneratedMethod.cs create mode 100644 SDL3-CS.SourceGeneration/Helper.cs create mode 100644 SDL3-CS.SourceGeneration/Properties/launchSettings.json create mode 100644 SDL3-CS.SourceGeneration/SDL3-CS.SourceGeneration.csproj create mode 100644 SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs diff --git a/SDL3-CS.SourceGeneration/Changes.cs b/SDL3-CS.SourceGeneration/Changes.cs new file mode 100644 index 0000000..02a70eb --- /dev/null +++ b/SDL3-CS.SourceGeneration/Changes.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace SDL3.SourceGeneration +{ + [Flags] + public enum Changes + { + None, + + /// + /// Change const char* function parameters to ReadOnlySpan<byte>. + /// + ChangeParamsToReadOnlySpan = 1 << 0, + + /// + /// Change char * or const char * return type to . + /// + ChangeReturnTypeToString = 1 << 1, + + /// + /// Call SDL_free on the returned pointer. + /// + FreeReturnedPointer = 1 << 2, + + /// + /// Remove from method name. + /// + TrimUnsafeFromName = 1 << 3, + } +} diff --git a/SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs b/SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs new file mode 100644 index 0000000..387c1df --- /dev/null +++ b/SDL3-CS.SourceGeneration/FriendlyOverloadGenerator.cs @@ -0,0 +1,173 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SDL3.SourceGeneration +{ + [Generator] + public class FriendlyOverloadGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new UnfriendlyMethodFinder()); + } + + private const string file_header = @"// +#nullable enable + +using System; + +"; + + public void Execute(GeneratorExecutionContext context) + { + var finder = (UnfriendlyMethodFinder)context.SyntaxReceiver!; + foreach (var kvp in finder.Methods) + { + string filename = kvp.Key; + var foundMethods = kvp.Value; + + var result = new StringBuilder(); + result.Append(file_header); + result.Append( + SyntaxFactory.NamespaceDeclaration( + SyntaxFactory.IdentifierName("SDL")) + .WithMembers( + SyntaxFactory.SingletonList( + SyntaxFactory.ClassDeclaration("SDL3") + .WithModifiers( + SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.UnsafeKeyword), + SyntaxFactory.Token(SyntaxKind.PartialKeyword))) + .WithMembers(SyntaxFactory.List(foundMethods.Select(makeFriendlyMethod))))) + .NormalizeWhitespace()); + + context.AddSource(filename, result.ToString()); + } + } + + private static MemberDeclarationSyntax makeFriendlyMethod(GeneratedMethod gm) + { + var returnType = gm.RequiredChanges.HasFlag(Changes.ChangeReturnTypeToString) + ? SyntaxFactory.ParseTypeName("string?") + : gm.NativeMethod.ReturnType; + + var identifier = gm.RequiredChanges.HasFlag(Changes.TrimUnsafeFromName) + ? SyntaxFactory.Identifier(gm.NativeMethod.Identifier.ValueText.Replace(Helper.UnsafePrefix, string.Empty)) + : gm.NativeMethod.Identifier; + + return SyntaxFactory.MethodDeclaration(returnType, identifier) + .WithModifiers( + SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword))) + .WithParameterList( + SyntaxFactory.ParameterList( + SyntaxFactory.SeparatedList(transformParams(gm)))) + .WithBody( + SyntaxFactory.Block( + makeMethodBody(gm))); + } + + private static IEnumerable transformParams(GeneratedMethod gm) + { + foreach (var param in gm.NativeMethod.ParameterList.Parameters) + { + if (param.IsTypeConstCharPtr()) + { + Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToReadOnlySpan)); + yield return param.WithType(SyntaxFactory.ParseTypeName("ReadOnlySpan")) + .WithAttributeLists(SyntaxFactory.List()); + } + else + { + yield return param.WithAttributeLists(SyntaxFactory.List()); // remove [NativeTypeName] + } + } + } + + private const string pointer_suffix = "Ptr"; + + private static StatementSyntax makeMethodBody(GeneratedMethod gm) + { + var expr = makeReturn(gm); + + foreach (var param in gm.NativeMethod.ParameterList.Parameters.Where(p => p.IsTypeConstCharPtr()).Reverse()) + { + Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToReadOnlySpan)); + + expr = SyntaxFactory.FixedStatement( + SyntaxFactory.VariableDeclaration( + SyntaxFactory.ParseTypeName("byte*"), + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.VariableDeclarator( + param.Identifier.ValueText + pointer_suffix) + .WithInitializer( + SyntaxFactory.EqualsValueClause( + SyntaxFactory.IdentifierName(param.Identifier))))), + expr); + } + + return expr; + } + + private static StatementSyntax makeReturn(GeneratedMethod gm) + { + ExpressionSyntax expr; + + if (gm.RequiredChanges.HasFlag(Changes.ChangeReturnTypeToString)) + { + expr = SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName("PtrToStringUTF8")) + .WithArguments(new[] + { + SyntaxFactory.Argument(makeFunctionCall(gm)), + SyntaxFactory.Argument(SyntaxFactory.LiteralExpression( + gm.RequiredChanges.HasFlag(Changes.FreeReturnedPointer) + ? SyntaxKind.TrueLiteralExpression + : SyntaxKind.FalseLiteralExpression)) + } + ); + } + else + { + expr = makeFunctionCall(gm); + } + + if (gm.NativeMethod.ReturnType.IsVoid()) + return SyntaxFactory.ExpressionStatement(expr); + + return SyntaxFactory.ReturnStatement(expr); + } + + private static InvocationExpressionSyntax makeFunctionCall(GeneratedMethod gm) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName(gm.NativeMethod.Identifier)) + .WithArguments(makeArguments(gm)); + } + + private static IEnumerable makeArguments(GeneratedMethod gm) + { + foreach (var param in gm.NativeMethod.ParameterList.Parameters) + { + if (param.IsTypeConstCharPtr()) + { + Debug.Assert(gm.RequiredChanges.HasFlag(Changes.ChangeParamsToReadOnlySpan)); + yield return SyntaxFactory.Argument(SyntaxFactory.IdentifierName(param.Identifier.ValueText + pointer_suffix)); + } + else + { + yield return SyntaxFactory.Argument(SyntaxFactory.IdentifierName(param.Identifier)); + } + } + } + } +} diff --git a/SDL3-CS.SourceGeneration/GeneratedMethod.cs b/SDL3-CS.SourceGeneration/GeneratedMethod.cs new file mode 100644 index 0000000..693d97a --- /dev/null +++ b/SDL3-CS.SourceGeneration/GeneratedMethod.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SDL3.SourceGeneration +{ + public record GeneratedMethod + { + public readonly MethodDeclarationSyntax NativeMethod; + public readonly Changes RequiredChanges; + + public GeneratedMethod(MethodDeclarationSyntax nativeMethod, Changes requiredChanges) + { + NativeMethod = nativeMethod; + RequiredChanges = requiredChanges; + } + } +} diff --git a/SDL3-CS.SourceGeneration/Helper.cs b/SDL3-CS.SourceGeneration/Helper.cs new file mode 100644 index 0000000..39e0fec --- /dev/null +++ b/SDL3-CS.SourceGeneration/Helper.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SDL3.SourceGeneration +{ + public static class Helper + { + public const string UnsafePrefix = "Unsafe_"; + + public static bool IsVoid(this TypeSyntax type) => type is PredefinedTypeSyntax predefined + && predefined.Keyword.IsKind(SyntaxKind.VoidKeyword); + + public static bool IsBytePtr(this TypeSyntax? type) => type is PointerTypeSyntax syntax + && syntax.ElementType is PredefinedTypeSyntax predefined + && predefined.Keyword.IsKind(SyntaxKind.ByteKeyword); + + public static IEnumerable GetReturnAttributes(this MethodDeclarationSyntax method) + { + foreach (var list in method.AttributeLists) + { + if (list.Target?.Identifier.IsKind(SyntaxKind.ReturnKeyword) == true) + { + foreach (var attribute in list.Attributes) + yield return attribute; + } + } + } + + public static IEnumerable GetAttributes(this ParameterSyntax parameter) + { + foreach (var list in parameter.AttributeLists) + { + foreach (var attribute in list.Attributes) + yield return attribute; + } + } + + public static bool IsConstCharPtr(this AttributeSyntax attribute) + { + if (attribute.Name is IdentifierNameSyntax identifier + && identifier.Identifier.ValueText == "NativeTypeName" + && attribute.ArgumentList != null) + { + return attribute.ArgumentList.Arguments.Any(a => a.Expression is LiteralExpressionSyntax literal + && isConstCharPtr(literal.Token.ValueText)); + } + + return false; + } + + private static bool isConstCharPtr(string text) + { + switch (text) + { + case "const char*": + case "const char *": + return true; + + default: + return false; + } + } + + public static bool IsReturnTypeConstCharPtr(this MethodDeclarationSyntax method) => method.GetReturnAttributes().Any(IsConstCharPtr); + + public static bool IsTypeConstCharPtr(this ParameterSyntax parameter) => parameter.GetAttributes().Any(IsConstCharPtr); + + public static InvocationExpressionSyntax WithArguments(this InvocationExpressionSyntax invocationExpression, IEnumerable arguments) + => invocationExpression.WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))); + } +} diff --git a/SDL3-CS.SourceGeneration/Properties/launchSettings.json b/SDL3-CS.SourceGeneration/Properties/launchSettings.json new file mode 100644 index 0000000..76566e2 --- /dev/null +++ b/SDL3-CS.SourceGeneration/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "DebugRoslynSourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../SDL3-CS/SDL3-CS.csproj" + } + } +} diff --git a/SDL3-CS.SourceGeneration/SDL3-CS.SourceGeneration.csproj b/SDL3-CS.SourceGeneration/SDL3-CS.SourceGeneration.csproj new file mode 100644 index 0000000..987a1ca --- /dev/null +++ b/SDL3-CS.SourceGeneration/SDL3-CS.SourceGeneration.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + false + enable + latest + + true + true + + SDL3.SourceGeneration + SDL3.SourceGeneration + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs b/SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs new file mode 100644 index 0000000..56bfa67 --- /dev/null +++ b/SDL3-CS.SourceGeneration/UnfriendlyMethodFinder.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SDL3.SourceGeneration +{ + public class UnfriendlyMethodFinder : ISyntaxReceiver + { + public readonly Dictionary> Methods = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is MethodDeclarationSyntax method) + { + string name = method.Identifier.ValueText; + bool isUnsafe = name.StartsWith($"{Helper.UnsafePrefix}SDL_"); + + if (!name.StartsWith("SDL_") && !isUnsafe) + return; + + if (method.ParameterList.Parameters.Any(p => p.Identifier.IsKind(SyntaxKind.ArgListKeyword))) + return; + + var changes = Changes.None; + + // if the method is not marked unsafe, the `byte*` is not a string. + if (method.ReturnType.IsBytePtr() && isUnsafe) + { + changes |= Changes.ChangeReturnTypeToString | Changes.TrimUnsafeFromName; + + if (!method.IsReturnTypeConstCharPtr()) + changes |= Changes.FreeReturnedPointer; + } + + foreach (var parameter in method.ParameterList.Parameters) + { + if (parameter.IsTypeConstCharPtr()) + changes |= Changes.ChangeParamsToReadOnlySpan; + } + + if (changes != Changes.None) + { + string fileName = Path.GetFileName(method.SyntaxTree.FilePath); + + if (!Methods.TryGetValue(fileName, out var list)) + Methods[fileName] = list = []; + + list.Add(new GeneratedMethod(method, changes)); + } + } + } + } +} diff --git a/SDL3-CS.sln b/SDL3-CS.sln index 45f3332..e3d75c1 100644 --- a/SDL3-CS.sln +++ b/SDL3-CS.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL3-CS", "SDL3-CS\SDL3-CS.csproj", "{3AEE5979-6974-4BD6-9BB1-1409B0BB469B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL3-CS.SourceGeneration", "SDL3-CS.SourceGeneration\SDL3-CS.SourceGeneration.csproj", "{432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,5 +30,9 @@ Global {3AEE5979-6974-4BD6-9BB1-1409B0BB469B}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AEE5979-6974-4BD6-9BB1-1409B0BB469B}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AEE5979-6974-4BD6-9BB1-1409B0BB469B}.Release|Any CPU.Build.0 = Release|Any CPU + {432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SDL3-CS/SDL3-CS.csproj b/SDL3-CS/SDL3-CS.csproj index 6b3d095..9ddb845 100644 --- a/SDL3-CS/SDL3-CS.csproj +++ b/SDL3-CS/SDL3-CS.csproj @@ -8,4 +8,8 @@ $(NoWarn);SYSLIB1054;CA1401 + + + +