mirror of https://github.com/ppy/SDL3-CS.git
Add source generator for `ReadOnlySpan<byte>` overloads of C `const char *` functions
This commit is contained in:
parent
75db9b16c1
commit
c5e3181962
|
|
@ -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<byte></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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"DebugRoslynSourceGenerator": {
|
||||
"commandName": "DebugRoslynComponent",
|
||||
"targetProject": "../SDL3-CS/SDL3-CS.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue