File: JSExportGenerator.cs
Web Access
Project: JSImportGenerator.csproj (Microsoft.Interop.JavaScript.JSImportGenerator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Microsoft.Interop.JavaScript
{
    [Generator]
    public sealed class JSExportGenerator : IIncrementalGenerator
    {
        internal sealed record IncrementalStubGenerationContext(
            JSSignatureContext SignatureContext,
            ContainingSyntaxContext ContainingSyntaxContext,
            MethodSignatureDiagnosticLocations DiagnosticLocation,
            JSExportData JSExportData);

        public static class StepNames
        {
            public const string CalculateStubInformation = nameof(CalculateStubInformation);
            public const string GenerateSingleStub = nameof(GenerateSingleStub);
        }

        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            var assemblyName = context.CompilationProvider.Select(static (compilation, ct) => compilation.AssemblyName);
            var methodsToGenerate = context.SyntaxProvider
                .ForAttributeWithMetadataName(Constants.JSExportAttribute,
                    static (node, ct) => node is MethodDeclarationSyntax,
                    static (context, ct) => new { Syntax = (MethodDeclarationSyntax)context.TargetNode, Symbol = (IMethodSymbol)context.TargetSymbol })
                .Where(static data =>
                    JSInteropDiagnosticsAnalyzer.GetDiagnosticIfInvalidMethodForGeneration(
                        data.Syntax, data.Symbol,
                        GeneratorDiagnostics.InvalidExportAttributedMethodSignature,
                        GeneratorDiagnostics.InvalidExportAttributedMethodContainingTypeMissingModifiers,
                        requiresImplementation: true) is null);

            IncrementalValueProvider<StubEnvironment> stubEnvironment = context.CreateStubEnvironmentProvider();
            IncrementalValuesProvider<(string Source, string Registration, string Attribute)> generateSingleStub = methodsToGenerate
                .Combine(stubEnvironment)
                .Select(static (data, ct) => CalculateStubInformation(data.Left.Syntax, data.Left.Symbol, data.Right, ct))
                .WithTrackingName(StepNames.CalculateStubInformation)
                .Select(static (data, ct) => GenerateSource(data))
                .WithTrackingName(StepNames.GenerateSingleStub);

            context.RegisterSourceOutput(generateSingleStub.Collect().Combine(assemblyName), static (context, data) =>
            {
                if (data.Left.IsEmpty)
                {
                    return;
                }

                var writer = new IndentedTextWriter();
                writer.WriteLine("// <auto-generated/>");
                WriteRegistrationSource(writer, data.Left, data.Right);
                foreach (var generated in data.Left)
                {
                    writer.Write(generated.Source);
                }
                context.AddSource("JSExports.g.cs", writer.ToString());
            });
        }

        private static JSExportData? ProcessJSExportAttribute(AttributeData attrData)
        {
            if (attrData.AttributeClass?.TypeKind is null or TypeKind.Error)
            {
                return null;
            }

            return new JSExportData();
        }

        private static IncrementalStubGenerationContext CalculateStubInformation(
            MethodDeclarationSyntax originalSyntax,
            IMethodSymbol symbol,
            StubEnvironment environment,
            CancellationToken ct)
        {
            ct.ThrowIfCancellationRequested();
            AttributeData? jsExportAttr = null;
            foreach (AttributeData attr in symbol.GetAttributes())
            {
                if (attr.AttributeClass is not null
                    && attr.AttributeClass.ToDisplayString() == Constants.JSExportAttribute)
                {
                    jsExportAttr = attr;
                }
            }

            Debug.Assert(jsExportAttr is not null);

            var locations = new MethodSignatureDiagnosticLocations(originalSyntax);
            var generatorDiagnostics = new GeneratorDiagnosticsBag(new DescriptorProvider(), locations, SR.ResourceManager, typeof(FxResources.Microsoft.Interop.JavaScript.JSImportGenerator.SR));
            JSExportData jsExportData = ProcessJSExportAttribute(jsExportAttr!) ?? new JSExportData();
            var signatureContext = JSSignatureContext.Create(symbol, environment, generatorDiagnostics, ct);
            ContainingSyntaxContext containingTypeContext = originalSyntax.GetContainingSyntaxContext();

            return new IncrementalStubGenerationContext(signatureContext, containingTypeContext, locations, jsExportData);
        }

        private static void WriteRegistrationSource(
            IndentedTextWriter writer,
            ImmutableArray<(string Source, string Registration, string Attribute)> methods,
            string assemblyName)
        {
            const string GeneratedNamespace = "System.Runtime.InteropServices.JavaScript";
            const string InitializerClass = "__GeneratedInitializer";

            writer.WriteLine($"namespace {GeneratedNamespace}");
            using (writer.WriteBlock())
            {
                writer.WriteLine($"[{Constants.CompilerGeneratedAttributeGlobal}]");
                writer.WriteLine($"class {InitializerClass}");
                using (writer.WriteBlock())
                {
                    writer.WriteLine($"[{Constants.ThreadStaticGlobal}]");
                    writer.WriteLine("static bool initialized;");
                    // Preserve the registration entry point even when the application is trimmed.
                    writer.WriteLine($"[{Constants.ModuleInitializerAttributeGlobal}, {Constants.DynamicDependencyAttributeGlobal}({Constants.DynamicallyAccessedMemberTypesGlobal}.PublicMethods | {Constants.DynamicallyAccessedMemberTypesGlobal}.NonPublicMethods, {CodeWriterHelpers.StringLiteral(GeneratedNamespace + "." + InitializerClass)}, {CodeWriterHelpers.StringLiteral(assemblyName)})]");
                    writer.WriteLine("static internal void __TrimmingPreserve_()");
                    using (writer.WriteBlock())
                    {
                    }
                    writer.WriteLine();

                    foreach (var method in methods)
                    {
                        writer.WriteLine($"[{method.Attribute}]");
                    }
                    writer.WriteLine("static void __Register_()");
                    using (writer.WriteBlock())
                    {
                        writer.WriteLine($"if (initialized || {Constants.OSArchitectureGlobal} != {Constants.ArchitectureWasmGlobal})");
                        writer.Indent++;
                        writer.WriteLine("return;");
                        writer.Indent--;
                        writer.WriteLine("initialized = true;");
                        foreach (var method in methods)
                        {
                            writer.WriteLine(method.Registration);
                        }
                    }
                }
            }
        }

        private static (string Source, string Registration, string Attribute) GenerateSource(IncrementalStubGenerationContext incrementalContext)
        {
            var diagnostics = new GeneratorDiagnosticsBag(new DescriptorProvider(), incrementalContext.DiagnosticLocation, SR.ResourceManager, typeof(FxResources.Microsoft.Interop.JavaScript.JSImportGenerator.SR));
            ImmutableArray<TypePositionInfo> signatureElements = incrementalContext.SignatureContext.SignatureContext.ElementTypeInformation;
            ImmutableArray<TypePositionInfo> allElements = signatureElements.Add(new TypePositionInfo(
                new ReferenceTypeInfo(Constants.ExceptionGlobal, Constants.ExceptionGlobal),
                new JSMarshallingInfo(NoMarshallingInfo.Instance, new JSSimpleTypeInfo(KnownManagedType.Exception, Constants.ExceptionGlobal))
                {
                    JSType = System.Runtime.InteropServices.JavaScript.JSTypeFlags.Error,
                })
            {
                InstanceIdentifier = Constants.ArgumentException,
                ManagedIndex = TypePositionInfo.ExceptionIndex,
                NativeIndex = signatureElements.Length,
                RefKind = RefKind.Out,
                IsErrorHandlingPosition = true,
            });

            for (int i = 0; i < allElements.Length; i++)
            {
                if (allElements[i].IsNativeReturnPosition && allElements[i].ManagedType != SpecialTypeInfo.Void)
                {
                    // Passing the return slot by reference preserves partial runtime initialization.
                    allElements = allElements.SetItem(i, allElements[i] with
                    {
                        ManagedIndex = TypePositionInfo.ReturnIndex,
                        NativeIndex = allElements.Length,
                        RefKind = RefKind.Out,
                    });
                }
            }

            var stubGenerator = new UnmanagedToManagedStubGenerator(
                allElements,
                diagnostics,
                new CompositeMarshallingGeneratorResolver(
                    new NoSpanAndTaskMixingResolver(),
                    new JSGeneratorResolver()));

            var writer = new IndentedTextWriter();
            incrementalContext.ContainingSyntaxContext.WriteTo(
                writer,
                (Context: incrementalContext, Generator: stubGenerator),
                static (writer, state) => WriteWrapper(writer, state.Context, state.Generator));

            JSSignatureContext signature = incrementalContext.SignatureContext;
            string signatures = SignatureBindingHelpers.CreateSignaturesArgument(signatureElements, StubCodeContext.DefaultNativeToManagedStub);
            string registration = $"{Constants.JSFunctionSignatureGlobal}.{Constants.BindCSFunctionMethod}({CodeWriterHelpers.StringLiteral(signature.QualifiedMethodName)}, {signature.TypesHash.ToString(CultureInfo.InvariantCulture)}, {signatures});";
            string attribute = $"{Constants.DynamicDependencyAttributeGlobal}({CodeWriterHelpers.StringLiteral(signature.WrapperName)}, {CodeWriterHelpers.StringLiteral(signature.StubTypeFullName)}, {CodeWriterHelpers.StringLiteral(signature.AssemblyName)})";

            return (writer.ToString(), registration, attribute);
        }

        private static void WriteWrapper(
            IndentedTextWriter writer,
            IncrementalStubGenerationContext context,
            UnmanagedToManagedStubGenerator stubGenerator)
        {
            const string InnerWrapperName = "__Stub";
            writer.WriteLine($"[{Constants.DebuggerNonUserCodeAttribute}]");
            writer.WriteLine($"internal static unsafe void {context.SignatureContext.WrapperName}({Constants.JSMarshalerArgumentGlobal}* {Constants.ArgumentsBuffer})");
            using (writer.WriteBlock())
            {
                writer.WriteLine("unsafe");
                using (writer.WriteBlock())
                {
                    WriteWrapperToInnerStubCall(writer, context.SignatureContext.SignatureContext.ElementTypeInformation, InnerWrapperName);
                    GeneratedMethodSignature signature = stubGenerator.GenerateAbiMethodSignatureData();
                    writer.WriteLine($"[{Constants.DebuggerNonUserCodeAttribute}]");
                    writer.WriteLine($"{signature.ReturnType} {InnerWrapperName}{signature.ParameterList}");
                    writer.Write(stubGenerator.GenerateStubBodyForMethod(context.SignatureContext.MethodName));
                }
            }
        }

        private static void WriteWrapperToInnerStubCall(
            IndentedTextWriter writer,
            ImmutableArray<TypePositionInfo> signatureElements,
            string innerWrapperName)
        {
            writer.Write($"{innerWrapperName}(");
            bool hasReturn = true;
            foreach (TypePositionInfo nativeArgument in signatureElements.Where(static element => element.NativeIndex != TypePositionInfo.UnsetIndex).OrderBy(static element => element.NativeIndex))
            {
                if (nativeArgument.IsNativeReturnPosition)
                {
                    hasReturn = nativeArgument.ManagedType != SpecialTypeInfo.Void;
                    continue;
                }
                writer.Write($"{Constants.ArgumentsBuffer}[{nativeArgument.NativeIndex + 2}], ");
            }
            writer.Write(Constants.ArgumentsBuffer);
            if (hasReturn)
            {
                writer.Write($", {Constants.ArgumentsBuffer} + 1");
            }
            writer.WriteLine(");");
        }
    }
}