Browse Source

Api gen wip

Krzysztof Krysiński 1 year ago
parent
commit
c9cca51347

+ 7 - 0
src/PixiEditor.Extensions.WasmRuntime/ApiFunctionAttribute.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Extensions.WasmRuntime;
+
+[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
+public class ApiFunctionAttribute(string name) : Attribute
+{
+    public string Name { get; } = name;
+}

+ 13 - 0
src/PixiEditor.Extensions.WasmRuntime/ApiGroupHandler.cs

@@ -0,0 +1,13 @@
+using PixiEditor.Extensions.FlyUI.Elements;
+using PixiEditor.Extensions.WasmRuntime.Management;
+
+namespace PixiEditor.Extensions.WasmRuntime;
+
+// This is a "dummy" class, all properties and methods are never actually used or set, it is used to tell code generators the implementation of the API
+// Compiler will convert all functions with [ApiFunction] attribute to an actual WASM linker function
+internal class ApiGroupHandler
+{
+    public ExtensionServices Api { get; }
+    protected LayoutBuilder LayoutBuilder { get; }
+    protected ObjectManager NativeObjectManager { get; }
+}

+ 4 - 2
src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs

@@ -10,7 +10,7 @@ using Wasmtime;
 
 namespace PixiEditor.Extensions.WasmRuntime;
 
-public class WasmExtensionInstance : Extension
+public partial class WasmExtensionInstance : Extension
 {
     public Instance? Instance { get; private set; }
 
@@ -23,6 +23,8 @@ public class WasmExtensionInstance : Extension
     private ObjectManager NativeObjectManager { get; set; }
     private WasmMemoryUtility WasmMemoryUtility { get; set; }
 
+    partial void LinkApiFunctions();
+
     public WasmExtensionInstance(Linker linker, Store store, Module module)
     {
         Linker = linker;
@@ -69,7 +71,7 @@ public class WasmExtensionInstance : Extension
 
     private void DefinePixiEditorApi()
     {
-        Linker.DefineFunction("env", "log_message",(int messageOffset, int messageLength) =>
+        Linker.DefineFunction("env", "log_message", (int messageOffset, int messageLength) =>
         {
             string messageString = WasmMemoryUtility.GetString(messageOffset, messageLength);
             Console.WriteLine(messageString.ReplaceLineEndings());

+ 17 - 0
src/PixiEditor.Extensions.WasmRuntime/WindowingApi.cs

@@ -0,0 +1,17 @@
+using PixiEditor.Extensions.FlyUI.Elements;
+
+namespace PixiEditor.Extensions.WasmRuntime;
+
+internal class WindowingApi : ApiGroupHandler
+{
+    [ApiFunction("create_popup_window")]
+    public int CreatePopupWindow(string title, byte[] bodySpan)
+    {
+        var body = LayoutBuilder.Deserialize(bodySpan, DuplicateResolutionTactic.ThrowException);
+
+        var popupWindow = Api.Windowing.CreatePopupWindow(title, body.BuildNative());
+
+        int handle = NativeObjectManager.AddObject(popupWindow);
+        return handle;
+    }
+}

+ 128 - 0
src/PixiEditor.WasmApi.Gen/ApiGenerator.cs

@@ -0,0 +1,128 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PixiEditor.Api.Gen;
+
+[Generator]
+public class ApiGenerator : IIncrementalGenerator
+{
+    private const string ApiFunctionAttributeName = "ApiFunctionAttribute";
+    public void Initialize(IncrementalGeneratorInitializationContext context)
+    {
+        var methods = context.SyntaxProvider.CreateSyntaxProvider(CouldBeApiImplAsync, GetApiFunctionMethodOrNull)
+            .Where(x => x is not null)
+            .Collect();
+
+        context.RegisterSourceOutput(methods, GenerateLinkerCode);
+    }
+
+    private void GenerateLinkerCode(SourceProductionContext ctx, ImmutableArray<IMethodSymbol?> symbols)
+    {
+        if (symbols.IsDefaultOrEmpty) return;
+
+        List<string> linkingMethodsCode = new List<string>();
+
+        foreach (IMethodSymbol? method in symbols)
+        {
+            if (method == null) continue;
+
+            linkingMethodsCode.Add(GenerateLinkingCodeForMethod(method));
+        }
+
+        ctx.AddSource($"{TargetNamespace}.{TargetClassName}.g.cs", BuildFinalCode(linkingMethodsCode));
+    }
+
+    private string GenerateLinkingCodeForMethod(IMethodSymbol method)
+    {
+        AttributeData importName = method.GetAttributes().First(x => x.NamedArguments[0].Key == "Name");
+        string name = (string)importName.NamedArguments[0].Value.Value;
+
+        ImmutableArray<IParameterSymbol> arguments = method.Parameters;
+
+        List<string> convertedParams = new List<string>();
+        foreach (var argSymbol in arguments)
+        {
+            convertedParams.AddRange(TypeConversionTable.ConvertTypeToFunctionParams(argSymbol));
+        }
+
+        ParameterListSyntax paramList = SyntaxFactory.ParseParameterList(string.Join(",", convertedParams));
+
+        SyntaxList<StatementSyntax> statements = BuildFunctionBody(method, paramList);
+
+        BlockSyntax body = SyntaxFactory.Block(statements);
+
+        var methodExpression = SyntaxFactory.AnonymousMethodExpression(paramList, body);
+
+        SyntaxFactory.ParseStatement($"Linker.DefineFunction(\"env\", {name})
+    }
+
+    private SyntaxList<StatementSyntax> BuildFunctionBody(IMethodSymbol method, ParameterListSyntax paramList)
+    {
+        SyntaxList<StatementSyntax> syntaxes = new SyntaxList<StatementSyntax>();
+        foreach (SyntaxReference? reference in method.DeclaringSyntaxReferences)
+        {
+            if (reference.GetSyntax() is StatementSyntax statementSyntax)
+                syntaxes = syntaxes.Add(statementSyntax);
+        }
+
+        return syntaxes;
+    }
+
+    private bool CouldBeApiImplAsync(SyntaxNode node, CancellationToken cancellation)
+    {
+        if (node is not AttributeSyntax attribute)
+            return false;
+
+        string? name = ExtractName(attribute.Name);
+
+        return name is "ApiFunction" or ApiFunctionAttributeName;
+    }
+
+    private string? ExtractName(NameSyntax? attributeName)
+    {
+        return attributeName switch
+        {
+            SimpleNameSyntax ins => ins.Identifier.Text,
+            QualifiedNameSyntax qns => qns.Right.Identifier.Text,
+            _ => null
+        };
+    }
+
+    private IMethodSymbol? GetApiFunctionMethodOrNull(GeneratorSyntaxContext context, CancellationToken cancelToken)
+    {
+        AttributeSyntax member = (AttributeSyntax)context.Node;
+
+        if (member.Parent?.Parent is not MethodDeclarationSyntax methodDeclarationSyntax)
+            return null;
+
+        var symbol = context.SemanticModel.GetDeclaredSymbol(member, cancelToken);
+
+        if (symbol is IMethodSymbol methodSymbol)
+        {
+            if (methodSymbol.ReceiverType == null)
+                return null;
+
+            return methodSymbol is null || !IsApiFunction(methodSymbol) ? null : methodSymbol;
+        }
+    }
+
+    private bool IsApiFunction(IMethodSymbol methodSymbol)
+    {
+        return methodSymbol.GetAttributes().Any(x => x.AttributeClass is {
+            Name: ApiFunctionAttributeName,
+            ContainingNamespace: {
+                Name: "PixiEditor.Extensions.WasmRuntime",
+                ContainingNamespace.IsGlobalNamespace: true
+        } });
+    }
+}
+
+class ApiFunction
+{
+
+}

+ 26 - 0
src/PixiEditor.WasmApi.Gen/PixiEditor.WasmApi.Gen.csproj

@@ -0,0 +1,26 @@
+<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>PixiEditor.Api.Gen</RootNamespace>
+        <PackageId>PixiEditor.WasmApi.Gen</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.9.2"/>
+        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2"/>
+    </ItemGroup>
+
+
+</Project>

+ 23 - 0
src/PixiEditor.WasmApi.Gen/TypeConversionTable.cs

@@ -0,0 +1,23 @@
+using System;
+using Microsoft.CodeAnalysis;
+
+namespace PixiEditor.Api.Gen;
+
+public static class TypeConversionTable
+{
+    public static string[] ConvertTypeToFunctionParams(IParameterSymbol symbol)
+    {
+        if(IsLengthType(symbol))
+        {
+            return [$"int {symbol.Name}Pointer", $"int {symbol.Name}Length"];
+        }
+
+        return [$"int {symbol.Name}"];
+    }
+
+    private static bool IsLengthType(IParameterSymbol symbol)
+    {
+        return symbol.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase)
+               || symbol.Type.Name.Equals("byte[]", StringComparison.OrdinalIgnoreCase);
+    }
+}

+ 45 - 0
src/PixiEditor.sln

@@ -110,6 +110,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiDocks.Avalonia", "..\..
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Numerics", "PixiEditor.Numerics\PixiEditor.Numerics.csproj", "{FDA53E74-24B0-4304-8BC7-1A580E3CE7B4}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.WasmApi.Gen", "PixiEditor.WasmApi.Gen\PixiEditor.WasmApi.Gen.csproj", "{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -1875,6 +1877,48 @@ Global
 		{FDA53E74-24B0-4304-8BC7-1A580E3CE7B4}.Steam|x64.Build.0 = Debug|Any CPU
 		{FDA53E74-24B0-4304-8BC7-1A580E3CE7B4}.Steam|x86.ActiveCfg = Debug|Any CPU
 		{FDA53E74-24B0-4304-8BC7-1A580E3CE7B4}.Steam|x86.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|x64.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|x86.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevSteam|Any CPU.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevSteam|Any CPU.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevSteam|x86.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevSteam|x86.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX|x64.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.MSIX|x86.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Release|Any CPU.Build.0 = Release|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Release|x64.ActiveCfg = Release|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Release|x64.Build.0 = Release|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Release|x86.ActiveCfg = Release|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Release|x86.Build.0 = Release|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Steam|x64.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Steam|x86.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1921,6 +1965,7 @@ Global
 		{427CE098-4B13-4E46-8C66-D924140B6CAE} = {F1FDFA82-0C74-446A-AD7D-DE17EC2CF1E8}
 		{174E2684-9C49-460C-A9F1-A6920F00C036} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{FDA53E74-24B0-4304-8BC7-1A580E3CE7B4} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}