Add source generator for `ReadOnlySpan<byte>` overloads of C `const char *` functions

This commit is contained in:
Susko3 2024-04-06 14:24:33 +02:00
parent 75db9b16c1
commit c5e3181962
9 changed files with 405 additions and 0 deletions

View File

@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
/// <summary>
/// Change <c>const char*</c> function parameters to <c>ReadOnlySpan&lt;byte&gt;</c>.
/// </summary>
ChangeParamsToReadOnlySpan = 1 << 0,
/// <summary>
/// Change <c>char *</c> or <c>const char *</c> return type to <see cref="string"/>.
/// </summary>
ChangeReturnTypeToString = 1 << 1,
/// <summary>
/// Call <c>SDL_free</c> on the returned pointer.
/// </summary>
FreeReturnedPointer = 1 << 2,
/// <summary>
/// Remove <see cref="Helper.UnsafePrefix"/> from method name.
/// </summary>
TrimUnsafeFromName = 1 << 3,
}
}

View File

@ -0,0 +1,173 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 = @"// <auto-generated/>
#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<MemberDeclarationSyntax>(
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<ParameterSyntax> 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<byte>"))
.WithAttributeLists(SyntaxFactory.List<AttributeListSyntax>());
}
else
{
yield return param.WithAttributeLists(SyntaxFactory.List<AttributeListSyntax>()); // 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<ArgumentSyntax> 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));
}
}
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<AttributeSyntax> 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<AttributeSyntax> 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<ArgumentSyntax> arguments)
=> invocationExpression.WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments)));
}
}

View File

@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"DebugRoslynSourceGenerator": {
"commandName": "DebugRoslynComponent",
"targetProject": "../SDL3-CS/SDL3-CS.csproj"
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<RootNamespace>SDL3.SourceGeneration</RootNamespace>
<PackageId>SDL3.SourceGeneration</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<string, List<GeneratedMethod>> 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));
}
}
}
}
}

View File

@ -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

View File

@ -8,4 +8,8 @@
<NoWarn>$(NoWarn);SYSLIB1054;CA1401</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SDL3-CS.SourceGeneration\SDL3-CS.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
</Project>