diff --git a/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets b/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets index 2dfc975f16..668ea155a0 100644 --- a/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets +++ b/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets @@ -95,6 +95,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. $(_CsWinRTToolsDirectory) $(_CsWinRTToolsDirectory) $(_CsWinRTToolsDirectory) + $(_CsWinRTToolsDirectory) $(CsWinRTInteropGenEffectiveToolsDirectory) diff --git a/nuget/Microsoft.Windows.CsWinRT.nuspec b/nuget/Microsoft.Windows.CsWinRT.nuspec index 1b9f18d74a..999baf730d 100644 --- a/nuget/Microsoft.Windows.CsWinRT.nuspec +++ b/nuget/Microsoft.Windows.CsWinRT.nuspec @@ -56,6 +56,8 @@ + + diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 133b4ebd46..cb4a1adc7d 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -215,10 +215,17 @@ Copyright (C) Microsoft Corporation. All rights reserved. + + + @@ -333,19 +340,102 @@ $(CsWinRTInternalProjection) - - - - - - + + + <_CsWinRTRefFilterLines Include="$([System.Text.RegularExpressions.Regex]::Split('$(CsWinRTFilters)', '[\r\n]+'))" /> + <_CsWinRTRefPrivateFilterLines Include="$([System.Text.RegularExpressions.Regex]::Split('$(CsWinRTPrivateFilters)', '[\r\n]+'))" /> + + <_CsWinRTRefIncludes Include="$([System.String]::Copy('%(_CsWinRTRefFilterLines.Identity)').Trim().Substring(9))" + Condition="$([System.String]::Copy('%(_CsWinRTRefFilterLines.Identity)').Trim().StartsWith('-include '))" /> + <_CsWinRTRefExcludes Include="$([System.String]::Copy('%(_CsWinRTRefFilterLines.Identity)').Trim().Substring(9))" + Condition="$([System.String]::Copy('%(_CsWinRTRefFilterLines.Identity)').Trim().StartsWith('-exclude '))" /> + + <_CsWinRTRefPrivateIncludes Include="$([System.String]::Copy('%(_CsWinRTRefPrivateFilterLines.Identity)').Trim().Substring(9))" + Condition="$([System.String]::Copy('%(_CsWinRTRefPrivateFilterLines.Identity)').Trim().StartsWith('-include '))" /> + <_CsWinRTRefPrivateExcludes Include="$([System.String]::Copy('%(_CsWinRTRefPrivateFilterLines.Identity)').Trim().Substring(9))" + Condition="$([System.String]::Copy('%(_CsWinRTRefPrivateFilterLines.Identity)').Trim().StartsWith('-exclude '))" /> + + + + + <_CsWinRTRefHasWindows Include="@(_CsWinRTRefIncludes)" Condition="'%(Identity)' == 'Windows'" /> + <_CsWinRTRefPrivateHasWindows Include="@(_CsWinRTRefPrivateIncludes)" Condition="'%(Identity)' == 'Windows'" /> + + + <_CsWinRTRefIncludes Include="WindowsRuntime.Internal" /> + <_CsWinRTRefInteropInputs Include="$(CsWinRTInteropMetadata)" /> + + + <_CsWinRTRefPrivateIncludes Include="WindowsRuntime.Internal" /> + <_CsWinRTRefPrivateInteropInputs Include="$(CsWinRTInteropMetadata)" /> + + + + + <_CsWinRTRefInputs Include="@(CsWinRTInputs->'%(FullPath)')" /> + <_CsWinRTRefInputs Include="$(CsWinRTWindowsMetadata)" Condition="'$(CsWinRTWindowsMetadata)' != ''" /> + <_CsWinRTRefInputs Include="@(_CsWinRTRefInteropInputs)" /> + + <_CsWinRTRefPrivateInputs Include="@(CsWinRTInputs->'%(FullPath)')" /> + <_CsWinRTRefPrivateInputs Include="$(CsWinRTWindowsMetadata)" Condition="'$(CsWinRTWindowsMetadata)' != ''" /> + <_CsWinRTRefPrivateInputs Include="@(_CsWinRTRefPrivateInteropInputs)" /> + + + + + + + + + - + - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8d3fa0ea34..14e54d26ee 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -44,6 +44,7 @@ $(MSBuildThisFileDirectory)WinRT.Interop.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) $(MSBuildThisFileDirectory)WinRT.Impl.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) $(MSBuildThisFileDirectory)WinRT.Projection.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) + $(MSBuildThisFileDirectory)WinRT.Projection.Ref.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) $(MSBuildThisFileDirectory)WinRT.WinMD.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) AnyCPU diff --git a/src/Projections/Directory.Build.targets b/src/Projections/Directory.Build.targets deleted file mode 100644 index 2a71437499..0000000000 --- a/src/Projections/Directory.Build.targets +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/WinRT.Generator.Tasks/RunCsWinRTProjectionRefGenerator.cs b/src/WinRT.Generator.Tasks/RunCsWinRTProjectionRefGenerator.cs new file mode 100644 index 0000000000..187ba044d7 --- /dev/null +++ b/src/WinRT.Generator.Tasks/RunCsWinRTProjectionRefGenerator.cs @@ -0,0 +1,310 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Build.Tasks; + +/// +/// The custom MSBuild task that invokes the 'cswinrtprojectionrefgen' tool to generate +/// reference-projection C# sources. This is the C# replacement for the historical +/// '<Exec Command="$(CsWinRTCommand)">' invocation in the 'CsWinRTGenerateProjection' +/// MSBuild target. The tool produces .cs files that get included in the user's library +/// or component compilation via the 'CsWinRTIncludeProjection' target. +/// +public sealed class RunCsWinRTProjectionRefGenerator : ToolTask +{ + /// + /// Gets or sets the input WinMD files (or directories that should be recursively scanned) + /// providing the Windows Runtime metadata to project. Mirrors the C++ '-input' arg. + /// + [Required] + public ITaskItem[]? InputWinMDPaths { get; set; } + + /// + /// Gets or sets the directory where the generated .cs files will be placed. + /// Mirrors the C++ '-output' arg. + /// + [Required] + public string? OutputDirectory { get; set; } + + /// + /// Gets or sets the target framework for which the projection is being generated. + /// CsWinRT 3.0 requires this to be 'net10.0' or later. + /// + [Required] + public string? TargetFramework { get; set; } + + /// + /// Gets or sets the optional Windows metadata token (literal path, 'local', 'sdk', 'sdk+', + /// or a version string like '10.0.26100.0'). The token is expanded in-tool to the actual + /// set of .winmd files (mirrors the C++ tool's cmd_reader.h behavior). + /// + public string? WindowsMetadata { get; set; } + + /// + /// Gets or sets the namespace prefixes to include in the projection. Mirrors the C++ '-include' arg. + /// + public ITaskItem[]? IncludeNamespaces { get; set; } + + /// + /// Gets or sets the namespace prefixes to exclude from the projection. Mirrors the C++ '-exclude' arg. + /// + public ITaskItem[]? ExcludeNamespaces { get; set; } + + /// + /// Gets or sets the namespace prefixes to exclude from the projection additions. + /// Mirrors the C++ '-addition_exclude' arg. + /// + public ITaskItem[]? AdditionExcludeNamespaces { get; set; } + + /// + /// Gets or sets whether to enable verbose progress logging. Mirrors the C++ '-verbose' arg. + /// + public bool Verbose { get; set; } + + /// + /// Gets or sets whether to generate a Windows Runtime component projection. + /// Mirrors the C++ '-component' arg. + /// + public bool Component { get; set; } + + /// + /// Gets or sets whether to generate a private (internal) projection. Mirrors the C++ '-internal' arg. + /// CsWinRT 3.0 leftover; preserved for OLD-target parity. + /// + public bool InternalProjection { get; set; } + + /// + /// Gets or sets whether to generate an embedded projection. Mirrors the C++ '-embedded' arg. + /// CsWinRT 3.0 leftover; preserved for OLD-target parity. + /// + public bool Embedded { get; set; } + + /// + /// Gets or sets whether to emit enums as public when used with the embedded option. + /// Mirrors the C++ '-public_enums' arg. + /// CsWinRT 3.0 leftover; preserved for OLD-target parity. + /// + public bool PublicEnums { get; set; } + + /// + /// Gets or sets whether to make exclusive-to interfaces public in the projection + /// (default is internal). Mirrors the C++ '-public_exclusiveto' arg. + /// + public bool PublicExclusiveTo { get; set; } + + /// + /// Gets or sets whether exclusive-to interfaces should support IDynamicInterfaceCastable. + /// Mirrors the C++ '-idic_exclusiveto' arg. + /// + public bool IdicExclusiveTo { get; set; } + + /// + /// Gets or sets whether the generated projection should be a reference assembly projection. + /// Mirrors the C++ '-reference_projection' arg. + /// + public bool ReferenceProjection { get; set; } + + /// + /// Gets or sets the tools directory where the 'cswinrtprojectionrefgen' tool is located. + /// + [Required] + public string? CsWinRTToolsDirectory { get; set; } + + /// + /// Gets or sets the architecture of 'cswinrtprojectionrefgen' to use. + /// + /// + /// If not set, the architecture will be determined based on the current process architecture. + /// + public string? CsWinRTToolsArchitecture { get; set; } + + /// + /// Gets or sets additional arguments to pass to the tool. + /// + public ITaskItem[]? AdditionalArguments { get; set; } + + /// + protected override string ToolName => "cswinrtprojectionrefgen.exe"; + + /// +#if NET10_0_OR_GREATER + [MemberNotNullWhen(true, nameof(InputWinMDPaths))] + [MemberNotNullWhen(true, nameof(OutputDirectory))] + [MemberNotNullWhen(true, nameof(TargetFramework))] + [MemberNotNullWhen(true, nameof(CsWinRTToolsDirectory))] +#endif + protected override bool ValidateParameters() + { + if (!base.ValidateParameters()) + { + return false; + } + + if (InputWinMDPaths is not { Length: > 0 }) + { + Log.LogWarning("Invalid 'InputWinMDPaths' input(s)."); + + return false; + } + + if (string.IsNullOrEmpty(OutputDirectory)) + { + Log.LogWarning("'OutputDirectory' is required."); + + return false; + } + + if (string.IsNullOrEmpty(TargetFramework)) + { + Log.LogWarning("'TargetFramework' is required."); + + return false; + } + + if (CsWinRTToolsDirectory is null || !Directory.Exists(CsWinRTToolsDirectory)) + { + Log.LogWarning("Tools directory '{0}' is invalid or does not exist.", CsWinRTToolsDirectory); + + return false; + } + + if (CsWinRTToolsArchitecture is not null && + !CsWinRTToolsArchitecture.Equals("x86", StringComparison.OrdinalIgnoreCase) && + !CsWinRTToolsArchitecture.Equals("x64", StringComparison.OrdinalIgnoreCase) && + !CsWinRTToolsArchitecture.Equals("arm64", StringComparison.OrdinalIgnoreCase) && + !CsWinRTToolsArchitecture.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase)) + { + Log.LogWarning("Tools architecture '{0}' is invalid (it must be 'x86', 'x64', 'arm64', or 'AnyCPU').", CsWinRTToolsArchitecture); + + return false; + } + + return true; + } + + /// + [SuppressMessage("Style", "IDE0072", Justification = "We always use 'x64' as a fallback for all other CPU architectures.")] + protected override string GenerateFullPathToTool() + { + string? effectiveArchitecture = CsWinRTToolsArchitecture; + + // Special case for when 'AnyCPU' is specified (mostly for testing scenarios). + // We just reuse the exact input directory and assume the architecture matches. + // This makes it easy to run the task against a local build of 'cswinrtprojectionrefgen'. + if (effectiveArchitecture?.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase) is true) + { + return Path.Combine(CsWinRTToolsDirectory!, ToolName); + } + + // If the architecture is not specified, determine it based on the current process architecture + effectiveArchitecture ??= RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "x64", + Architecture.Arm64 => "arm64", + _ => "x64" + }; + + // The tool is inside an architecture-specific subfolder, as it's a native binary + string architectureDirectory = $"win-{effectiveArchitecture}"; + + return Path.Combine(CsWinRTToolsDirectory!, architectureDirectory, ToolName); + } + + /// + protected override string GenerateResponseFileCommands() + { + StringBuilder args = new(); + + IEnumerable inputWinMDPaths = InputWinMDPaths!.Select(static path => path.ItemSpec); + string inputWinMDPathsArg = string.Join(",", inputWinMDPaths); + + AppendResponseFileCommand(args, "--input-paths", inputWinMDPathsArg); + AppendResponseFileCommand(args, "--output-directory", OutputDirectory!); + AppendResponseFileCommand(args, "--target-framework", TargetFramework!); + + if (!string.IsNullOrEmpty(WindowsMetadata)) + { + AppendResponseFileCommand(args, "--windows-metadata", WindowsMetadata!); + } + + if (IncludeNamespaces is { Length: > 0 }) + { + AppendResponseFileCommand(args, "--include-namespaces", string.Join(",", IncludeNamespaces.Select(static i => i.ItemSpec))); + } + + if (ExcludeNamespaces is { Length: > 0 }) + { + AppendResponseFileCommand(args, "--exclude-namespaces", string.Join(",", ExcludeNamespaces.Select(static i => i.ItemSpec))); + } + + if (AdditionExcludeNamespaces is { Length: > 0 }) + { + AppendResponseFileCommand(args, "--addition-exclude-namespaces", string.Join(",", AdditionExcludeNamespaces.Select(static i => i.ItemSpec))); + } + + if (Verbose) + { + AppendResponseFileCommand(args, "--verbose", "true"); + } + + if (Component) + { + AppendResponseFileCommand(args, "--component", "true"); + } + + if (InternalProjection) + { + AppendResponseFileCommand(args, "--internal", "true"); + } + + if (Embedded) + { + AppendResponseFileCommand(args, "--embedded", "true"); + } + + if (PublicEnums) + { + AppendResponseFileCommand(args, "--public-enums", "true"); + } + + if (PublicExclusiveTo) + { + AppendResponseFileCommand(args, "--public-exclusive-to", "true"); + } + + if (IdicExclusiveTo) + { + AppendResponseFileCommand(args, "--idic-exclusive-to", "true"); + } + + if (ReferenceProjection) + { + AppendResponseFileCommand(args, "--reference-projection", "true"); + } + + // Add any additional arguments that are not statically known + foreach (ITaskItem additionalArgument in AdditionalArguments ?? []) + { + _ = args.AppendLine(additionalArgument.ItemSpec); + } + + return args.ToString(); + } + + /// + /// Appends a command line argument to the response file arguments, with the right format. + /// + /// The command line arguments being built. + /// The command name to append. + /// The command value to append. + private static void AppendResponseFileCommand(StringBuilder args, string commandName, string commandValue) + { + _ = args.Append($"{commandName} ").AppendLine(commandValue); + } +} diff --git a/src/WinRT.Projection.Generator.Writer.TestRunner/Program.cs b/src/WinRT.Projection.Generator.Writer.TestRunner/Program.cs new file mode 100644 index 0000000000..b8152be4ae --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer.TestRunner/Program.cs @@ -0,0 +1,402 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using WindowsRuntime.ProjectionGenerator.Writer; + +namespace WindowsRuntime.ProjectionGenerator.Writer.TestRunner; + +internal static class Program +{ + public static int Main(string[] args) + { + // Modes: + // 1) Single .winmd path → simple test (legacy) + // 2) "compare" [--ref] → SDK projection mode + // 3) "compare-xaml" [--ref] → XAML projection mode + // 4) "compare-authoring" → component projection mode (uses fixed paths + // that mirror the .rsp file in 'authoring-projection\generated-sources\ProjectionGenerator.rsp' + // shipped in the test inputs folder). + // + // The optional [--ref] flag enables reference-projection mode (mirrors the C++ + // tool's -reference_projection flag and the CsWinRTGenerateReferenceProjection + // MSBuild property): emits a public-API-only projection without + // IWindowsRuntimeInterface markers, ABI helpers, vtables, etc. + bool refMode = Array.IndexOf(args, "--ref") >= 0; + if (args.Length >= 4 && args[0] == "compare") + { + return RunCompare(args[1], args[2], args[3], refMode); + } + if (args.Length >= 4 && args[0] == "compare-xaml") + { + return RunCompareXaml(args[1], args[2], args[3], refMode); + } + if (args.Length >= 2 && args[0] == "compare-authoring") + { + return RunCompareAuthoring(args[1]); + } + + return RunSimple(args); + } + + private static int RunSimple(string[] args) + { + string winmdPath = args.Length > 0 ? args[0] : @"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; + string outputFolder = args.Length > 1 ? args[1] : Path.Combine(Path.GetTempPath(), "CsWinRTPort", $"out-{DateTime.UtcNow:yyyyMMddHHmmss}"); + + if (!File.Exists(winmdPath)) + { + Console.Error.WriteLine($"Input file not found: {winmdPath}"); + return 1; + } + + Console.WriteLine($"Input: {winmdPath}"); + Console.WriteLine($"Output: {outputFolder}"); + + try + { + ProjectionWriter.Run(new ProjectionWriterOptions + { + InputPaths = new[] { winmdPath }, + OutputFolder = outputFolder, + Include = new[] { "Windows", "WindowsRuntime.Internal" }, + Verbose = true, + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"ERROR: {ex.Message}"); + Console.Error.WriteLine(ex.StackTrace); + return 1; + } + + if (Directory.Exists(outputFolder)) + { + string[] files = Directory.GetFiles(outputFolder, "*.cs", SearchOption.AllDirectories); + Console.WriteLine($"Generated {files.Length} files"); + } + return 0; + } + + /// + /// Runs the projection writer with the same options the build pipeline uses for the SDK + /// projection (the truth output to compare against). + /// + private static int RunCompare(string winmdFolder, string internalWinmd, string output, bool referenceProjection = false) + { + if (Directory.Exists(output)) + { + Directory.Delete(output, true); + } + _ = Directory.CreateDirectory(output); + + // Truth uses per-contract WinMD files (one .winmd per ApiContract). When the caller passes + // the unified UnionMetadata\Windows.winmd, redirect to the per-contract folder so the + // [WindowsRuntimeMetadata(...)] attribute argument matches truth (= contract file stem). + string resolvedWinmd = winmdFolder; + const string PerContractFolder = @"C:\Users\sergiopedri\.nuget\packages\microsoft.windows.sdk.net.ref\10.0.26100.85-preview\winmd\windows"; + if (winmdFolder.EndsWith(@"\Windows.winmd", StringComparison.OrdinalIgnoreCase) && Directory.Exists(PerContractFolder)) + { + resolvedWinmd = PerContractFolder; + } + + try + { + ProjectionWriter.Run(new ProjectionWriterOptions + { + InputPaths = new[] { resolvedWinmd, internalWinmd }, + OutputFolder = output, + ReferenceProjection = referenceProjection, + Include = new[] + { + "Windows", + "WindowsRuntime.Internal", + "Windows.UI.Xaml.Interop", + "Windows.UI.Xaml.Data.BindableAttribute", + "Windows.UI.Xaml.Markup.ContentPropertyAttribute", + }, + Exclude = new[] + { + "Windows.UI.Colors", + "Windows.UI.ColorHelper", + "Windows.UI.IColorHelper", + "Windows.UI.IColors", + "Windows.UI.Text.FontWeights", + "Windows.UI.Text.IFontWeights", + "Windows.UI.Xaml", + "Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper", + "Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper", + }, + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"ERROR: {ex.Message}"); + Console.Error.WriteLine(ex.StackTrace); + return 1; + } + + Console.WriteLine($"Generated {Directory.GetFiles(output, "*.cs").Length} files in {output}"); + return 0; + } + + /// + /// Runs the projection writer with the same options the build pipeline uses for the XAML + /// projection (the truth output to compare against). Mirrors the .rsp file in the XAML truth folder. + /// + private static int RunCompareXaml(string xamlWinmdFolder, string internalWinmd, string output, bool referenceProjection = false) + { + if (Directory.Exists(output)) + { + Directory.Delete(output, true); + } + _ = Directory.CreateDirectory(output); + + // Same as compare: prefer per-contract .winmd folder so [WindowsRuntimeMetadata(...)] matches truth. + // The XAML scenario uses the 'xaml' subfolder which contains a larger Windows.Foundation.UniversalApiContract.winmd + // that holds the Windows.UI.Xaml.* types as well. + string resolvedWinmd = xamlWinmdFolder; + const string PerContractFolder = @"C:\Users\sergiopedri\.nuget\packages\microsoft.windows.sdk.net.ref\10.0.26100.85-preview\winmd\xaml"; + if (xamlWinmdFolder.EndsWith(@"\Windows.winmd", StringComparison.OrdinalIgnoreCase) && Directory.Exists(PerContractFolder)) + { + resolvedWinmd = PerContractFolder; + } + + try + { + ProjectionWriter.Run(new ProjectionWriterOptions + { + InputPaths = new[] { resolvedWinmd, internalWinmd }, + OutputFolder = output, + ReferenceProjection = referenceProjection, + // Mirrors the XAML projection generation .rsp: + // -exclude Windows, then -include for the specific XAML namespaces and helpers. + Include = new[] + { + "Windows.UI.Colors", + "Windows.UI.ColorHelper", + "Windows.UI.IColorHelper", + "Windows.UI.IColors", + "Windows.UI.Text.FontWeights", + "Windows.UI.Text.IFontWeights", + "Windows.UI.Xaml", + "Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper", + "Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper", + }, + Exclude = new[] + { + "Windows", + "Windows.UI.Xaml.Interop", + "Windows.UI.Xaml.Data.BindableAttribute", + "Windows.UI.Xaml.Markup.ContentPropertyAttribute", + }, + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"ERROR: {ex.Message}"); + Console.Error.WriteLine(ex.StackTrace); + return 1; + } + + Console.WriteLine($"Generated {Directory.GetFiles(output, "*.cs").Length} files in {output}"); + return 0; + } + + /// + /// Runs the projection writer with the same options the build pipeline uses for component + /// authoring (the truth output to compare against). Mirrors the .rsp file in the authoring truth folder. + /// + private static int RunCompareAuthoring(string output) + { + const string AuthoringTestWinmd = @"C:\Users\sergiopedri\Downloads\authoring-projection\AuthoringTest.winmd"; + const string InternalWinmd = @"C:\Users\sergiopedri\.nuget\packages\microsoft.windows.cswinrt\3.0.0-prerelease-ci.260430.9\metadata\WindowsRuntime.Internal.winmd"; + const string AppSdkInteractive = @"C:\Users\sergiopedri\.nuget\packages\microsoft.windowsappsdk.interactiveexperiences\1.8.251104001\metadata\10.0.18362.0"; + const string AppSdkWinui = @"C:\Users\sergiopedri\.nuget\packages\microsoft.windowsappsdk.winui\1.8.251105000\metadata"; + const string AppSdkFoundation = @"C:\Users\sergiopedri\.nuget\packages\microsoft.windowsappsdk.foundation\1.8.251104000\metadata"; + const string WebView2 = @"C:\Users\sergiopedri\.nuget\packages\microsoft.web.webview2\1.0.3179.45\lib\Microsoft.Web.WebView2.Core.winmd"; + const string SdkUnionMetadata = @"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; + + if (Directory.Exists(output)) + { + Directory.Delete(output, true); + } + _ = Directory.CreateDirectory(output); + + // Mirrors '-include AuthoringTest.*' from the truth .rsp + string[] includes = new[] + { + "AuthoringTest.BasicEnum", + "AuthoringTest.FlagsEnum", + "AuthoringTest.BasicDelegate", + "AuthoringTest.ComplexDelegate", + "AuthoringTest.DoubleDelegate", + "AuthoringTest.BasicClass", + "AuthoringTest.CustomWWW", + "AuthoringTest.BasicStruct", + "AuthoringTest.ComplexStruct", + "AuthoringTest.IBasicClassClass", + "AuthoringTest.CustomProperty", + "AuthoringTest.CustomPropertyStructType", + "AuthoringTest.ICustomPropertyClass", + "AuthoringTest.CustomPropertyRecordTypeFactory", + "AuthoringTest.ICustomPropertyRecordTypeFactoryStatic", + "AuthoringTest.ICustomPropertyRecordTypeFactoryClass", + "AuthoringTest.CustomPropertyProviderWithExplicitImplementation", + "AuthoringTest.CustomPropertyWithExplicitImplementation", + "AuthoringTest.IDouble", + "AuthoringTest.IAnotherInterface", + "AuthoringTest.TestClass", + "AuthoringTest.CustomDictionary", + "AuthoringTest.DisposableClass", + "AuthoringTest.IDisposableClassClass", + "AuthoringTest.ITestClassStatic", + "AuthoringTest.ITestClassFactory", + "AuthoringTest.ITestClassClass", + "AuthoringTest.CustomReadOnlyDictionary", + "AuthoringTest.ICustomReadOnlyDictionaryFactory", + "AuthoringTest.CustomVector", + "AuthoringTest.ICustomVectorFactory", + "AuthoringTest.CustomVectorView", + "AuthoringTest.ICustomVectorViewFactory", + "AuthoringTest.CustomVector2", + "AuthoringTest.ICustomVector2Factory", + "AuthoringTest.StaticClass", + "AuthoringTest.IStaticClassStatic", + "AuthoringTest.IStaticClassClass", + "AuthoringTest.ButtonUtils", + "AuthoringTest.CustomButton", + "AuthoringTest.ICustomButtonFactory", + "AuthoringTest.ICustomButtonClass", + "AuthoringTest.IButtonUtilsStatic", + "AuthoringTest.IButtonUtilsClass", + "AuthoringTest.CustomStackPanel", + "AuthoringTest.ICustomStackPanelClass", + "AuthoringTest.CustomXamlServiceProvider", + "AuthoringTest.CustomNotifyPropertyChanged", + "AuthoringTest.CustomNotifyPropertyChangedAndChanging", + "AuthoringTest.CustomCommand", + "AuthoringTest.ICustomCommandClass", + "AuthoringTest.CustomNotifyCollectionChanged", + "AuthoringTest.CustomNotifyDataErrorInfo", + "AuthoringTest.CustomNotifyDataErrorInfo2", + "AuthoringTest.CustomEnumerable", + "AuthoringTest.ICustomEnumerableFactory", + "AuthoringTest.CustomXamlMetadataProvider", + "AuthoringTest.SingleInterfaceClass", + "AuthoringTest.IDouble2", + "AuthoringTest.ExplicltlyImplementedClass", + "AuthoringTest.IExplicltlyImplementedClassClass", + "AuthoringTest.ObservableVector", + "AuthoringTest.IInterfaceInheritance", + "AuthoringTest.InterfaceInheritance", + "AuthoringTest.MultipleInterfaceMappingClass", + "AuthoringTest.CustomDictionary2", + "AuthoringTest.TestCollection", + "AuthoringTest.ITestCollectionClass", + "AuthoringTest.IPartialInterface", + "AuthoringTest.PartialClass", + "AuthoringTest.PartialStruct", + "AuthoringTest.IPartialClassFactory", + "AuthoringTest.IPartialClassClass", + "AuthoringTest.IPublicInterface", + "AuthoringTest.ICustomInterfaceGuid", + "AuthoringTest.CustomInterfaceGuidClass", + "AuthoringTest.NonActivatableType", + "AuthoringTest.INonActivatableTypeClass", + "AuthoringTest.NonActivatableFactory", + "AuthoringTest.INonActivatableFactoryStatic", + "AuthoringTest.INonActivatableFactoryClass", + "AuthoringTest.TypeOnlyActivatableViaItsOwnFactory", + "AuthoringTest.ITypeOnlyActivatableViaItsOwnFactoryStatic", + "AuthoringTest.ITypeOnlyActivatableViaItsOwnFactoryClass", + "AuthoringTest.AnotherNamespace.IOutParams", + "AuthoringTest.AnotherNamespace.NullableParamClass", + "AuthoringTest.AnotherNamespace.INullableParamClassClass", + "AuthoringTest.AnotherNamespace.MappedTypeParamClass", + "AuthoringTest.AnotherNamespace.IMappedTypeParamClassClass", + "AuthoringTest.AnotherNamespace.MixedArrayClass", + "AuthoringTest.AnotherNamespace.IMixedArrayClassClass", + "AuthoringTest.AnotherNamespace.ICustomResource", + "AuthoringTest.AnotherNamespace.DisposableResource", + "AuthoringTest.AnotherNamespace.MultiConstructorClass", + "AuthoringTest.AnotherNamespace.IMultiConstructorClassFactory", + "AuthoringTest.AnotherNamespace.IMultiConstructorClassClass", + "AuthoringTest.AnotherNamespace.StaticComplexProps", + "AuthoringTest.AnotherNamespace.IStaticComplexPropsStatic", + "AuthoringTest.AnotherNamespace.IStaticComplexPropsClass", + "AuthoringTest.AnotherNamespace.OverloadedMethodClass", + "AuthoringTest.AnotherNamespace.IOverloadedMethodClassStatic", + "AuthoringTest.AnotherNamespace.IOverloadedMethodClassClass", + "AuthoringTest.AnotherNamespace.InnerStruct", + "AuthoringTest.AnotherNamespace.OuterStruct", + "AuthoringTest.AnotherNamespace.MultiParamDelegate", + "AuthoringTest.AnotherNamespace.StructParamDelegate", + "AuthoringTest.AnotherNamespace.StructReturnDelegate", + "AuthoringTest.AnotherNamespace.DetailedFlags", + "AuthoringTest.AnotherNamespace.Priority", + "AuthoringTest.AnotherNamespace.AsyncMethodClass", + "AuthoringTest.AnotherNamespace.IAsyncMethodClassClass", + "AuthoringTest.AnotherNamespace.IVersionedInterface", + "AuthoringTest.AnotherNamespace.DeprecatedMembersClass", + "AuthoringTest.AnotherNamespace.IDeprecatedMembersClassClass", + "AuthoringTest.AnotherNamespace.NotifyWithCustomInterface", + "AuthoringTest.AnotherNamespace.INotifyWithCustomInterfaceClass", + "AuthoringTest.AnotherNamespace.FactoryAndStaticClass", + "AuthoringTest.AnotherNamespace.IFactoryAndStaticClassStatic", + "AuthoringTest.AnotherNamespace.IFactoryAndStaticClassFactory", + "AuthoringTest.AnotherNamespace.IFactoryAndStaticClassClass", + "AuthoringTest.AnotherNamespace.IFullFeaturedInterface", + "AuthoringTest.AnotherNamespace.FullFeaturedClass", + "AuthoringTest.AnotherNamespace.IFullFeaturedClassClass", + "AuthoringTest.AnotherNamespace.AnotherNamespaceContract", + "AuthoringTest.AnotherNamespace.ContractVersionedClass", + "AuthoringTest.AnotherNamespace.IContractVersionedClassClass", + "AuthoringTest.AnotherNamespace.ContractVersionedClassV2", + "AuthoringTest.AnotherNamespace.IContractVersionedClassV2Class", + "AuthoringTest.AnotherNamespace.IContractVersionedMembersV1", + "AuthoringTest.AnotherNamespace.IContractVersionedMembersV2", + "AuthoringTest.AnotherNamespace.ContractVersionedMembersClass", + "AuthoringTest.AnotherNamespace.IContractVersionedMembersClassClass", + "AuthoringTest.AnotherNamespace.IVersionedMembersV1", + "AuthoringTest.AnotherNamespace.IVersionedMembersV2", + "AuthoringTest.AnotherNamespace.VersionedMembersClass", + "AuthoringTest.AnotherNamespace.IVersionedMembersClassClass", + }; + + // Inputs: SDK + WindowsAppSDK + WinUI + WebView2 + WindowsRuntime.Internal + AuthoringTest + string[] inputs = new[] + { + SdkUnionMetadata, + AppSdkInteractive, + AppSdkWinui, + AppSdkFoundation, + WebView2, + InternalWinmd, + AuthoringTestWinmd, + }; + + try + { + ProjectionWriter.Run(new ProjectionWriterOptions + { + InputPaths = inputs, + OutputFolder = output, + Include = includes, + Exclude = new[] { "Windows" }, + Component = true, + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"ERROR: {ex.Message}"); + Console.Error.WriteLine(ex.StackTrace); + return 1; + } + + Console.WriteLine($"Generated {Directory.GetFiles(output, "*.cs").Length} files in {output}"); + return 0; + } +} + diff --git a/src/WinRT.Projection.Generator.Writer.TestRunner/WinRT.Projection.Generator.Writer.TestRunner.csproj b/src/WinRT.Projection.Generator.Writer.TestRunner/WinRT.Projection.Generator.Writer.TestRunner.csproj new file mode 100644 index 0000000000..bcef2c24fc --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer.TestRunner/WinRT.Projection.Generator.Writer.TestRunner.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + 14.0 + enable + WindowsRuntime.ProjectionGenerator.Writer.TestRunner + $(NoWarn);CS1591;CS1573;CS1574;CS1712;CS1734;IDE0005;IDE0010;IDE0011;IDE0022;IDE0028;IDE0046;IDE0057;IDE0078;IDE0090;IDE0270;IDE0290;IDE0300;IDE0301;IDE0305;IDE0306;IDE0058;IDE0008;IDE0007;IDE0150;IDE0042;IDE0017;IDE0019;IDE0021;IDE0040;IDE0044;IDE0050;IDE0052;IDE0055;IDE0059;IDE0060;IDE0063;IDE0066;IDE0083;IDE0130;IDE0180 + + + + + diff --git a/src/WinRT.Projection.Generator.Writer/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Generator.Writer/Generation/ProjectionGenerator.cs new file mode 100644 index 0000000000..422d99afe7 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Generation/ProjectionGenerator.cs @@ -0,0 +1,417 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Orchestrates the projection generation. Mirrors the body of cswinrt::run in main.cpp. +/// +internal sealed class ProjectionGenerator +{ + private readonly Settings _settings; + private readonly MetadataCache _cache; + private readonly CancellationToken _token; + + public ProjectionGenerator(Settings settings, MetadataCache cache, CancellationToken token) + { + _settings = settings; + _cache = cache; + _token = token; + } + + public void Run() + { + // Set the static cache reference for writers that need source-file paths + CodeWriters.SetMetadataCache(_cache); + + // Find component activatable classes (component mode only) + HashSet componentActivatable = new(); + Dictionary> componentByModule = new(StringComparer.Ordinal); + + if (_settings.Component) + { + foreach ((_, NamespaceMembers members) in _cache.Namespaces) + { + foreach (TypeDefinition type in members.Classes) + { + if (!_settings.Filter.Includes(type)) { continue; } + if (TypeCategorization.HasAttribute(type, "Windows.Foundation.Metadata", "ActivatableAttribute") || + TypeCategorization.HasAttribute(type, "Windows.Foundation.Metadata", "StaticAttribute")) + { + _ = componentActivatable.Add(type); + string moduleName = Path.GetFileNameWithoutExtension(_cache.GetSourcePath(type)); + if (!componentByModule.TryGetValue(moduleName, out HashSet? set)) + { + set = new HashSet(); + componentByModule[moduleName] = set; + } + _ = set.Add(type); + } + } + } + } + + if (_settings.Verbose) + { + foreach (string p in _settings.Input) + { + Console.Out.WriteLine($"input: {p}"); + } + Console.Out.WriteLine($"output: {_settings.OutputFolder}"); + } + + // Write GeneratedInterfaceIIDs file (mirrors main.cpp logic) + bool iidWritten = false; + if (!_settings.ReferenceProjection) + { + // Collect factory interfaces (Static/Activatable/Composable) referenced by included + // classes globally. Their IIDs must be present in GeneratedInterfaceIIDs.cs even if + // the filter excludes them, because static class members reference them. + HashSet factoryInterfacesGlobal = new(); + foreach ((_, NamespaceMembers nsMembers) in _cache.Namespaces) + { + foreach (TypeDefinition type in nsMembers.Classes) + { + if (!_settings.Filter.Includes(type)) { continue; } + // Skip mapped classes whose ABI surface is suppressed (e.g. + // 'Windows.UI.Xaml.Interop.NotifyCollectionChangedEventArgs' maps to + // 'System.Collections.Specialized.NotifyCollectionChangedEventArgs' with + // EmitAbi=false). Their factory/statics interfaces should also be skipped. + string clsNs = type.Namespace?.Value ?? string.Empty; + string clsNm = type.Name?.Value ?? string.Empty; + MappedType? clsMapped = MappedTypes.Get(clsNs, clsNm); + if (clsMapped is not null && !clsMapped.EmitAbi) { continue; } + foreach (KeyValuePair kv in AttributedTypes.Get(type, _cache)) + { + TypeDefinition? facType = kv.Value.Type; + if (facType is not null) { _ = factoryInterfacesGlobal.Add(facType); } + } + } + } + + HashSet interfacesFromClassesEmitted = new(); + TypeWriter guidWriter = new(_settings, "ABI"); + CodeWriters.WriteInterfaceIidsBegin(guidWriter); + // Iterate namespaces in sorted order (mirrors C++ std::map + // iteration). Within each namespace, types are already sorted by SortMembersByName. + // The sorted-by-namespace order produces the parent-before-child grouping in the + // GeneratedInterfaceIIDs.cs output (e.g. Windows.ApplicationModel.* types before + // Windows.ApplicationModel.Activation.* types). + foreach ((string ns, NamespaceMembers members) in _cache.Namespaces.OrderBy(kvp => kvp.Key, System.StringComparer.Ordinal)) + { + foreach (TypeDefinition type in members.Types) + { + bool isFactoryInterface = factoryInterfacesGlobal.Contains(type); + if (!_settings.Filter.Includes(type) && !isFactoryInterface) { continue; } + if (TypeCategorization.IsGeneric(type)) { continue; } + string ns2 = type.Namespace?.Value ?? string.Empty; + string nm2 = type.Name?.Value ?? string.Empty; + MappedType? m = MappedTypes.Get(ns2, nm2); + if (m is not null && !m.EmitAbi) { continue; } + iidWritten = true; + TypeCategory cat = TypeCategorization.GetCategory(type); + switch (cat) + { + case TypeCategory.Delegate: + CodeWriters.WriteIidGuidPropertyFromSignature(guidWriter, type); + CodeWriters.WriteIidGuidPropertyFromType(guidWriter, type); + break; + case TypeCategory.Enum: + CodeWriters.WriteIidGuidPropertyFromSignature(guidWriter, type); + break; + case TypeCategory.Interface: + CodeWriters.WriteIidGuidPropertyFromType(guidWriter, type); + break; + case TypeCategory.Struct: + CodeWriters.WriteIidGuidPropertyFromSignature(guidWriter, type); + break; + case TypeCategory.Class: + CodeWriters.WriteIidGuidPropertyForClassInterfaces(guidWriter, type, interfacesFromClassesEmitted); + break; + } + } + } + CodeWriters.WriteInterfaceIidsEnd(guidWriter); + if (iidWritten) + { + guidWriter.FlushToFile(Path.Combine(_settings.OutputFolder, "GeneratedInterfaceIIDs.cs")); + } + } + + ConcurrentDictionary defaultInterfaceEntries = new(); + ConcurrentBag> exclusiveToInterfaceEntries = new(); + ConcurrentDictionary authoredTypeNameToMetadataMap = new(); + bool projectionFileWritten = false; + + // Process namespaces sequentially for now (C++ used task_group / parallel processing) + foreach ((string ns, NamespaceMembers members) in _cache.Namespaces) + { + _token.ThrowIfCancellationRequested(); + bool wrote = ProcessNamespace(ns, members, componentActivatable, defaultInterfaceEntries, exclusiveToInterfaceEntries, authoredTypeNameToMetadataMap); + if (wrote) + { + projectionFileWritten = true; + } + } + + // Component mode: write the WinRT_Module.cs file with activation factory entry points + if (_settings.Component) + { + TextWriter wm = new(); + CodeWriters.WriteFileHeader(wm); + CodeWriters.WriteModuleActivationFactory(wm, componentByModule); + wm.FlushToFile(Path.Combine(_settings.OutputFolder, "WinRT_Module.cs")); + projectionFileWritten = true; + } + + // Write WindowsRuntimeDefaultInterfaces.cs and WindowsRuntimeExclusiveToInterfaces.cs + if (defaultInterfaceEntries.Count > 0 && !_settings.ReferenceProjection) + { + List> sorted = new(defaultInterfaceEntries); + sorted.Sort((a, b) => System.StringComparer.Ordinal.Compare(a.Key, b.Key)); + CodeWriters.WriteDefaultInterfacesClass(_settings, sorted); + } + + if (!exclusiveToInterfaceEntries.IsEmpty && _settings.Component && !_settings.ReferenceProjection) + { + List> sorted = new(exclusiveToInterfaceEntries); + sorted.Sort((a, b) => System.StringComparer.Ordinal.Compare(a.Key, b.Key)); + CodeWriters.WriteExclusiveToInterfacesClass(_settings, sorted); + } + + // Write strings/ base files (ComInteropExtensions etc.) + if (projectionFileWritten) + { + WriteBaseStrings(); + } + } + + /// + /// Processes a single namespace and writes its projection file. Returns whether a file was written. + /// + private bool ProcessNamespace(string ns, NamespaceMembers members, HashSet componentActivatable, + ConcurrentDictionary defaultInterfaceEntries, ConcurrentBag> exclusiveToInterfaceEntries, + ConcurrentDictionary authoredTypeNameToMetadataMap) + { + TypeWriter w = new(_settings, ns); + w.WriteFileHeader(); + + bool written = false; + + // Phase 1: TypeMapGroup assembly attributes + if (!_settings.ReferenceProjection) + { + CodeWriters.WritePragmaDisableIL2026(w); + foreach (TypeDefinition type in members.Types) + { + if (!_settings.Filter.Includes(type)) { continue; } + if (TypeCategorization.IsGeneric(type)) { continue; } + string ns2 = type.Namespace?.Value ?? string.Empty; + string nm2 = type.Name?.Value ?? string.Empty; + MappedType? m = MappedTypes.Get(ns2, nm2); + if (m is not null && !m.EmitAbi) { continue; } + + TypeCategory cat = TypeCategorization.GetCategory(type); + switch (cat) + { + case TypeCategory.Class: + if (!TypeCategorization.IsStatic(type) && !TypeCategorization.IsAttributeType(type)) + { + if (_settings.Component) + { + CodeWriters.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(w, type); + } + else + { + CodeWriters.WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(w, type, false); + } + } + break; + case TypeCategory.Delegate: + CodeWriters.WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(w, type, true); + CodeWriters.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(w, type); + break; + case TypeCategory.Enum: + CodeWriters.WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(w, type, true); + CodeWriters.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(w, type); + break; + case TypeCategory.Interface: + CodeWriters.WriteWinRTIdicTypeMapGroupAssemblyAttribute(w, type); + CodeWriters.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(w, type); + break; + case TypeCategory.Struct: + if (!TypeCategorization.IsApiContractType(type)) + { + CodeWriters.WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(w, type, true); + CodeWriters.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(w, type); + } + break; + } + } + CodeWriters.WritePragmaRestoreIL2026(w); + } + + // Phase 2: Projected types + w.WriteBeginProjectedNamespace(); + + foreach (TypeDefinition type in members.Types) + { + if (!_settings.Filter.Includes(type)) { continue; } + string ns2 = type.Namespace?.Value ?? string.Empty; + string nm2 = type.Name?.Value ?? string.Empty; + // Skip generic types and mapped types (mirrors C++ logic) + if (MappedTypes.Get(ns2, nm2) is not null || TypeCategorization.IsGeneric(type)) + { + written = true; + continue; + } + + // Write the projected type per category + TypeCategory category = TypeCategorization.GetCategory(type); + CodeWriters.WriteType(w, type, category, _settings, _cache); + + if (category == TypeCategory.Class && !TypeCategorization.IsAttributeType(type)) + { + CodeWriters.AddDefaultInterfaceEntry(w, type, defaultInterfaceEntries); + CodeWriters.AddExclusiveToInterfaceEntries(w, type, exclusiveToInterfaceEntries); + CodeWriters.AddMetadataTypeEntry(w, type, authoredTypeNameToMetadataMap); + if (_settings.Component && componentActivatable.Contains(type)) + { + CodeWriters.WriteFactoryClass(w, type); + } + } + else if (category is TypeCategory.Delegate or TypeCategory.Enum or TypeCategory.Interface) + { + CodeWriters.AddMetadataTypeEntry(w, type, authoredTypeNameToMetadataMap); + } + else if (category == TypeCategory.Struct && !TypeCategorization.IsApiContractType(type)) + { + CodeWriters.AddMetadataTypeEntry(w, type, authoredTypeNameToMetadataMap); + } + + written = true; + } + + w.WriteEndProjectedNamespace(); + + if (!written) + { + return false; + } + + // Phase 3: ABI types (when not reference projection) + if (!_settings.ReferenceProjection) + { + // Collect factory interfaces (Static/Activatable/Composable) referenced by classes + // included in this namespace. These must have their ABI Methods classes emitted even + // when the filter excludes them, because the projected static class members dispatch + // through them. Mirrors C++ behavior of always emitting factory interface ABI for + // included classes. + HashSet factoryInterfacesInThisNs = new(); + foreach (TypeDefinition type in members.Types) + { + if (!_settings.Filter.Includes(type)) { continue; } + if (TypeCategorization.GetCategory(type) != TypeCategory.Class) { continue; } + foreach (KeyValuePair kv in AttributedTypes.Get(type, _cache)) + { + AttributedType info = kv.Value; + TypeDefinition? facType = info.Type; + if (facType is null) { continue; } + // Only consider factory interfaces in the same namespace as we're processing. + string facNs = facType.Namespace?.Value ?? string.Empty; + if (facNs != ns) { continue; } + _ = factoryInterfacesInThisNs.Add(facType); + } + } + + w.WriteBeginAbiNamespace(); + foreach (TypeDefinition type in members.Types) + { + bool isFactoryInterface = factoryInterfacesInThisNs.Contains(type); + if (!_settings.Filter.Includes(type) && !isFactoryInterface) { continue; } + if (TypeCategorization.IsGeneric(type)) { continue; } + string ns2 = type.Namespace?.Value ?? string.Empty; + string nm2 = type.Name?.Value ?? string.Empty; + MappedType? m = MappedTypes.Get(ns2, nm2); + if (m is not null && !m.EmitAbi) { continue; } + if (TypeCategorization.IsApiContractType(type)) { continue; } + if (TypeCategorization.IsAttributeType(type)) { continue; } + + TypeCategory category = TypeCategorization.GetCategory(type); + CodeWriters.WriteAbiType(w, type, category, _settings); + } + w.WriteEndAbiNamespace(); + } + + // Phase 4: Custom additions to namespaces (mirrors C++ main.cpp) + foreach ((string addNs, string resName) in Additions.All) + { + if (addNs == ns && _settings.AdditionFilter.Includes(ns)) + { + using System.IO.Stream? stream = typeof(ProjectionWriter).Assembly.GetManifestResourceStream(resName); + if (stream is null) { continue; } + using System.IO.StreamReader reader = new(stream); + string content = reader.ReadToEnd(); + w.Write(content); + } + } + + // Output to file + string filename = ns + ".cs"; + string fullPath = Path.Combine(_settings.OutputFolder, filename); + w.FlushToFile(fullPath); + return true; + } + + /// + /// Writes the embedded string resources (e.g., ComInteropExtensions.cs, InspectableVftbl.cs) + /// to the output folder. + /// + private void WriteBaseStrings() + { + Assembly asm = typeof(ProjectionWriter).Assembly; + foreach (string resName in asm.GetManifestResourceNames()) + { + // Resource names look like 'WindowsRuntime.ProjectionGenerator.Writer.Resources.Base.ComInteropExtensions.cs' + if (!resName.Contains(".Resources.Base.")) + { + continue; + } + // Skip ComInteropExtensions if Windows is not included + string fileName = resName[(resName.IndexOf(".Resources.Base.", StringComparison.Ordinal) + ".Resources.Base.".Length)..]; + if (fileName == "ComInteropExtensions.cs" && !_settings.Filter.Includes("Windows")) + { + continue; + } + + using Stream stream = asm.GetManifestResourceStream(resName)!; + using StreamReader reader = new(stream); + string content = reader.ReadToEnd(); + + // For ComInteropExtensions, prepend the UAC_VERSION define + if (fileName == "ComInteropExtensions.cs") + { + int uapContractVersion = _cache.Find("Windows.Graphics.Display.DisplayInformation") is not null ? 15 : 7; + content = $"#define UAC_VERSION_{uapContractVersion}\n" + content; + } + + // Mirror the C++ tool: every emitted .cs file gets the auto-generated header. + // See main.cpp where 'write_file_header(ws);' is called before each base string is written. + TextWriter headerWriter = new(); + CodeWriters.WriteFileHeader(headerWriter); + string header = headerWriter.FlushToString(); + + string outPath = Path.Combine(_settings.OutputFolder, fileName); + File.WriteAllText(outPath, header + content); + } + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/Additions.cs b/src/WinRT.Projection.Generator.Writer/Helpers/Additions.cs new file mode 100644 index 0000000000..168ffd98b0 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/Additions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Registry of namespace addition files. Mirrors the C++ strings::additions array. +/// Each addition is the content of a .cs file that gets appended to the +/// projection of the matching namespace. +/// +internal static class Additions +{ + /// + /// (namespace, embedded-resource-manifest-name) pairs. The manifest-name resolves to a + /// call. + /// + public static readonly IReadOnlyList<(string Namespace, string ResourceName)> All = new (string, string)[] + { + ("Microsoft.UI.Dispatching", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Dispatching.Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.CornerRadius.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.Duration.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.GridLength.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.SR.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.Thickness.cs"), + ("Microsoft.UI.Xaml.Controls.Primitives", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Controls.Primitives.Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs"), + ("Microsoft.UI.Xaml.Media", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Media.Microsoft.UI.Xaml.Media.Matrix.cs"), + ("Microsoft.UI.Xaml.Media.Animation", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Media.Animation.Microsoft.UI.Xaml.Media.Animation.KeyTime.cs"), + ("Microsoft.UI.Xaml.Media.Animation", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Media.Animation.Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs"), + ("Microsoft.UI.Xaml.Media.Media3D", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Microsoft.UI.Xaml.Media.Media3D.Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs"), + ("Windows.Storage", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.Storage.WindowsRuntimeStorageExtensions.cs"), + ("Windows.UI", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Windows.UI.Color.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Windows.System.DispatcherQueueSynchronizationContext.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.CornerRadius.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.Duration.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.GridLength.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.SR.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.Thickness.cs"), + ("Windows.UI.Xaml.Controls.Primitives", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Controls.Primitives.Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs"), + ("Windows.UI.Xaml.Media", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Media.Windows.UI.Xaml.Media.Matrix.cs"), + ("Windows.UI.Xaml.Media.Animation", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Media.Animation.Windows.UI.Xaml.Media.Animation.KeyTime.cs"), + ("Windows.UI.Xaml.Media.Animation", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Media.Animation.Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs"), + ("Windows.UI.Xaml.Media.Media3D", "WindowsRuntime.ProjectionGenerator.Writer.Resources.Additions.Windows.UI.Xaml.Media.Media3D.Windows.UI.Xaml.Media.Media3D.Matrix3D.cs"), + }; +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/AttributedTypes.cs b/src/WinRT.Projection.Generator.Writer/Helpers/AttributedTypes.cs new file mode 100644 index 0000000000..536f7d88e9 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/AttributedTypes.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Information about an [Activatable]/[Static]/[Composable] factory interface. +/// Mirrors C++ attributed_type. +/// +internal sealed class AttributedType +{ + public TypeDefinition? Type { get; set; } + public bool Activatable { get; set; } + public bool Statics { get; set; } + public bool Composable { get; set; } + public bool Visible { get; set; } +} + +/// +/// Helpers for activator/static/composable factory enumeration. Mirrors C++ get_attributed_types. +/// +internal static class AttributedTypes +{ + /// + /// Returns the (interface_name, AttributedType) entries for the given runtime class type. + /// Mirrors C++ get_attributed_types. + /// + public static IEnumerable> Get(TypeDefinition type, MetadataCache cache) + { + Dictionary result = new(System.StringComparer.Ordinal); + + for (int i = 0; i < type.CustomAttributes.Count; i++) + { + CustomAttribute attr = type.CustomAttributes[i]; + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + if (attrType is null) { continue; } + string ns = attrType.Namespace?.Value ?? string.Empty; + string name = attrType.Name?.Value ?? string.Empty; + if (ns != "Windows.Foundation.Metadata") { continue; } + + AttributedType info = new(); + switch (name) + { + case "ActivatableAttribute": + info.Type = GetSystemType(attr, cache); + info.Activatable = true; + break; + case "StaticAttribute": + info.Type = GetSystemType(attr, cache); + info.Statics = true; + break; + case "ComposableAttribute": + info.Type = GetSystemType(attr, cache); + info.Composable = true; + info.Visible = GetVisibility(attr) == 2; + break; + default: + continue; + } + + string key = info.Type?.Name?.Value ?? string.Empty; + result[key] = info; + } + + // C++ uses std::map which iterates in sorted-by-key order. + // The key is the factory-interface type name (e.g. 'IButtonUtilsStatic'), so the inheritance + // order in the generated code is alphabetical by interface name. + SortedDictionary sorted = new(System.StringComparer.Ordinal); + foreach (KeyValuePair kv in result) + { + sorted[kv.Key] = kv.Value; + } + return sorted; + } + + /// + /// Extracts the System.Type argument from the attribute's fixed args. + /// + private static TypeDefinition? GetSystemType(CustomAttribute attr, MetadataCache cache) + { + if (attr.Signature is null) { return null; } + for (int i = 0; i < attr.Signature.FixedArguments.Count; i++) + { + CustomAttributeArgument arg = attr.Signature.FixedArguments[i]; + // For System.Type args in WinMD, the value is typically a TypeSignature + if (arg.Element is TypeSignature sig) + { + string typeName = sig.FullName ?? string.Empty; + TypeDefinition? td = cache.Find(typeName); + if (td is not null) { return td; } + } + else if (arg.Element is string s) + { + TypeDefinition? td = cache.Find(s); + if (td is not null) { return td; } + } + } + return null; + } + + /// + /// Extracts the visibility int from a [ComposableAttribute] (the enum value arg). + /// + private static int GetVisibility(CustomAttribute attr) + { + if (attr.Signature is { FixedArguments: [_, { Element: int e }, ..] }) + { + return e; + } + return 0; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/ContractPlatforms.cs b/src/WinRT.Projection.Generator.Writer/Helpers/ContractPlatforms.cs new file mode 100644 index 0000000000..e21c0a4bd7 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/ContractPlatforms.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Maps Windows Runtime API contracts to their first available Windows SDK platform version. +/// Mirrors the C++ contract_mappings table in helpers.h. +/// +internal static class ContractPlatforms +{ + private static readonly Dictionary> s_table = Build(); + + /// + /// Returns the platform version (e.g., "10.0.17763.0") that introduced the given contract version, + /// or empty string if not found. + /// + public static string GetPlatform(string contractName, int contractVersion) + { + if (!s_table.TryGetValue(contractName, out List<(int Version, string Platform)>? versions)) + { + return string.Empty; + } + // Find the first version >= contractVersion (mirrors std::lower_bound) + for (int i = 0; i < versions.Count; i++) + { + if (versions[i].Version >= contractVersion) + { + return versions[i].Platform; + } + } + return string.Empty; + } + + private static Dictionary> Build() + { + Dictionary> t = new(); + + void Add(string name, params (int v, string p)[] vs) + { + List<(int, string)> list = new(); + foreach (var (v, p) in vs) { list.Add((v, p)); } + t[name] = list; + } + + Add("Windows.AI.MachineLearning.MachineLearningContract", + (1, "10.0.17763.0"), (2, "10.0.18362.0"), (3, "10.0.19041.0"), (4, "10.0.20348.0"), (5, "10.0.22000.0")); + Add("Windows.AI.MachineLearning.Preview.MachineLearningPreviewContract", + (1, "10.0.17134.0"), (2, "10.0.17763.0")); + Add("Windows.ApplicationModel.Calls.Background.CallsBackgroundContract", + (1, "10.0.17763.0"), (2, "10.0.18362.0"), (3, "10.0.20348.0"), (4, "10.0.22621.0")); + Add("Windows.ApplicationModel.Calls.CallsPhoneContract", + (4, "10.0.17763.0"), (5, "10.0.18362.0"), (6, "10.0.20348.0"), (7, "10.0.22621.0")); + Add("Windows.ApplicationModel.Calls.CallsVoipContract", + (1, "10.0.10586.0"), (2, "10.0.16299.0"), (3, "10.0.17134.0"), (4, "10.0.17763.0"), (5, "10.0.26100.0")); + Add("Windows.ApplicationModel.CommunicationBlocking.CommunicationBlockingContract", + (2, "10.0.17763.0")); + Add("Windows.ApplicationModel.SocialInfo.SocialInfoContract", + (1, "10.0.14393.0"), (2, "10.0.15063.0")); + Add("Windows.ApplicationModel.StartupTaskContract", + (2, "10.0.16299.0"), (3, "10.0.17134.0")); + Add("Windows.Devices.Custom.CustomDeviceContract", + (1, "10.0.16299.0")); + Add("Windows.Devices.DevicesLowLevelContract", + (2, "10.0.14393.0"), (3, "10.0.15063.0")); + Add("Windows.Devices.Printers.PrintersContract", + (1, "10.0.10586.0")); + Add("Windows.Devices.SmartCards.SmartCardBackgroundTriggerContract", + (3, "10.0.16299.0")); + Add("Windows.Devices.SmartCards.SmartCardEmulatorContract", + (5, "10.0.16299.0"), (6, "10.0.17763.0")); + Add("Windows.Foundation.FoundationContract", + (1, "10.0.10240.0"), (2, "10.0.10586.0"), (3, "10.0.15063.0"), (4, "10.0.19041.0")); + Add("Windows.Foundation.UniversalApiContract", + (1, "10.0.10240.0"), (2, "10.0.10586.0"), (3, "10.0.14393.0"), (4, "10.0.15063.0"), (5, "10.0.16299.0"), + (6, "10.0.17134.0"), (7, "10.0.17763.0"), (8, "10.0.18362.0"), (10, "10.0.19041.0"), (12, "10.0.20348.0"), + (14, "10.0.22000.0"), (15, "10.0.22621.0"), (19, "10.0.26100.0")); + Add("Windows.Foundation.VelocityIntegration.VelocityIntegrationContract", + (1, "10.0.17134.0")); + Add("Windows.Gaming.XboxLive.StorageApiContract", + (1, "10.0.16299.0")); + Add("Windows.Graphics.Printing3D.Printing3DContract", + (2, "10.0.10586.0"), (3, "10.0.14393.0"), (4, "10.0.16299.0")); + Add("Windows.Networking.Connectivity.WwanContract", + (1, "10.0.10240.0"), (2, "10.0.17134.0"), (3, "10.0.26100.0")); + Add("Windows.Networking.Sockets.ControlChannelTriggerContract", + (3, "10.0.17763.0")); + Add("Windows.Security.Isolation.IsolatedWindowsEnvironmentContract", + (1, "10.0.19041.0"), (3, "10.0.20348.0"), (4, "10.0.22621.0"), (5, "10.0.26100.0")); + Add("Windows.Services.Maps.GuidanceContract", + (3, "10.0.17763.0")); + Add("Windows.Services.Maps.LocalSearchContract", + (4, "10.0.17763.0")); + Add("Windows.Services.Store.StoreContract", + (1, "10.0.14393.0"), (2, "10.0.15063.0"), (3, "10.0.17134.0"), (4, "10.0.17763.0")); + Add("Windows.Services.TargetedContent.TargetedContentContract", + (1, "10.0.15063.0")); + Add("Windows.Storage.Provider.CloudFilesContract", + (4, "10.0.19041.0"), (6, "10.0.20348.0"), (7, "10.0.22621.0")); + Add("Windows.System.Profile.ProfileHardwareTokenContract", + (1, "10.0.14393.0")); + Add("Windows.System.Profile.ProfileRetailInfoContract", + (1, "10.0.20348.0")); + Add("Windows.System.Profile.ProfileSharedModeContract", + (1, "10.0.14393.0"), (2, "10.0.15063.0")); + Add("Windows.System.Profile.SystemManufacturers.SystemManufacturersContract", + (3, "10.0.17763.0")); + Add("Windows.System.SystemManagementContract", + (6, "10.0.17763.0"), (7, "10.0.19041.0")); + Add("Windows.UI.UIAutomation.UIAutomationContract", + (1, "10.0.20348.0"), (2, "10.0.22000.0")); + Add("Windows.UI.ViewManagement.ViewManagementViewScalingContract", + (1, "10.0.14393.0")); + Add("Windows.UI.Xaml.Core.Direct.XamlDirectContract", + (1, "10.0.17763.0"), (2, "10.0.18362.0"), (3, "10.0.20348.0"), (5, "10.0.22000.0")); + + return t; + } +} + +/// +/// Static lookup for namespaces with addition files. Mirrors C++ has_addition_to_type. +/// +internal static class AdditionTypes +{ + private static readonly Dictionary> s_table = new(System.StringComparer.Ordinal) + { + { "Microsoft.UI.Xaml", new(System.StringComparer.Ordinal) { "Thickness" } }, + { "Microsoft.UI.Xaml.Controls.Primitives", new(System.StringComparer.Ordinal) { "GeneratorPosition" } }, + { "Microsoft.UI.Xaml.Media", new(System.StringComparer.Ordinal) { "Matrix" } }, + { "Microsoft.UI.Xaml.Media.Animation", new(System.StringComparer.Ordinal) { "KeyTime" } }, + { "Windows.UI", new(System.StringComparer.Ordinal) { "Color" } }, + { "Windows.UI.Xaml", new(System.StringComparer.Ordinal) { "Thickness" } }, + { "Windows.UI.Xaml.Controls.Primitives", new(System.StringComparer.Ordinal) { "GeneratorPosition" } }, + { "Windows.UI.Xaml.Media", new(System.StringComparer.Ordinal) { "Matrix" } }, + { "Windows.UI.Xaml.Media.Animation", new(System.StringComparer.Ordinal) { "KeyTime" } }, + }; + + /// Mirrors C++ has_addition_to_type. + public static bool HasAdditionToType(string typeNamespace, string typeName) + { + if (s_table.TryGetValue(typeNamespace, out HashSet? names) && names.Contains(typeName)) + { + return true; + } + return false; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/GuidGenerator.cs b/src/WinRT.Projection.Generator.Writer/Helpers/GuidGenerator.cs new file mode 100644 index 0000000000..0535031628 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/GuidGenerator.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Security.Cryptography; +using System.Text; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ guid_generator.h. Generates Windows Runtime parameterized GUIDs (PIIDs) +/// using the WinRT-defined namespace GUID (d57af411-737b-c042-abae-878b1e16adee) and SHA-1. +/// +internal static class GuidGenerator +{ + // The WinRT namespace GUID, in the same byte order as cppwinrt's namespace_guid. + // Per cppwinrt: { 0xd57af411, 0x737b, 0xc042, { 0xab, 0xae, 0x87, 0x8b, 0x1e, 0x16, 0xad, 0xee } } + // Layout (little-endian format): bytes are { 0x11, 0xf4, 0x7a, 0xd5, 0x7b, 0x73, 0x42, 0xc0, 0xab, 0xae, 0x87, 0x8b, 0x1e, 0x16, 0xad, 0xee } + private static readonly byte[] s_namespaceBytes = + { + 0x11, 0xf4, 0x7a, 0xd5, + 0x7b, 0x73, + 0x42, 0xc0, + 0xab, 0xae, 0x87, 0x8b, 0x1e, 0x16, 0xad, 0xee + }; + + /// + /// Generates a GUID for the given Windows Runtime parameterized type signature. + /// + /// The parameterized signature (e.g., "pinterface({...};Boolean)"). + /// The resulting GUID. + public static Guid Generate(string signature) + { + byte[] sigBytes = Encoding.UTF8.GetBytes(signature); + byte[] buffer = new byte[s_namespaceBytes.Length + sigBytes.Length]; + Array.Copy(s_namespaceBytes, buffer, s_namespaceBytes.Length); + Array.Copy(sigBytes, 0, buffer, s_namespaceBytes.Length, sigBytes.Length); + + byte[] hash = SHA1.HashData(buffer); + + // Take first 16 bytes + byte[] guidBytes = new byte[16]; + Array.Copy(hash, guidBytes, 16); + + // Endian swap (Data1, Data2, Data3 are in big-endian in the SHA1 hash; .NET Guid expects little-endian) + Swap(guidBytes, 0, 3); + Swap(guidBytes, 1, 2); + Swap(guidBytes, 4, 5); + Swap(guidBytes, 6, 7); + + // Set named GUID fields: version 5, variant RFC4122 + guidBytes[7] = (byte)((guidBytes[7] & 0x0f) | 0x50); + guidBytes[8] = (byte)((guidBytes[8] & 0x3f) | 0x80); + + return new Guid(guidBytes); + } + + private static void Swap(byte[] arr, int a, int b) + { + (arr[a], arr[b]) = (arr[b], arr[a]); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/Helpers.cs b/src/WinRT.Projection.Generator.Writer/Helpers/Helpers.cs new file mode 100644 index 0000000000..63271bad1b --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/Helpers.cs @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Collections; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// General-purpose helpers from C++ helpers.h and code_writers.h. +/// +internal static class Helpers +{ + private static readonly HashSet s_csharpKeywords = new(System.StringComparer.Ordinal) + { + "abstract","as","base","bool","break","byte","case","catch","char","checked","class","const","continue", + "decimal","default","delegate","do","double","else","enum","event","explicit","extern","false","finally", + "fixed","float","for","foreach","goto","if","implicit","in","int","interface","internal","is","lock","long", + "namespace","new","null","object","operator","out","override","params","private","protected","public", + "readonly","ref","return","sbyte","sealed","short","sizeof","stackalloc","static","string","struct","switch", + "this","throw","true","try","typeof","uint","ulong","unchecked","unsafe","ushort","using","virtual","void", + "volatile","while" + }; + + /// Mirrors C++ is_keyword. + public static bool IsKeyword(string s) => s_csharpKeywords.Contains(s); + + /// Mirrors C++ write_escaped_identifier: prefix C# keywords with @. + public static void WriteEscapedIdentifier(TextWriter w, string identifier) + { + if (IsKeyword(identifier)) + { + w.Write("@"); + } + w.Write(identifier); + } + + /// Mirrors C++ internal_accessibility. + public static string InternalAccessibility(Settings settings) => + settings.Internal || settings.Embedded ? "internal" : "public"; + + /// + /// Returns the type referenced by an [ExclusiveTo] attribute on the given interface, + /// or null if the interface is not exclusive-to anything (or the attribute argument + /// can't be resolved). Mirrors the C++ logic that walks an interface's + /// Windows.Foundation.Metadata.ExclusiveToAttribute and reads its System.Type argument. + /// + public static TypeDefinition? GetExclusiveToType(TypeDefinition iface, MetadataCache cache) + { + for (int i = 0; i < iface.CustomAttributes.Count; i++) + { + CustomAttribute attr = iface.CustomAttributes[i]; + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + if (attrType is null) { continue; } + string ns = attrType.Namespace?.Value ?? string.Empty; + string name = attrType.Name?.Value ?? string.Empty; + if (ns != "Windows.Foundation.Metadata" || name != "ExclusiveToAttribute") { continue; } + if (attr.Signature is null) { continue; } + for (int j = 0; j < attr.Signature.FixedArguments.Count; j++) + { + CustomAttributeArgument arg = attr.Signature.FixedArguments[j]; + if (arg.Element is TypeSignature sig) + { + string typeName = sig.FullName ?? string.Empty; + TypeDefinition? td = cache.Find(typeName); + if (td is not null) { return td; } + } + else if (arg.Element is AsmResolver.Utf8String s) + { + TypeDefinition? td = cache.Find(s.Value); + if (td is not null) { return td; } + } + else if (arg.Element is string ss) + { + TypeDefinition? td = cache.Find(ss); + if (td is not null) { return td; } + } + } + } + return null; + } + + /// Strip everything from a backtick onwards (C++ write_code behavior for type names). + public static string StripBackticks(string typeName) + { + int idx = typeName.IndexOf('`'); + return idx >= 0 ? typeName.Substring(0, idx) : typeName; + } + + /// Returns true if the type has the named CustomAttribute. + public static bool HasAttribute(IHasCustomAttribute member, string ns, string name) + => TypeCategorization.HasAttribute(member, ns, name); + + /// Returns the matching CustomAttribute, or null. + public static CustomAttribute? GetAttribute(IHasCustomAttribute member, string ns, string name) + => TypeCategorization.GetAttribute(member, ns, name); + + /// Returns true if the InterfaceImpl is the [Default] interface. + public static bool IsDefaultInterface(InterfaceImplementation impl) + => HasAttribute(impl, "Windows.Foundation.Metadata", "DefaultAttribute"); + + /// Returns true if the InterfaceImpl is [Overridable]. + public static bool IsOverridable(InterfaceImplementation impl) + => HasAttribute(impl, "Windows.Foundation.Metadata", "OverridableAttribute"); + + /// True if a method is the special "remove_xxx" event remover (mirrors C++ is_remove_overload). + public static bool IsRemoveOverload(MethodDefinition m) + => m.IsSpecialName && (m.Name?.Value?.StartsWith("remove_", System.StringComparison.Ordinal) == true); + + /// Method has [NoExceptionAttribute] or is a remove overload. + public static bool IsNoExcept(MethodDefinition m) + => IsRemoveOverload(m) || HasAttribute(m, "Windows.Foundation.Metadata", "NoExceptionAttribute"); + + /// Property has [NoExceptionAttribute]. + public static bool IsNoExcept(PropertyDefinition p) + => HasAttribute(p, "Windows.Foundation.Metadata", "NoExceptionAttribute"); + + /// Mirrors C++ get_default_interface: returns the [Default] interface. + public static ITypeDefOrRef? GetDefaultInterface(TypeDefinition type) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (IsDefaultInterface(impl) && impl.Interface is not null) + { + return impl.Interface; + } + } + return null; + } + + /// Mirrors C++ get_property_methods: returns (getter, setter) for a property. + public static (MethodDefinition? Getter, MethodDefinition? Setter) GetPropertyMethods(PropertyDefinition prop) + { + return (prop.GetMethod, prop.SetMethod); + } + + /// Mirrors C++ get_event_methods: returns (add, remove) for an event. + public static (MethodDefinition? Add, MethodDefinition? Remove) GetEventMethods(EventDefinition evt) + { + return (evt.AddMethod, evt.RemoveMethod); + } + + /// Mirrors C++ get_delegate_invoke: returns the Invoke method of a delegate type. + public static MethodDefinition? GetDelegateInvoke(TypeDefinition type) + { + foreach (MethodDefinition m in type.Methods) + { + if (m.IsSpecialName && m.Name == "Invoke") + { + return m; + } + } + return null; + } + + /// Get the (uint32_t arg) value out of a [ContractVersionAttribute] (mirrors C++ get_contract_version). + public static int? GetContractVersion(TypeDefinition type) + { + CustomAttribute? attr = GetAttribute(type, "Windows.Foundation.Metadata", "ContractVersionAttribute"); + if (attr is null) { return null; } + // C++ reads index 1 - the second positional arg + if (attr.Signature is not null && attr.Signature.FixedArguments.Count > 1) + { + object? v = attr.Signature.FixedArguments[1].Element; + if (v is uint u) { return (int)u; } + if (v is int i) { return i; } + } + return null; + } + + /// Get the (uint32_t arg) value out of a [VersionAttribute] (mirrors C++ get_version). + public static int? GetVersion(TypeDefinition type) + { + CustomAttribute? attr = GetAttribute(type, "Windows.Foundation.Metadata", "VersionAttribute"); + if (attr is null) { return null; } + if (attr.Signature is not null && attr.Signature.FixedArguments.Count > 0) + { + object? v = attr.Signature.FixedArguments[0].Element; + if (v is uint u) { return (int)u; } + if (v is int i) { return i; } + } + return null; + } + + /// Mirrors C++ has_default_constructor. + public static bool HasDefaultConstructor(TypeDefinition type) + { + foreach (MethodDefinition m in type.Methods) + { + if (m.IsRuntimeSpecialName && m.Name == ".ctor" && m.Parameters.Count == 0) + { + return true; + } + } + return false; + } + + /// Mirrors C++ is_constructor. + public static bool IsConstructor(MethodDefinition m) + => m.IsRuntimeSpecialName && m.Name == ".ctor"; + + /// Mirrors C++ is_special. + public static bool IsSpecial(MethodDefinition m) + => m.IsSpecialName || m.IsRuntimeSpecialName; +} + +/// +/// Mirrors C++ method_signature: enumerates parameters and return value of a method. +/// +internal sealed class MethodSig +{ + public MethodDefinition Method { get; } + public List Params { get; } + public ParameterDefinition? ReturnParam { get; } + + public MethodSig(MethodDefinition method) : this(method, null) { } + + public MethodSig(MethodDefinition method, AsmResolver.DotNet.Signatures.GenericContext? genCtx) + { + Method = method; + Params = new List(method.Parameters.Count); + // The return parameter is the one with sequence 0 (if any) + ReturnParam = null; + foreach (ParameterDefinition p in method.ParameterDefinitions) + { + if (p.Sequence == 0) + { + ReturnParam = p; + break; + } + } + + // Iterate signature parameters + if (method.Signature is MethodSignature sig) + { + _substitutedReturnType = genCtx is not null && sig.ReturnType is not null + ? sig.ReturnType.InstantiateGenericTypes(genCtx.Value) + : sig.ReturnType; + for (int i = 0; i < sig.ParameterTypes.Count; i++) + { + TypeSignature pt = sig.ParameterTypes[i]; + if (genCtx is not null) { pt = pt.InstantiateGenericTypes(genCtx.Value); } + Params.Add(new ParamInfo(method.Parameters[i], pt)); + } + } + } + +#pragma warning disable IDE0032 // Use auto property — manual backing field needed for substituted return type + private readonly TypeSignature? _substitutedReturnType; +#pragma warning restore IDE0032 + + public TypeSignature? ReturnType => _substitutedReturnType is TypeSignature t && + t is not CorLibTypeSignature { ElementType: ElementType.Void } + ? _substitutedReturnType + : null; + + public string ReturnParamName(string defaultName = "__return_value__") + => ReturnParam?.Name?.Value ?? defaultName; +} + +/// One param: links the parameter definition to its signature type. +internal sealed record ParamInfo(Parameter Parameter, TypeSignature Type); + +/// Param category mirroring C++ param_category. +internal enum ParamCategory +{ + In, + Ref, + Out, + PassArray, + FillArray, + ReceiveArray, +} + +/// Helpers for parameter analysis. +internal static class ParamHelpers +{ + public static ParamCategory GetParamCategory(ParamInfo p) + { + bool isArray = p.Type is SzArrayTypeSignature; + bool isOut = p.Parameter.Definition?.IsOut == true; + bool isIn = p.Parameter.Definition?.IsIn == true; + // Check both the captured signature type and the parameter's own type (handles cases where + // the signature is wrapped in a ByReferenceTypeSignature only on one side after substitution). + // Also peel custom modifiers (e.g. modreq[InAttribute]) which can hide a ByRef beneath. + bool isByRef = IsByRefType(p.Type) || IsByRefType(p.Parameter.ParameterType); + // If byref and underlying is an array, treat as array param (PassArray/ReceiveArray/FillArray) + // based on in/out flags. WinRT metadata represents 'out byte[]' as 'byte[]&' with [out]. + bool isByRefArray = isByRef && PeelByRefAndCustomModifiers(p.Type) is SzArrayTypeSignature; + if (isArray || isByRefArray) + { + if (isIn) { return ParamCategory.PassArray; } + if (isByRef && isOut) { return ParamCategory.ReceiveArray; } + return ParamCategory.FillArray; + } + if (isOut) { return ParamCategory.Out; } + if (isByRef) { return ParamCategory.Ref; } + return ParamCategory.In; + } + + private static TypeSignature? PeelByRefAndCustomModifiers(TypeSignature? sig) + { + TypeSignature? cur = sig; + while (true) + { + if (cur is CustomModifierTypeSignature cm) { cur = cm.BaseType; continue; } + if (cur is ByReferenceTypeSignature br) { cur = br.BaseType; continue; } + break; + } + return cur; + } + + private static bool IsByRefType(TypeSignature? sig) + { + // Strip custom modifiers (e.g. modreq[InAttribute] or modopt[IsExternalInit]) before checking byref. + TypeSignature? cur = sig; + while (cur is CustomModifierTypeSignature cm) + { + cur = cm.BaseType; + } + return cur is ByReferenceTypeSignature; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/MappedTypes.cs b/src/WinRT.Projection.Generator.Writer/Helpers/MappedTypes.cs new file mode 100644 index 0000000000..79dae1b2fd --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/MappedTypes.cs @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ mapped_type struct in helpers.h. +/// Maps a Windows Runtime type to the corresponding .NET type. +/// +internal sealed record MappedType( + string AbiName, + string MappedNamespace, + string MappedName, + bool RequiresMarshaling = false, + bool HasCustomMembersOutput = false, + bool EmitAbi = false); + +/// +/// Static lookup table for Windows Runtime → .NET type mappings (from helpers.h). +/// +internal static class MappedTypes +{ + private static readonly Dictionary> _byNamespace = Build(); + + public static MappedType? Get(string typeNamespace, string typeName) + { + if (_byNamespace.TryGetValue(typeNamespace, out Dictionary? namesp) && + namesp.TryGetValue(typeName, out MappedType? mapped)) + { + return mapped; + } + return null; + } + + public static bool HasNamespace(string typeNamespace) => _byNamespace.ContainsKey(typeNamespace); + + private static Dictionary> Build() + { + Dictionary> result = new(System.StringComparer.Ordinal); + + // helper to add a type entry + void Add(string ns, MappedType mt) + { + if (!result.TryGetValue(ns, out Dictionary? bag)) + { + bag = new Dictionary(System.StringComparer.Ordinal); + result[ns] = bag; + } + bag[mt.AbiName] = mt; + } + + // Microsoft.UI.Xaml + Add("Microsoft.UI.Xaml", new("CornerRadius", "Microsoft.UI.Xaml", "CornerRadius", false, false, true)); + Add("Microsoft.UI.Xaml", new("CornerRadiusHelper", "", "")); + Add("Microsoft.UI.Xaml", new("Duration", "Microsoft.UI.Xaml", "Duration", false, false, true)); + Add("Microsoft.UI.Xaml", new("DurationHelper", "", "")); + Add("Microsoft.UI.Xaml", new("GridLength", "Microsoft.UI.Xaml", "GridLength", false, false, true)); + Add("Microsoft.UI.Xaml", new("GridLengthHelper", "", "")); + Add("Microsoft.UI.Xaml", new("ICornerRadiusHelper", "", "")); + Add("Microsoft.UI.Xaml", new("ICornerRadiusHelperStatics", "", "")); + Add("Microsoft.UI.Xaml", new("IDurationHelper", "", "")); + Add("Microsoft.UI.Xaml", new("IDurationHelperStatics", "", "")); + Add("Microsoft.UI.Xaml", new("IGridLengthHelper", "", "")); + Add("Microsoft.UI.Xaml", new("IGridLengthHelperStatics", "", "")); + Add("Microsoft.UI.Xaml", new("IThicknessHelper", "", "")); + Add("Microsoft.UI.Xaml", new("IThicknessHelperStatics", "", "")); + Add("Microsoft.UI.Xaml", new("IXamlServiceProvider", "System", "IServiceProvider")); + Add("Microsoft.UI.Xaml", new("ThicknessHelper", "", "")); + + // Microsoft.UI.Xaml.Controls.Primitives + Add("Microsoft.UI.Xaml.Controls.Primitives", new("GeneratorPositionHelper", "", "")); + Add("Microsoft.UI.Xaml.Controls.Primitives", new("IGeneratorPositionHelper", "", "")); + Add("Microsoft.UI.Xaml.Controls.Primitives", new("IGeneratorPositionHelperStatics", "", "")); + + // Microsoft.UI.Xaml.Data + Add("Microsoft.UI.Xaml.Data", new("DataErrorsChangedEventArgs", "System.ComponentModel", "DataErrorsChangedEventArgs")); + Add("Microsoft.UI.Xaml.Data", new("INotifyDataErrorInfo", "System.ComponentModel", "INotifyDataErrorInfo", true, true)); + Add("Microsoft.UI.Xaml.Data", new("INotifyPropertyChanged", "System.ComponentModel", "INotifyPropertyChanged")); + Add("Microsoft.UI.Xaml.Data", new("PropertyChangedEventArgs", "System.ComponentModel", "PropertyChangedEventArgs")); + Add("Microsoft.UI.Xaml.Data", new("PropertyChangedEventHandler", "System.ComponentModel", "PropertyChangedEventHandler")); + + // Microsoft.UI.Xaml.Input + Add("Microsoft.UI.Xaml.Input", new("ICommand", "System.Windows.Input", "ICommand", true)); + + // Microsoft.UI.Xaml.Interop + Add("Microsoft.UI.Xaml.Interop", new("IBindableIterable", "System.Collections", "IEnumerable", true, true)); + Add("Microsoft.UI.Xaml.Interop", new("IBindableIterator", "System.Collections", "IEnumerator", true, true)); + Add("Microsoft.UI.Xaml.Interop", new("IBindableVector", "System.Collections", "IList", true, true)); + Add("Microsoft.UI.Xaml.Interop", new("INotifyCollectionChanged", "System.Collections.Specialized", "INotifyCollectionChanged", true)); + Add("Microsoft.UI.Xaml.Interop", new("NotifyCollectionChangedAction", "System.Collections.Specialized", "NotifyCollectionChangedAction")); + Add("Microsoft.UI.Xaml.Interop", new("NotifyCollectionChangedEventArgs", "System.Collections.Specialized", "NotifyCollectionChangedEventArgs", true)); + Add("Microsoft.UI.Xaml.Interop", new("NotifyCollectionChangedEventHandler", "System.Collections.Specialized", "NotifyCollectionChangedEventHandler", true)); + + // Microsoft.UI.Xaml.Media + Add("Microsoft.UI.Xaml.Media", new("IMatrixHelper", "", "")); + Add("Microsoft.UI.Xaml.Media", new("IMatrixHelperStatics", "", "")); + Add("Microsoft.UI.Xaml.Media", new("MatrixHelper", "", "")); + + // Microsoft.UI.Xaml.Media.Animation + Add("Microsoft.UI.Xaml.Media.Animation", new("IKeyTimeHelper", "", "")); + Add("Microsoft.UI.Xaml.Media.Animation", new("IKeyTimeHelperStatics", "", "")); + Add("Microsoft.UI.Xaml.Media.Animation", new("IRepeatBehaviorHelper", "", "")); + Add("Microsoft.UI.Xaml.Media.Animation", new("IRepeatBehaviorHelperStatics", "", "")); + Add("Microsoft.UI.Xaml.Media.Animation", new("KeyTime", "Microsoft.UI.Xaml.Media.Animation", "KeyTime", false, false, true)); + Add("Microsoft.UI.Xaml.Media.Animation", new("KeyTimeHelper", "", "")); + Add("Microsoft.UI.Xaml.Media.Animation", new("RepeatBehavior", "Microsoft.UI.Xaml.Media.Animation", "RepeatBehavior", false, false, true)); + Add("Microsoft.UI.Xaml.Media.Animation", new("RepeatBehaviorHelper", "", "")); + + // Microsoft.UI.Xaml.Media.Media3D + Add("Microsoft.UI.Xaml.Media.Media3D", new("IMatrix3DHelper", "", "")); + Add("Microsoft.UI.Xaml.Media.Media3D", new("IMatrix3DHelperStatics", "", "")); + Add("Microsoft.UI.Xaml.Media.Media3D", new("Matrix3D", "Microsoft.UI.Xaml.Media.Media3D", "Matrix3D", false, false, true)); + Add("Microsoft.UI.Xaml.Media.Media3D", new("Matrix3DHelper", "", "")); + + // Windows.Foundation + Add("Windows.Foundation", new("AsyncActionCompletedHandler", "Windows.Foundation", "AsyncActionCompletedHandler")); + Add("Windows.Foundation", new("AsyncActionProgressHandler`1", "Windows.Foundation", "AsyncActionProgressHandler`1")); + Add("Windows.Foundation", new("AsyncActionWithProgressCompletedHandler`1", "Windows.Foundation", "AsyncActionWithProgressCompletedHandler`1")); + Add("Windows.Foundation", new("AsyncOperationCompletedHandler`1", "Windows.Foundation", "AsyncOperationCompletedHandler`1")); + Add("Windows.Foundation", new("AsyncOperationProgressHandler`2", "Windows.Foundation", "AsyncOperationProgressHandler`2")); + Add("Windows.Foundation", new("AsyncOperationWithProgressCompletedHandler`2", "Windows.Foundation", "AsyncOperationWithProgressCompletedHandler`2")); + Add("Windows.Foundation", new("AsyncStatus", "Windows.Foundation", "AsyncStatus")); + Add("Windows.Foundation", new("DateTime", "System", "DateTimeOffset", true)); + Add("Windows.Foundation", new("EventHandler`1", "System", "EventHandler`1", false)); + Add("Windows.Foundation", new("EventRegistrationToken", "WindowsRuntime.InteropServices", "EventRegistrationToken", false)); + Add("Windows.Foundation", new("FoundationContract", "Windows.Foundation", "FoundationContract")); + Add("Windows.Foundation", new("HResult", "System", "Exception", true)); + Add("Windows.Foundation", new("IAsyncAction", "Windows.Foundation", "IAsyncAction")); + Add("Windows.Foundation", new("IAsyncActionWithProgress`1", "Windows.Foundation", "IAsyncActionWithProgress`1")); + Add("Windows.Foundation", new("IAsyncInfo", "Windows.Foundation", "IAsyncInfo")); + Add("Windows.Foundation", new("IAsyncOperationWithProgress`2", "Windows.Foundation", "IAsyncOperationWithProgress`2")); + Add("Windows.Foundation", new("IAsyncOperation`1", "Windows.Foundation", "IAsyncOperation`1")); + Add("Windows.Foundation", new("IClosable", "System", "IDisposable", true, true)); + Add("Windows.Foundation", new("IMemoryBufferReference", "Windows.Foundation", "IMemoryBufferReference")); + Add("Windows.Foundation", new("IPropertyValue", "Windows.Foundation", "IPropertyValue", true)); + Add("Windows.Foundation", new("IReferenceArray`1", "Windows.Foundation", "IReferenceArray", true)); + Add("Windows.Foundation", new("IReference`1", "System", "Nullable`1", true)); + Add("Windows.Foundation", new("IStringable", "Windows.Foundation", "IStringable")); + Add("Windows.Foundation", new("Point", "Windows.Foundation", "Point")); + Add("Windows.Foundation", new("PropertyType", "Windows.Foundation", "PropertyType")); + Add("Windows.Foundation", new("Rect", "Windows.Foundation", "Rect")); + Add("Windows.Foundation", new("Size", "Windows.Foundation", "Size")); + Add("Windows.Foundation", new("TimeSpan", "System", "TimeSpan", true)); + Add("Windows.Foundation", new("TypedEventHandler`2", "System", "EventHandler`2", false)); + Add("Windows.Foundation", new("UniversalApiContract", "Windows.Foundation", "UniversalApiContract")); + Add("Windows.Foundation", new("Uri", "System", "Uri", true)); + + // Windows.Foundation.Collections + Add("Windows.Foundation.Collections", new("CollectionChange", "Windows.Foundation.Collections", "CollectionChange")); + Add("Windows.Foundation.Collections", new("IIterable`1", "System.Collections.Generic", "IEnumerable`1", true, true)); + Add("Windows.Foundation.Collections", new("IIterator`1", "System.Collections.Generic", "IEnumerator`1", true, true)); + Add("Windows.Foundation.Collections", new("IKeyValuePair`2", "System.Collections.Generic", "KeyValuePair`2", true)); + Add("Windows.Foundation.Collections", new("IMapChangedEventArgs`1", "Windows.Foundation.Collections", "IMapChangedEventArgs`1")); + Add("Windows.Foundation.Collections", new("IMapView`2", "System.Collections.Generic", "IReadOnlyDictionary`2", true, true)); + Add("Windows.Foundation.Collections", new("IMap`2", "System.Collections.Generic", "IDictionary`2", true, true)); + Add("Windows.Foundation.Collections", new("IObservableMap`2", "Windows.Foundation.Collections", "IObservableMap`2")); + Add("Windows.Foundation.Collections", new("IObservableVector`1", "Windows.Foundation.Collections", "IObservableVector`1")); + Add("Windows.Foundation.Collections", new("IVectorChangedEventArgs", "Windows.Foundation.Collections", "IVectorChangedEventArgs")); + Add("Windows.Foundation.Collections", new("IVectorView`1", "System.Collections.Generic", "IReadOnlyList`1", true, true)); + Add("Windows.Foundation.Collections", new("IVector`1", "System.Collections.Generic", "IList`1", true, true)); + Add("Windows.Foundation.Collections", new("MapChangedEventHandler`2", "Windows.Foundation.Collections", "MapChangedEventHandler`2")); + Add("Windows.Foundation.Collections", new("VectorChangedEventHandler`1", "Windows.Foundation.Collections", "VectorChangedEventHandler`1")); + + // Windows.Foundation.Metadata + Add("Windows.Foundation.Metadata", new("ApiContractAttribute", "Windows.Foundation.Metadata", "ApiContractAttribute")); + Add("Windows.Foundation.Metadata", new("AttributeTargets", "System", "AttributeTargets")); + Add("Windows.Foundation.Metadata", new("AttributeUsageAttribute", "System", "AttributeUsageAttribute")); + Add("Windows.Foundation.Metadata", new("ContractVersionAttribute", "Windows.Foundation.Metadata", "ContractVersionAttribute")); + + // Windows.Foundation.Numerics + Add("Windows.Foundation.Numerics", new("Matrix3x2", "System.Numerics", "Matrix3x2")); + Add("Windows.Foundation.Numerics", new("Matrix4x4", "System.Numerics", "Matrix4x4")); + Add("Windows.Foundation.Numerics", new("Plane", "System.Numerics", "Plane")); + Add("Windows.Foundation.Numerics", new("Quaternion", "System.Numerics", "Quaternion")); + Add("Windows.Foundation.Numerics", new("Vector2", "System.Numerics", "Vector2")); + Add("Windows.Foundation.Numerics", new("Vector3", "System.Numerics", "Vector3")); + Add("Windows.Foundation.Numerics", new("Vector4", "System.Numerics", "Vector4")); + + // Windows.Storage.Streams + Add("Windows.Storage.Streams", new("IBuffer", "Windows.Storage.Streams", "IBuffer")); + Add("Windows.Storage.Streams", new("IInputStream", "Windows.Storage.Streams", "IInputStream")); + Add("Windows.Storage.Streams", new("IOutputStream", "Windows.Storage.Streams", "IOutputStream")); + Add("Windows.Storage.Streams", new("IRandomAccessStream", "Windows.Storage.Streams", "IRandomAccessStream")); + Add("Windows.Storage.Streams", new("InputStreamOptions", "Windows.Storage.Streams", "InputStreamOptions")); + + // Windows.UI.Xaml + Add("Windows.UI.Xaml", new("CornerRadius", "Windows.UI.Xaml", "CornerRadius", false, false, true)); + Add("Windows.UI.Xaml", new("CornerRadiusHelper", "", "")); + Add("Windows.UI.Xaml", new("Duration", "Windows.UI.Xaml", "Duration", false, false, true)); + Add("Windows.UI.Xaml", new("DurationHelper", "", "")); + Add("Windows.UI.Xaml", new("GridLength", "Windows.UI.Xaml", "GridLength", false, false, true)); + Add("Windows.UI.Xaml", new("GridLengthHelper", "", "")); + Add("Windows.UI.Xaml", new("ICornerRadiusHelper", "", "")); + Add("Windows.UI.Xaml", new("ICornerRadiusHelperStatics", "", "")); + Add("Windows.UI.Xaml", new("IDurationHelper", "", "")); + Add("Windows.UI.Xaml", new("IDurationHelperStatics", "", "")); + Add("Windows.UI.Xaml", new("IGridLengthHelper", "", "")); + Add("Windows.UI.Xaml", new("IGridLengthHelperStatics", "", "")); + Add("Windows.UI.Xaml", new("IThicknessHelper", "", "")); + Add("Windows.UI.Xaml", new("IThicknessHelperStatics", "", "")); + Add("Windows.UI.Xaml", new("IXamlServiceProvider", "System", "IServiceProvider")); + Add("Windows.UI.Xaml", new("ThicknessHelper", "", "")); + + // Windows.UI.Xaml.Controls.Primitives + Add("Windows.UI.Xaml.Controls.Primitives", new("GeneratorPositionHelper", "", "")); + Add("Windows.UI.Xaml.Controls.Primitives", new("IGeneratorPositionHelper", "", "")); + Add("Windows.UI.Xaml.Controls.Primitives", new("IGeneratorPositionHelperStatics", "", "")); + + // Windows.UI.Xaml.Data + Add("Windows.UI.Xaml.Data", new("DataErrorsChangedEventArgs", "System.ComponentModel", "DataErrorsChangedEventArgs")); + Add("Windows.UI.Xaml.Data", new("INotifyDataErrorInfo", "System.ComponentModel", "INotifyDataErrorInfo", true, true)); + Add("Windows.UI.Xaml.Data", new("INotifyPropertyChanged", "System.ComponentModel", "INotifyPropertyChanged")); + Add("Windows.UI.Xaml.Data", new("PropertyChangedEventArgs", "System.ComponentModel", "PropertyChangedEventArgs")); + Add("Windows.UI.Xaml.Data", new("PropertyChangedEventHandler", "System.ComponentModel", "PropertyChangedEventHandler")); + + // Windows.UI.Xaml.Input + Add("Windows.UI.Xaml.Input", new("ICommand", "System.Windows.Input", "ICommand", true)); + + // Windows.UI.Xaml.Interop + Add("Windows.UI.Xaml.Interop", new("IBindableIterable", "System.Collections", "IEnumerable", true, true)); + Add("Windows.UI.Xaml.Interop", new("IBindableIterator", "System.Collections", "IEnumerator", true, true)); + Add("Windows.UI.Xaml.Interop", new("IBindableVector", "System.Collections", "IList", true, true)); + Add("Windows.UI.Xaml.Interop", new("INotifyCollectionChanged", "System.Collections.Specialized", "INotifyCollectionChanged", true)); + Add("Windows.UI.Xaml.Interop", new("NotifyCollectionChangedAction", "System.Collections.Specialized", "NotifyCollectionChangedAction")); + Add("Windows.UI.Xaml.Interop", new("NotifyCollectionChangedEventArgs", "System.Collections.Specialized", "NotifyCollectionChangedEventArgs", true)); + Add("Windows.UI.Xaml.Interop", new("NotifyCollectionChangedEventHandler", "System.Collections.Specialized", "NotifyCollectionChangedEventHandler", true)); + Add("Windows.UI.Xaml.Interop", new("TypeKind", "Windows.UI.Xaml.Interop", "TypeKind", true)); + Add("Windows.UI.Xaml.Interop", new("TypeName", "System", "Type", true)); + + // Windows.UI.Xaml.Media + Add("Windows.UI.Xaml.Media", new("IMatrixHelper", "", "")); + Add("Windows.UI.Xaml.Media", new("IMatrixHelperStatics", "", "")); + Add("Windows.UI.Xaml.Media", new("MatrixHelper", "", "")); + + // Windows.UI.Xaml.Media.Animation + Add("Windows.UI.Xaml.Media.Animation", new("IKeyTimeHelper", "", "")); + Add("Windows.UI.Xaml.Media.Animation", new("IKeyTimeHelperStatics", "", "")); + Add("Windows.UI.Xaml.Media.Animation", new("IRepeatBehaviorHelper", "", "")); + Add("Windows.UI.Xaml.Media.Animation", new("IRepeatBehaviorHelperStatics", "", "")); + Add("Windows.UI.Xaml.Media.Animation", new("KeyTime", "Windows.UI.Xaml.Media.Animation", "KeyTime", false, false, true)); + Add("Windows.UI.Xaml.Media.Animation", new("KeyTimeHelper", "", "")); + Add("Windows.UI.Xaml.Media.Animation", new("RepeatBehavior", "Windows.UI.Xaml.Media.Animation", "RepeatBehavior", false, false, true)); + Add("Windows.UI.Xaml.Media.Animation", new("RepeatBehaviorHelper", "", "")); + + // Windows.UI.Xaml.Media.Media3D + Add("Windows.UI.Xaml.Media.Media3D", new("IMatrix3DHelper", "", "")); + Add("Windows.UI.Xaml.Media.Media3D", new("IMatrix3DHelperStatics", "", "")); + Add("Windows.UI.Xaml.Media.Media3D", new("Matrix3D", "Windows.UI.Xaml.Media.Media3D", "Matrix3D", false, false, true)); + Add("Windows.UI.Xaml.Media.Media3D", new("Matrix3DHelper", "", "")); + + // WindowsRuntime.Internal + Add("WindowsRuntime.Internal", new("HWND", "System", "IntPtr")); + Add("WindowsRuntime.Internal", new("ProjectionInternalAttribute", "", "")); + + return result; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/Settings.cs b/src/WinRT.Projection.Generator.Writer/Helpers/Settings.cs new file mode 100644 index 0000000000..1246a17de8 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/Settings.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ settings_type from settings.h. +/// +internal sealed class Settings +{ + public HashSet Input { get; } = new(); + public string OutputFolder { get; set; } = string.Empty; + public bool Verbose { get; set; } + public HashSet Include { get; } = new(); + public HashSet Exclude { get; } = new(); + public HashSet AdditionExclude { get; } = new(); + public TypeFilter Filter { get; set; } = TypeFilter.Empty; + public TypeFilter AdditionFilter { get; set; } = TypeFilter.Empty; + public bool NetstandardCompat { get; set; } + public bool Component { get; set; } + public bool Internal { get; set; } + public bool Embedded { get; set; } + public bool PublicEnums { get; set; } + public bool PublicExclusiveTo { get; set; } + public bool IdicExclusiveTo { get; set; } + public bool ReferenceProjection { get; set; } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/TypeFilter.cs b/src/WinRT.Projection.Generator.Writer/Helpers/TypeFilter.cs new file mode 100644 index 0000000000..4a3d7bde12 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/TypeFilter.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AsmResolver; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ winmd::reader::filter include/exclude logic. +/// Filters use longest-prefix-match semantics: type/namespace is checked against +/// each prefix in include/exclude lists, and the longest matching prefix wins. +/// +internal readonly struct TypeFilter +{ + private readonly List _include; + private readonly List _exclude; + + public static TypeFilter Empty { get; } = new(Array.Empty(), Array.Empty()); + + public TypeFilter(IEnumerable include, IEnumerable exclude) + { + _include = include.OrderByDescending(s => s.Length).ToList(); + _exclude = exclude.OrderByDescending(s => s.Length).ToList(); + } + + /// + /// Whether this filter matches everything by default (no include rules). + /// + public bool MatchesAllByDefault => _include == null || _include.Count == 0; + + /// + /// Returns whether the given type name passes the include/exclude filter. + /// Mirrors the C++ winmd::reader::filter algorithm: rules are sorted by descending + /// prefix length (with includes winning ties over excludes); the first matching rule wins. + /// Match semantics split the full type name into namespace.typeName parts and treat + /// the rule prefix as either a namespace-prefix or a namespace + typename-prefix. + /// + public bool Includes(string fullName) + { + if ((_include == null || _include.Count == 0) && (_exclude == null || _exclude.Count == 0)) + { + return true; + } + + // Split into namespace + typename at the LAST '.'. Mirrors C++: + // auto position = type.find_last_of('.'); + // includes(type.substr(0, position), type.substr(position + 1)); + // When there's no '.', position = npos, and both substrings collapse to the full string. + int dot = fullName.LastIndexOf('.'); + string ns; + string name; + if (dot < 0) + { + ns = fullName; + name = fullName; + } + else + { + ns = fullName.Substring(0, dot); + name = fullName.Substring(dot + 1); + } + + // Walk both lists in descending length order; on tie, includes win over excludes. + // (Both _include and _exclude are pre-sorted by descending length in the constructor.) + int incIdx = 0; + int excIdx = 0; + while (true) + { + string? incRule = (_include != null && incIdx < _include.Count) ? _include[incIdx] : null; + string? excRule = (_exclude != null && excIdx < _exclude.Count) ? _exclude[excIdx] : null; + if (incRule == null && excRule == null) { break; } + + bool pickInclude; + if (incRule == null) + { + pickInclude = false; + } + else if (excRule == null) + { + pickInclude = true; + } + else + { + // Equal length: include wins (matches C++ sort key 'pair{size, !isInclude}' descending). + pickInclude = incRule.Length >= excRule.Length; + } + + string rule = pickInclude ? incRule! : excRule!; + if (Match(ns, name, rule)) + { + return pickInclude; + } + if (pickInclude) { incIdx++; } else { excIdx++; } + } + + // No rule matched. If we have any include rules, default-exclude; else default-include. + return _include == null || _include.Count == 0; + } + + /// Mirrors C++ filter::match. + private static bool Match(string typeNamespace, string typeName, string rule) + { + if (rule.Length <= typeNamespace.Length) + { + return typeNamespace.StartsWith(rule, StringComparison.Ordinal); + } + if (!rule.StartsWith(typeNamespace, StringComparison.Ordinal)) + { + return false; + } + if (rule[typeNamespace.Length] != '.') + { + return false; + } + // The rest of the rule (after 'namespace.') is matched as a prefix against typeName. + string rest = rule.Substring(typeNamespace.Length + 1); + return typeName.StartsWith(rest, StringComparison.Ordinal); + } + + public bool Includes(TypeDefinition type) + { + return Includes(GetFullName(type)); + } + + public static string GetFullName(TypeDefinition type) + { + Utf8String? ns = type.Namespace; + Utf8String? name = type.Name; + if (ns is null || ns.Length == 0) + { + return name?.Value ?? string.Empty; + } + return ns + "." + (name?.Value ?? string.Empty); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/WindowsMetadataExpander.cs b/src/WinRT.Projection.Generator.Writer/Helpers/WindowsMetadataExpander.cs new file mode 100644 index 0000000000..7343d8322e --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Helpers/WindowsMetadataExpander.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Xml; +using Microsoft.Win32; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Expands a Windows metadata token (e.g. "sdk", "sdk+", "local", "10.0.26100.0", +/// or a literal path) into the set of .winmd files that the C++ cswinrt.exe tool's +/// cmd_reader.h would have expanded for the same input. Mirrors the logic in +/// src/cswinrt/cmd_reader.h. +/// +public static partial class WindowsMetadataExpander +{ + /// + /// Matches an SDK version string like "10.0.26100.0" or "10.0.26100.0+" + /// (the trailing + indicates that extension SDKs should also be included). + /// + [GeneratedRegex(@"^(\d+\.\d+\.\d+\.\d+)\+?$")] + private static partial Regex SdkVersionRegex { get; } + + /// + /// Expands a single Windows metadata token to the resulting set of .winmd file paths + /// (or directory paths that should be recursively scanned by the writer). + /// + /// The token to expand (path, "local", "sdk", "sdk+", or a version string). + /// A list of paths suitable for . + public static List Expand(string token) + { + List result = []; + + if (string.IsNullOrWhiteSpace(token)) + { + return result; + } + + // Existing file or directory: pass through as-is (the writer handles both). + if (File.Exists(token) || Directory.Exists(token)) + { + result.Add(token); + return result; + } + + // "local" => %WinDir%\System32\WinMetadata (or SysNative on x86 process) + if (string.Equals(token, "local", StringComparison.Ordinal)) + { + string winDir = Environment.GetEnvironmentVariable("windir") ?? @"C:\Windows"; + string subdir = Environment.Is64BitProcess ? "System32" : "SysNative"; + string local = Path.Combine(winDir, subdir, "WinMetadata"); + if (Directory.Exists(local)) + { + result.Add(local); + } + return result; + } + + // "sdk" / "sdk+" => current Windows SDK version + // "10.0.X.Y" / "10.0.X.Y+" => specific version + bool includeExtensions = token.EndsWith('+'); + string sdkVersion = string.Empty; + + if (token is "sdk" or "sdk+") + { + sdkVersion = TryGetCurrentSdkVersion(); + } + else + { + Match m = SdkVersionRegex.Match(token); + if (m.Success) + { + sdkVersion = m.Groups[1].Value; + } + } + + if (!string.IsNullOrEmpty(sdkVersion)) + { + string sdkPath = TryGetSdkPath(); + if (string.IsNullOrEmpty(sdkPath)) + { + throw new InvalidOperationException("Could not find the Windows SDK in the registry."); + } + + string platformXml = Path.Combine(sdkPath, "Platforms", "UAP", sdkVersion, "Platform.xml"); + AddFilesFromPlatformXml(result, sdkVersion, platformXml, sdkPath); + + if (includeExtensions) + { + string extensionSdks = Path.Combine(sdkPath, "Extension SDKs"); + if (Directory.Exists(extensionSdks)) + { + foreach (string item in Directory.EnumerateDirectories(extensionSdks)) + { + string xml = Path.Combine(item, sdkVersion, "SDKManifest.xml"); + if (File.Exists(xml)) + { + AddFilesFromPlatformXml(result, sdkVersion, xml, sdkPath); + } + } + } + } + + return result; + } + + // No expansion matched - return the token as-is so the writer's "file not found" error + // surfaces with the original token in the message. + result.Add(token); + return result; + } + + /// Mirrors C++ get_sdk_path. + private static string TryGetSdkPath() + { + if (!OperatingSystem.IsWindows()) + { + return string.Empty; + } + // HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots\KitsRoot10 + // Try the WOW64 view first (matches C++ KEY_WOW64_32KEY), then default view. + const string subKey = @"SOFTWARE\Microsoft\Windows Kits\Installed Roots"; + try + { + using RegistryKey? wow = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subKey); + if (wow?.GetValue("KitsRoot10") is string p1 && !string.IsNullOrEmpty(p1)) + { + return p1; + } + using RegistryKey? def = Registry.LocalMachine.OpenSubKey(subKey); + if (def?.GetValue("KitsRoot10") is string p2 && !string.IsNullOrEmpty(p2)) + { + return p2; + } + } + catch + { + // Fall through + } + return string.Empty; + } + + /// Mirrors C++ get_sdk_version. + private static string TryGetCurrentSdkVersion() + { + string sdkPath = TryGetSdkPath(); + if (string.IsNullOrEmpty(sdkPath)) + { + return string.Empty; + } + string platforms = Path.Combine(sdkPath, "Platforms", "UAP"); + if (!Directory.Exists(platforms)) + { + return string.Empty; + } + + // Find the highest installed version that has a Platform.xml file. + Version best = new(0, 0, 0, 0); + string bestStr = string.Empty; + foreach (string dir in Directory.EnumerateDirectories(platforms)) + { + string name = Path.GetFileName(dir); + if (!Version.TryParse(name, out Version? v)) + { + continue; + } + string xml = Path.Combine(dir, "Platform.xml"); + if (!File.Exists(xml)) + { + continue; + } + if (v > best) + { + best = v; + bestStr = name; + } + } + return bestStr; + } + + /// Mirrors C++ add_files_from_xml. + private static void AddFilesFromPlatformXml(List result, string sdkVersion, string xmlPath, string sdkPath) + { + if (!File.Exists(xmlPath)) + { + throw new InvalidOperationException($"Could not read the Windows SDK's XML at {xmlPath}."); + } + + XmlReaderSettings settings = new() { DtdProcessing = DtdProcessing.Ignore, IgnoreWhitespace = true }; + using XmlReader reader = XmlReader.Create(xmlPath, settings); + while (reader.Read()) + { + if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "ApiContract") + { + continue; + } + string? name = reader.GetAttribute("name"); + string? version = reader.GetAttribute("version"); + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) + { + continue; + } + // \References\\\\.winmd + string winmd = Path.Combine(sdkPath, "References", sdkVersion, name, version, name + ".winmd"); + result.Add(winmd); + } + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Metadata/MetadataCache.cs b/src/WinRT.Projection.Generator.Writer/Metadata/MetadataCache.cs new file mode 100644 index 0000000000..0a80e3d0f5 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Metadata/MetadataCache.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ winmd::reader::cache from the WinMD library. +/// Loads one or more .winmd files and exposes types organized by namespace. +/// +internal sealed class MetadataCache +{ + private readonly Dictionary _namespaces = new(StringComparer.Ordinal); + private readonly Dictionary _typesByFullName = new(StringComparer.Ordinal); + private readonly Dictionary _typeToModulePath = new(); + private readonly List _modules = new(); + + public IReadOnlyDictionary Namespaces => _namespaces; + + public IReadOnlyList Modules => _modules; + + /// + /// The shared used for resolving TypeRefs in the loaded .winmd files. + /// All .winmd files share an mscorlib reference (v255.255.255.255 with the standard PKT), so we can + /// safely use a single runtime context for all of them. + /// + public RuntimeContext RuntimeContext { get; } + + private MetadataCache(RuntimeContext runtimeContext) + { + RuntimeContext = runtimeContext; + } + + public static MetadataCache Load(IEnumerable inputs) + { + // Collect all .winmd files first so the resolver knows about all of them. Dedupe by canonical + // absolute path so that the same physical file passed via two different but equivalent path + // strings (e.g. one absolute and one with '..' components, or one explicitly listed as a file + // and one picked up by an enclosing directory scan) is only loaded once. Loading the same + // .winmd twice causes duplicate types to be added to NamespaceMembers.Types and ultimately + // emitted twice in the same output file (CS0101). + HashSet seen = new(StringComparer.OrdinalIgnoreCase); + List winmdFiles = new(); + foreach (string input in inputs) + { + if (Directory.Exists(input)) + { + foreach (string path in Directory.EnumerateFiles(input, "*.winmd", SearchOption.AllDirectories)) + { + string canonical = Path.GetFullPath(path); + if (seen.Add(canonical)) + { + winmdFiles.Add(canonical); + } + } + } + else if (File.Exists(input)) + { + string canonical = Path.GetFullPath(input); + if (seen.Add(canonical)) + { + winmdFiles.Add(canonical); + } + } + else + { + throw new FileNotFoundException($"Input metadata file/directory not found: {input}", input); + } + } + + // Set up a PathAssemblyResolver scoped to the input .winmd files + // (and any sibling .winmd files in their directories) so type forwards and cross-references resolve. + string[] searchDirectories = winmdFiles + .Select(Path.GetDirectoryName) + .Where(d => d is not null) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray()!; + + PathAssemblyResolver resolver = PathAssemblyResolver.FromSearchDirectories(searchDirectories); + + // For .winmd files, mscorlib is referenced as v255.255.255.255. We use a synthetic runtime info + // (NetCoreApp 10.0 to match the project's TFM) — the actual version isn't important for resolving + // .winmd-internal TypeRefs since all .winmd cross-references go through PathAssemblyResolver + // by name. The AsmResolver runtime context just needs to be valid to bypass the implicit + // "probe runtime from PE image" path that fails for .winmd files (v255.255). + DotNetRuntimeInfo targetRuntime = DotNetRuntimeInfo.NetCoreApp(10, 0); + RuntimeContext runtimeContext = new(targetRuntime, resolver); + + MetadataCache cache = new(runtimeContext); + foreach (string winmd in winmdFiles) + { + cache.LoadFile(winmd); + } + cache.SortMembersByName(); + return cache; + } + + /// + /// Sorts each namespace's list alphabetically by type name. + /// Mirrors the C++ tool which uses std::map<std::string_view, TypeDef> for the + /// per-namespace types map, which iterates in sorted order. The C# port stores members in + /// insertion order; we explicitly sort here so all downstream iteration produces deterministic + /// output that matches the C++ tool exactly. + /// + private void SortMembersByName() + { + foreach (NamespaceMembers members in _namespaces.Values) + { + static int Compare(TypeDefinition a, TypeDefinition b) => System.StringComparer.Ordinal.Compare(a.Name?.Value ?? string.Empty, b.Name?.Value ?? string.Empty); + members.Types.Sort(Compare); + members.Interfaces.Sort(Compare); + members.Classes.Sort(Compare); + members.Enums.Sort(Compare); + members.Structs.Sort(Compare); + members.Delegates.Sort(Compare); + members.Attributes.Sort(Compare); + members.Contracts.Sort(Compare); + } + } + + private void LoadFile(string path) + { + AssemblyDefinition assemblyDefinition = RuntimeContext.LoadAssembly(path); + if (assemblyDefinition.Modules is not [ModuleDefinition module]) + { + throw new System.BadImageFormatException($"Expected exactly one module in '{path}'."); + } + _modules.Add(module); + string moduleFilePath = path; + + foreach (TypeDefinition type in module.TopLevelTypes) + { + string ns = type.Namespace?.Value ?? string.Empty; + string name = type.Name?.Value ?? string.Empty; + + // Skip the pseudo-type + if (name == "") + { + continue; + } + + // Dedupe by full type name. Multiple input .winmd files can legitimately define types + // with the same full name (e.g. WindowsRuntime.Internal types appearing in both + // WindowsRuntime.Internal.winmd and cswinrt.winmd, or types showing up in both an SDK + // contract winmd and a 3rd-party WinMD that re-exports/forwards them). The C++ cswinrt + // tool silently dedupes via 'std::map' in its cache; the C# port + // mirrors that here so the same input set produces semantically identical output. + // First-load-wins matches the C++ behavior (the map's insert is "no overwrite"). + string fullName = string.IsNullOrEmpty(ns) ? name : ns + "." + name; + if (_typesByFullName.ContainsKey(fullName)) + { + continue; + } + + if (!_namespaces.TryGetValue(ns, out NamespaceMembers? members)) + { + members = new NamespaceMembers(ns); + _namespaces[ns] = members; + } + members.AddType(type); + + _typesByFullName[fullName] = type; + _typeToModulePath[type] = moduleFilePath; + } + } + + /// + /// Gets the file path of the .winmd that contributed the given type. + /// + public string GetSourcePath(TypeDefinition type) + { + return _typeToModulePath.TryGetValue(type, out string? path) ? path : string.Empty; + } + + /// + /// Looks up a type by full name (namespace + "." + name). + /// + public TypeDefinition? Find(string fullName) + { + return _typesByFullName.TryGetValue(fullName, out TypeDefinition? type) ? type : null; + } + + /// + /// Gets a type by full name, throwing if not found. + /// + public TypeDefinition FindRequired(string fullName) + { + return Find(fullName) ?? throw new InvalidOperationException($"Required type '{fullName}' not found in metadata."); + } +} + +/// +/// Mirrors the C++ cache::namespace_members: the types in a particular namespace, +/// organized by category. +/// +internal sealed class NamespaceMembers +{ + public string Name { get; } + + public List Types { get; } = new(); + public List Interfaces { get; } = new(); + public List Classes { get; } = new(); + public List Enums { get; } = new(); + public List Structs { get; } = new(); + public List Delegates { get; } = new(); + public List Attributes { get; } = new(); + public List Contracts { get; } = new(); + + public NamespaceMembers(string name) + { + Name = name; + } + + public void AddType(TypeDefinition type) + { + Types.Add(type); + TypeCategory category = TypeCategorization.GetCategory(type); + switch (category) + { + case TypeCategory.Interface: + Interfaces.Add(type); + break; + case TypeCategory.Class: + if (TypeCategorization.IsAttributeType(type)) + { + Attributes.Add(type); + } + else + { + Classes.Add(type); + } + break; + case TypeCategory.Enum: + Enums.Add(type); + break; + case TypeCategory.Struct: + if (TypeCategorization.IsApiContractType(type)) + { + Contracts.Add(type); + } + else + { + Structs.Add(type); + } + break; + case TypeCategory.Delegate: + Delegates.Add(type); + break; + } + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Metadata/TypeCategorization.cs b/src/WinRT.Projection.Generator.Writer/Metadata/TypeCategorization.cs new file mode 100644 index 0000000000..f49db2924d --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Metadata/TypeCategorization.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ category enum in winmd::reader::category. +/// +internal enum TypeCategory +{ + Interface, + Class, + Enum, + Struct, + Delegate, +} + +/// +/// Static type categorization helpers, mirroring winmd::reader::get_category and various +/// helpers.h functions like is_static, is_attribute_type, etc. +/// +internal static class TypeCategorization +{ + /// Determines a type's category (class/interface/enum/struct/delegate). + public static TypeCategory GetCategory(TypeDefinition type) + { + if (type.IsInterface) + { + return TypeCategory.Interface; + } + ITypeDefOrRef? baseType = type.BaseType; + if (baseType is null) + { + return TypeCategory.Class; + } + Utf8String? baseNs = baseType.Namespace; + Utf8String? baseName = baseType.Name; + if (baseNs == "System" && baseName == "Enum") + { + return TypeCategory.Enum; + } + if (baseNs == "System" && baseName == "ValueType") + { + return TypeCategory.Struct; + } + if (baseNs == "System" && baseName == "MulticastDelegate") + { + return TypeCategory.Delegate; + } + return TypeCategory.Class; + } + + /// True if this is an Attribute-derived class. + public static bool IsAttributeType(TypeDefinition type) + { + if (GetCategory(type) != TypeCategory.Class) + { + return false; + } + // Check immediate base type for System.Attribute (winmd attribute types extend it directly). + ITypeDefOrRef? cur = type.BaseType; + while (cur is not null) + { + if (cur.Namespace == "System" && cur.Name == "Attribute") + { + return true; + } + // For attributes, the base type chain is short and we typically stop at a TypeRef + // pointing to System.Attribute. We don't try to resolve further. + return false; + } + return false; + } + + /// True if this is an API contract struct type. + public static bool IsApiContractType(TypeDefinition type) + { + return GetCategory(type) == TypeCategory.Struct && + HasAttribute(type, "Windows.Foundation.Metadata", "ApiContractAttribute"); + } + + /// True if this type is a static class (abstract+sealed). + public static bool IsStatic(TypeDefinition type) + { + return GetCategory(type) == TypeCategory.Class && type.IsAbstract && type.IsSealed; + } + + /// True if this is an interface marked [ExclusiveTo]. + public static bool IsExclusiveTo(TypeDefinition type) + { + return GetCategory(type) == TypeCategory.Interface && + HasAttribute(type, "Windows.Foundation.Metadata", "ExclusiveToAttribute"); + } + + /// True if this is a [Flags] enum. + public static bool IsFlagsEnum(TypeDefinition type) + { + return GetCategory(type) == TypeCategory.Enum && + HasAttribute(type, "System", "FlagsAttribute"); + } + + /// True if this is a generic type (has type parameters). + public static bool IsGeneric(TypeDefinition type) + { + return type.GenericParameters.Count > 0; + } + + /// True if this type is marked [ProjectionInternal]. + public static bool IsProjectionInternal(TypeDefinition type) + { + return HasAttribute(type, "WindowsRuntime.Internal", "ProjectionInternalAttribute"); + } + + /// True if this type's CustomAttributes contains the given attribute. + public static bool HasAttribute(IHasCustomAttribute member, string ns, string name) + { + for (int i = 0; i < member.CustomAttributes.Count; i++) + { + CustomAttribute attr = member.CustomAttributes[i]; + ITypeDefOrRef? type = attr.Constructor?.DeclaringType; + if (type is not null && type.Namespace == ns && type.Name == name) + { + return true; + } + } + return false; + } + + /// Gets the matching CustomAttribute or null. + public static CustomAttribute? GetAttribute(IHasCustomAttribute member, string ns, string name) + { + for (int i = 0; i < member.CustomAttributes.Count; i++) + { + CustomAttribute attr = member.CustomAttributes[i]; + ITypeDefOrRef? type = attr.Constructor?.DeclaringType; + if (type is not null && type.Namespace == ns && type.Name == name) + { + return attr; + } + } + return null; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Metadata/TypeSemantics.cs b/src/WinRT.Projection.Generator.Writer/Metadata/TypeSemantics.cs new file mode 100644 index 0000000000..8cac4af294 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Metadata/TypeSemantics.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors C++ fundamental_type in helpers.h. +/// +internal enum FundamentalType +{ + Boolean, + Char, + Int8, + UInt8, + Int16, + UInt16, + Int32, + UInt32, + Int64, + UInt64, + Float, + Double, + String, +} + +/// +/// Discriminated union of the type semantics from C++ type_semantics. +/// +internal abstract record TypeSemantics +{ + private TypeSemantics() { } + + public sealed record Fundamental(FundamentalType Type) : TypeSemantics; + public sealed record Object_ : TypeSemantics; + public sealed record Guid_ : TypeSemantics; + public sealed record Type_ : TypeSemantics; + public sealed record Definition(TypeDefinition Type) : TypeSemantics; + public sealed record GenericInstance(TypeDefinition GenericType, List GenericArgs) : TypeSemantics; + public sealed record GenericInstanceRef(ITypeDefOrRef GenericType, List GenericArgs) : TypeSemantics; + public sealed record GenericTypeIndex(int Index) : TypeSemantics; + public sealed record GenericMethodIndex(int Index) : TypeSemantics; + public sealed record GenericParameter_(GenericParameter Parameter) : TypeSemantics; + public sealed record Reference(TypeReference Reference_, bool IsValueType = false) : TypeSemantics; +} + +/// +/// Static helpers for converting AsmResolver type signatures into instances. +/// Mirrors get_type_semantics in helpers.h. +/// +internal static class TypeSemanticsFactory +{ + public static TypeSemantics Get(TypeSignature signature) + { + return signature switch + { + CorLibTypeSignature corlib => GetCorLib(corlib.ElementType), + GenericInstanceTypeSignature gi => GetGenericInstance(gi), + GenericParameterSignature gp => gp.ParameterType == GenericParameterType.Type + ? new TypeSemantics.GenericTypeIndex(gp.Index) + : new TypeSemantics.GenericMethodIndex(gp.Index), + TypeDefOrRefSignature tdorref => GetFromTypeDefOrRef(tdorref.Type, tdorref.IsValueType), + SzArrayTypeSignature sz => Get(sz.BaseType), // SZ arrays handled by callers + ByReferenceTypeSignature br => Get(br.BaseType), + _ => GetFromTypeDefOrRef(signature.GetUnderlyingTypeDefOrRef() ?? throw new System.InvalidOperationException("Unsupported signature: " + signature?.ToString())), + }; + } + + public static TypeSemantics GetFromTypeDefOrRef(ITypeDefOrRef type, bool isValueType = false) + { + if (type is TypeDefinition def) + { + return new TypeSemantics.Definition(def); + } + if (type is TypeReference reference) + { + string ns = reference.Namespace?.Value ?? string.Empty; + string name = reference.Name?.Value ?? string.Empty; + if (ns == "System" && name == "Guid") { return new TypeSemantics.Guid_(); } + if (ns == "System" && name == "Object") { return new TypeSemantics.Object_(); } + if (ns == "System" && name == "Type") { return new TypeSemantics.Type_(); } + return new TypeSemantics.Reference(reference, isValueType); + } + if (type is TypeSpecification spec && spec.Signature is GenericInstanceTypeSignature gi) + { + return GetGenericInstance(gi); + } + return new TypeSemantics.Reference((TypeReference)type, isValueType); + } + + private static TypeSemantics GetCorLib(ElementType elementType) + { + return elementType switch + { + ElementType.Boolean => new TypeSemantics.Fundamental(FundamentalType.Boolean), + ElementType.Char => new TypeSemantics.Fundamental(FundamentalType.Char), + ElementType.I1 => new TypeSemantics.Fundamental(FundamentalType.Int8), + ElementType.U1 => new TypeSemantics.Fundamental(FundamentalType.UInt8), + ElementType.I2 => new TypeSemantics.Fundamental(FundamentalType.Int16), + ElementType.U2 => new TypeSemantics.Fundamental(FundamentalType.UInt16), + ElementType.I4 => new TypeSemantics.Fundamental(FundamentalType.Int32), + ElementType.U4 => new TypeSemantics.Fundamental(FundamentalType.UInt32), + ElementType.I8 => new TypeSemantics.Fundamental(FundamentalType.Int64), + ElementType.U8 => new TypeSemantics.Fundamental(FundamentalType.UInt64), + ElementType.R4 => new TypeSemantics.Fundamental(FundamentalType.Float), + ElementType.R8 => new TypeSemantics.Fundamental(FundamentalType.Double), + ElementType.String => new TypeSemantics.Fundamental(FundamentalType.String), + ElementType.Object => new TypeSemantics.Object_(), + _ => throw new System.InvalidOperationException($"Unsupported corlib element type: {elementType}") + }; + } + + private static TypeSemantics GetGenericInstance(GenericInstanceTypeSignature gi) + { + ITypeDefOrRef genericType = gi.GenericType; + TypeDefinition? def = genericType as TypeDefinition; + // Always preserve the type arguments. + List args = new(gi.TypeArguments.Count); + foreach (TypeSignature arg in gi.TypeArguments) + { + args.Add(Get(arg)); + } + if (def is null) + { + // Wrap the generic-type reference along with the resolved type arguments. + return new TypeSemantics.GenericInstanceRef(genericType, args); + } + return new TypeSemantics.GenericInstance(def, args); + } +} + +/// +/// Type-name kind, mirrors C++ typedef_name_type. +/// +internal enum TypedefNameType +{ + Projected, + CCW, + ABI, + NonProjected, + StaticAbiClass, + EventSource, + Marshaller, + ArrayMarshaller, + InteropIID, +} + +/// +/// Type mapping helpers (e.g., to_csharp_type, to_dotnet_type) from C++ code_writers.h. +/// +internal static class FundamentalTypes +{ + public static string ToCSharpType(FundamentalType t) => t switch + { + FundamentalType.Boolean => "bool", + FundamentalType.Char => "char", + FundamentalType.Int8 => "sbyte", + FundamentalType.UInt8 => "byte", + FundamentalType.Int16 => "short", + FundamentalType.UInt16 => "ushort", + FundamentalType.Int32 => "int", + FundamentalType.UInt32 => "uint", + FundamentalType.Int64 => "long", + FundamentalType.UInt64 => "ulong", + FundamentalType.Float => "float", + FundamentalType.Double => "double", + FundamentalType.String => "string", + _ => "object" + }; + + public static string ToDotNetType(FundamentalType t) => t switch + { + FundamentalType.Boolean => "Boolean", + FundamentalType.Char => "Char", + FundamentalType.Int8 => "SByte", + FundamentalType.UInt8 => "Byte", + FundamentalType.Int16 => "Int16", + FundamentalType.UInt16 => "UInt16", + FundamentalType.Int32 => "Int32", + FundamentalType.UInt32 => "UInt32", + FundamentalType.Int64 => "Int64", + FundamentalType.UInt64 => "UInt64", + FundamentalType.Float => "Single", + FundamentalType.Double => "Double", + FundamentalType.String => "String", + _ => "Object" + }; +} diff --git a/src/WinRT.Projection.Generator.Writer/ProjectionWriter.cs b/src/WinRT.Projection.Generator.Writer/ProjectionWriter.cs new file mode 100644 index 0000000000..f16052a3c4 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/ProjectionWriter.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Public API for generating C# Windows Runtime projections from .winmd metadata. +/// This is the C# port of the C++ cswinrt.exe tool from src/cswinrt/. +/// +/// Usage: call with the desired options. The tool will generate +/// .cs files in the specified output folder, one per Windows Runtime namespace. +/// +/// +public static class ProjectionWriter +{ + /// + /// Runs projection generation. Mirrors the orchestration in the C++ cswinrt::run in main.cpp. + /// + /// The generation options (input metadata, output folder, filters). + public static void Run(ProjectionWriterOptions options) + { + ArgumentNullException.ThrowIfNull(options); + if (options.InputPaths == null || options.InputPaths.Count == 0) + { + throw new ArgumentException("At least one input metadata path must be provided.", nameof(options)); + } + if (string.IsNullOrEmpty(options.OutputFolder)) + { + throw new ArgumentException("Output folder must be provided.", nameof(options)); + } + + // Configure global settings (mirrors C++ settings_type) + Settings settings = new() + { + Verbose = options.Verbose, + Component = options.Component, + Internal = options.Internal, + Embedded = options.Embedded, + PublicEnums = options.PublicEnums, + PublicExclusiveTo = options.PublicExclusiveTo, + IdicExclusiveTo = options.IdicExclusiveTo, + ReferenceProjection = options.ReferenceProjection, + OutputFolder = Path.GetFullPath(options.OutputFolder), + }; + + foreach (string p in options.InputPaths) { _ = settings.Input.Add(p); } + foreach (string p in options.Include) { _ = settings.Include.Add(p); } + foreach (string p in options.Exclude) { _ = settings.Exclude.Add(p); } + foreach (string p in options.AdditionExclude) { _ = settings.AdditionExclude.Add(p); } + + settings.Filter = new TypeFilter(settings.Include, settings.Exclude); + settings.AdditionFilter = new TypeFilter(settings.Include, settings.AdditionExclude); + + _ = Directory.CreateDirectory(settings.OutputFolder); + + // Load metadata + MetadataCache cache = MetadataCache.Load(settings.Input); + + // Run the generator + ProjectionGenerator generator = new(settings, cache, options.CancellationToken); + generator.Run(); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/ProjectionWriterOptions.cs b/src/WinRT.Projection.Generator.Writer/ProjectionWriterOptions.cs new file mode 100644 index 0000000000..b072180615 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/ProjectionWriterOptions.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Input parameters for . Mirrors the C++ cswinrt.exe CLI options +/// from main.cpp's process_args(). +/// +public sealed class ProjectionWriterOptions +{ + /// + /// One or more .winmd files (or directories that will be recursively scanned for .winmd files) + /// providing the Windows Runtime metadata to project. + /// + public required IReadOnlyList InputPaths { get; init; } + + /// + /// The output folder where generated .cs files will be placed. Will be created if it doesn't exist. + /// + public required string OutputFolder { get; init; } + + /// + /// Optional list of namespace prefixes to include in the projection. + /// + public IReadOnlyList Include { get; init; } = Array.Empty(); + + /// + /// Optional list of namespace prefixes to exclude from the projection. + /// + public IReadOnlyList Exclude { get; init; } = Array.Empty(); + + /// + /// Optional list of namespace prefixes to exclude from the projection additions. + /// + public IReadOnlyList AdditionExclude { get; init; } = Array.Empty(); + + /// Generate a Windows Runtime component projection. + public bool Component { get; init; } + + /// Generate an internal (non-public) projection. + public bool Internal { get; init; } + + /// Generate an embedded projection. + public bool Embedded { get; init; } + + /// If true with embedded option, generate enums as public. + public bool PublicEnums { get; init; } + + /// Make exclusive-to interfaces public in the projection (default is internal). + public bool PublicExclusiveTo { get; init; } + + /// Make exclusive-to interfaces support IDynamicInterfaceCastable. + public bool IdicExclusiveTo { get; init; } + + /// Generate a projection to be used as a reference assembly. + public bool ReferenceProjection { get; init; } + + /// Show detailed progress information. + public bool Verbose { get; init; } + + /// Cancellation token for the operation. + public CancellationToken CancellationToken { get; init; } +} diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Dispatching/Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Dispatching/Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Dispatching/Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Dispatching/Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml.Controls.Primitives/Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Controls.Primitives/Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml.Controls.Primitives/Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Controls.Primitives/Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.KeyTime.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.KeyTime.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.KeyTime.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.KeyTime.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml.Media.Media3D/Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Media3D/Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml.Media.Media3D/Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Media3D/Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml.Media/Microsoft.UI.Xaml.Media.Matrix.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media/Microsoft.UI.Xaml.Media.Matrix.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml.Media/Microsoft.UI.Xaml.Media.Matrix.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media/Microsoft.UI.Xaml.Media.Matrix.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.CornerRadius.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.CornerRadius.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.CornerRadius.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.CornerRadius.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Duration.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Duration.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Duration.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Duration.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Thickness.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Thickness.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Thickness.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Thickness.cs diff --git a/src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.SR.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml._SR.cs similarity index 100% rename from src/cswinrt/strings/additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.SR.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml._SR.cs diff --git a/src/cswinrt/strings/additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs similarity index 98% rename from src/cswinrt/strings/additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs index 086210fae1..a48ffa4ec6 100644 --- a/src/cswinrt/strings/additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs +++ b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs @@ -1,293 +1,293 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Windows.Storage -{ - using global::System; - using global::System.Diagnostics; - using global::System.IO; - using global::System.Runtime.Versioning; - using global::System.Threading.Tasks; - using global::Microsoft.Win32.SafeHandles; - using global::Windows.Storage; - using global::Windows.Storage.FileProperties; - using global::Windows.Storage.Streams; - -#nullable enable - /// - /// Provides extension methods for working with Windows Runtime storage files and folders. - /// - public static class WindowsRuntimeStorageExtensions - { - /// - /// Opens a for reading from the specified . - /// - /// The to read from. - /// A that represents the asynchronous operation, with a as the result. - /// Thrown if is . - /// Thrown if the file could not be opened or retrieved as a stream. - [SupportedOSPlatform("windows10.0.10240.0")] - public static Task OpenStreamForReadAsync(this IStorageFile windowsRuntimeFile) - { - ArgumentNullException.ThrowIfNull(windowsRuntimeFile); - - // Helper with the actual read logic - [SupportedOSPlatform("windows10.0.10240.0")] - static async Task OpenStreamForReadCoreAsync(IStorageFile windowsRuntimeFile) - { - try - { - IRandomAccessStream windowsRuntimeStream = await windowsRuntimeFile - .OpenAsync(FileAccessMode.Read) - .AsTask().ConfigureAwait(false); - - return windowsRuntimeStream.AsStreamForRead(); - } - catch (Exception exception) - { - // From this API, callers expect an 'IOException' if something went wrong - WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); - - return null; - } - } - - return OpenStreamForReadCoreAsync(windowsRuntimeFile); - } - - /// - /// Opens a for writing to the specified . - /// - /// The to write to. - /// A that represents the asynchronous operation, with a as the result. - /// Thrown if is . - /// Thrown if the file could not be opened or retrieved as a stream. - [SupportedOSPlatform("windows10.0.10240.0")] - public static Task OpenStreamForWriteAsync(this IStorageFile windowsRuntimeFile) - { - ArgumentNullException.ThrowIfNull(windowsRuntimeFile); - - return OpenStreamForWriteWithOffsetAsync(windowsRuntimeFile, offset: 0); - } - - /// - /// Opens a for reading from a file in the specified . - /// - /// The that contains the file to read from. - /// The path, relative to , of the file to read from. - /// A that represents the asynchronous operation, with a as the result. - /// Thrown if or is . - /// Thrown if is empty or contains only whitespace. - /// Thrown if the file could not be opened or retrieved as a stream. - [SupportedOSPlatform("windows10.0.10240.0")] - public static Task OpenStreamForReadAsync(this IStorageFolder rootDirectory, string relativePath) - { - ArgumentNullException.ThrowIfNull(rootDirectory); - ArgumentException.ThrowIfNullOrWhiteSpace(relativePath); - - // Helper with the actual read logic - [SupportedOSPlatform("windows10.0.10240.0")] - static async Task OpenStreamForReadCoreAsync(IStorageFolder rootDirectory, string relativePath) - { - try - { - IStorageFile windowsRuntimeFile = await rootDirectory - .GetFileAsync(relativePath) - .AsTask() - .ConfigureAwait(false); - - return await windowsRuntimeFile - .OpenStreamForReadAsync() - .ConfigureAwait(false); - } - catch (Exception exception) - { - // Throw an 'IOException' (see notes above) - WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); - - return null; - } - } - - return OpenStreamForReadCoreAsync(rootDirectory, relativePath); - } - - /// - /// Opens a for writing to a file in the specified . - /// - /// The that contains or will contain the file to write to. - /// The path, relative to , of the file to write to. - /// The value that specifies how to handle the situation when the file already exists. - /// A that represents the asynchronous operation, with a as the result. - /// Thrown if or is . - /// Thrown if is empty or contains only whitespace. - /// Thrown if the file could not be opened or retrieved as a stream. - [SupportedOSPlatform("windows10.0.10240.0")] - public static Task OpenStreamForWriteAsync( - this IStorageFolder rootDirectory, - string relativePath, - CreationCollisionOption creationCollisionOption) - { - ArgumentNullException.ThrowIfNull(rootDirectory); - ArgumentException.ThrowIfNullOrWhiteSpace(relativePath); - - // Helper with the actual write logic - [SupportedOSPlatform("windows10.0.10240.0")] - static async Task OpenStreamForWriteCoreAsync( - IStorageFolder rootDirectory, - string relativePath, - CreationCollisionOption creationCollisionOption) - { - Debug.Assert(creationCollisionOption is - CreationCollisionOption.FailIfExists or - CreationCollisionOption.GenerateUniqueName or - CreationCollisionOption.OpenIfExists or - CreationCollisionOption.ReplaceExisting, - "The specified 'creationCollisionOption' argument has a value that is not a value we considered when devising the " + - "policy about 'Append-On-OpenIfExists' used in this method. Apparently a new enum value was added to the " + - "'CreationCollisionOption' type and we need to make sure that the policy still makes sense."); - - try - { - // Open file and set up default options for opening it - IStorageFile windowsRuntimeFile = await rootDirectory - .CreateFileAsync(relativePath, creationCollisionOption) - .AsTask() - .ConfigureAwait(false); - - long offset = 0; - - // If the specified option was 'OpenIfExists', then we will try to append, otherwise we will overwrite - if (creationCollisionOption is CreationCollisionOption.OpenIfExists) - { - BasicProperties fileProperties = await windowsRuntimeFile - .GetBasicPropertiesAsync() - .AsTask() - .ConfigureAwait(false); - - ulong fileSize = fileProperties.Size; - - Debug.Assert(fileSize <= long.MaxValue, ".NET streams assume that file sizes are not larger than 'long.MaxValue'."); - - offset = checked((long)fileSize); - } - - // Now open a file with the correct options - return await OpenStreamForWriteWithOffsetAsync(windowsRuntimeFile, offset).ConfigureAwait(false); - } - catch (Exception exception) when (exception is not IOException) - { - // Throw an 'IOException' (see notes above). We use a filter here to avoid unnecessarily - // re-throwing if we already have an 'IOException', since here we're also calling the - // 'OpenStreamForWriteWithOffsetAsync' helper, which will always throw that exception already. - WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); - - return null; - } - } - - return OpenStreamForWriteCoreAsync(rootDirectory, relativePath, creationCollisionOption); - } - - /// - /// Creates a for the specified . - /// - /// The to create a file handle for. - /// The mode to open the file with. - /// The mode to open the file with. - /// The to open the file with. - /// A for the specified storage file, or if the operation failed. - /// Thrown if is . - public static SafeFileHandle? CreateSafeFileHandle( - this IStorageFile windowsRuntimeFile, - FileAccess access = FileAccess.ReadWrite, - FileShare share = FileShare.Read, - FileOptions options = FileOptions.None) - { - ArgumentNullException.ThrowIfNull(windowsRuntimeFile); - - return global::WindowsRuntime.InteropServices.IStorageItemHandleAccessMethods.Create( - windowsRuntimeFile, - access, - share, - options); - } - - /// - /// Creates a for a file in the specified . - /// - /// The that contains the file. - /// The path, relative to , of the file to create a handle for. - /// The to use when opening the file. - /// A for the specified file, or if the operation failed. - public static SafeFileHandle? CreateSafeFileHandle( - this IStorageFolder rootDirectory, - string relativePath, - FileMode mode) - { - return rootDirectory.CreateSafeFileHandle(relativePath, mode, (mode is FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite)); - } - - /// - /// Creates a for a file in the specified . - /// - /// The that contains the file. - /// The path, relative to , of the file to create a handle for. - /// The to use when opening the file. - /// The mode to open the file with. - /// The mode to open the file with. - /// The to open the file with. - /// A for the specified file, or if the operation failed. - /// Thrown if or is . - public static SafeFileHandle? CreateSafeFileHandle( - this IStorageFolder rootDirectory, - string relativePath, - FileMode mode, - FileAccess access, - FileShare share = FileShare.Read, - FileOptions options = FileOptions.None) - { - ArgumentNullException.ThrowIfNull(rootDirectory); - ArgumentNullException.ThrowIfNull(relativePath); - - return global::WindowsRuntime.InteropServices.IStorageFolderHandleAccessMethods.Create( - rootDirectory, - relativePath, - mode, - access, - share, - options); - } - - /// - /// The offset to set in the returned stream. - [SupportedOSPlatform("windows10.0.10240.0")] - private static async Task OpenStreamForWriteWithOffsetAsync(IStorageFile windowsRuntimeFile, long offset) - { - Debug.Assert(windowsRuntimeFile is not null); - Debug.Assert(offset >= 0); - - try - { - IRandomAccessStream windowsRuntimeStream = await windowsRuntimeFile - .OpenAsync(FileAccessMode.ReadWrite) - .AsTask() - .ConfigureAwait(false); - - Stream managedStream = windowsRuntimeStream.AsStreamForWrite(); - - managedStream.Position = offset; - - return managedStream; - } - catch (Exception exception) - { - // Throw an 'IOException' (see notes above) - WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); - - return null; - } - } - } -#nullable restore -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Windows.Storage +{ + using global::System; + using global::System.Diagnostics; + using global::System.IO; + using global::System.Runtime.Versioning; + using global::System.Threading.Tasks; + using global::Microsoft.Win32.SafeHandles; + using global::Windows.Storage; + using global::Windows.Storage.FileProperties; + using global::Windows.Storage.Streams; + +#nullable enable + /// + /// Provides extension methods for working with Windows Runtime storage files and folders. + /// + public static class WindowsRuntimeStorageExtensions + { + /// + /// Opens a for reading from the specified . + /// + /// The to read from. + /// A that represents the asynchronous operation, with a as the result. + /// Thrown if is . + /// Thrown if the file could not be opened or retrieved as a stream. + [SupportedOSPlatform("windows10.0.10240.0")] + public static Task OpenStreamForReadAsync(this IStorageFile windowsRuntimeFile) + { + ArgumentNullException.ThrowIfNull(windowsRuntimeFile); + + // Helper with the actual read logic + [SupportedOSPlatform("windows10.0.10240.0")] + static async Task OpenStreamForReadCoreAsync(IStorageFile windowsRuntimeFile) + { + try + { + IRandomAccessStream windowsRuntimeStream = await windowsRuntimeFile + .OpenAsync(FileAccessMode.Read) + .AsTask().ConfigureAwait(false); + + return windowsRuntimeStream.AsStreamForRead(); + } + catch (Exception exception) + { + // From this API, callers expect an 'IOException' if something went wrong + WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); + + return null; + } + } + + return OpenStreamForReadCoreAsync(windowsRuntimeFile); + } + + /// + /// Opens a for writing to the specified . + /// + /// The to write to. + /// A that represents the asynchronous operation, with a as the result. + /// Thrown if is . + /// Thrown if the file could not be opened or retrieved as a stream. + [SupportedOSPlatform("windows10.0.10240.0")] + public static Task OpenStreamForWriteAsync(this IStorageFile windowsRuntimeFile) + { + ArgumentNullException.ThrowIfNull(windowsRuntimeFile); + + return OpenStreamForWriteWithOffsetAsync(windowsRuntimeFile, offset: 0); + } + + /// + /// Opens a for reading from a file in the specified . + /// + /// The that contains the file to read from. + /// The path, relative to , of the file to read from. + /// A that represents the asynchronous operation, with a as the result. + /// Thrown if or is . + /// Thrown if is empty or contains only whitespace. + /// Thrown if the file could not be opened or retrieved as a stream. + [SupportedOSPlatform("windows10.0.10240.0")] + public static Task OpenStreamForReadAsync(this IStorageFolder rootDirectory, string relativePath) + { + ArgumentNullException.ThrowIfNull(rootDirectory); + ArgumentException.ThrowIfNullOrWhiteSpace(relativePath); + + // Helper with the actual read logic + [SupportedOSPlatform("windows10.0.10240.0")] + static async Task OpenStreamForReadCoreAsync(IStorageFolder rootDirectory, string relativePath) + { + try + { + IStorageFile windowsRuntimeFile = await rootDirectory + .GetFileAsync(relativePath) + .AsTask() + .ConfigureAwait(false); + + return await windowsRuntimeFile + .OpenStreamForReadAsync() + .ConfigureAwait(false); + } + catch (Exception exception) + { + // Throw an 'IOException' (see notes above) + WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); + + return null; + } + } + + return OpenStreamForReadCoreAsync(rootDirectory, relativePath); + } + + /// + /// Opens a for writing to a file in the specified . + /// + /// The that contains or will contain the file to write to. + /// The path, relative to , of the file to write to. + /// The value that specifies how to handle the situation when the file already exists. + /// A that represents the asynchronous operation, with a as the result. + /// Thrown if or is . + /// Thrown if is empty or contains only whitespace. + /// Thrown if the file could not be opened or retrieved as a stream. + [SupportedOSPlatform("windows10.0.10240.0")] + public static Task OpenStreamForWriteAsync( + this IStorageFolder rootDirectory, + string relativePath, + CreationCollisionOption creationCollisionOption) + { + ArgumentNullException.ThrowIfNull(rootDirectory); + ArgumentException.ThrowIfNullOrWhiteSpace(relativePath); + + // Helper with the actual write logic + [SupportedOSPlatform("windows10.0.10240.0")] + static async Task OpenStreamForWriteCoreAsync( + IStorageFolder rootDirectory, + string relativePath, + CreationCollisionOption creationCollisionOption) + { + Debug.Assert(creationCollisionOption is + CreationCollisionOption.FailIfExists or + CreationCollisionOption.GenerateUniqueName or + CreationCollisionOption.OpenIfExists or + CreationCollisionOption.ReplaceExisting, + "The specified 'creationCollisionOption' argument has a value that is not a value we considered when devising the " + + "policy about 'Append-On-OpenIfExists' used in this method. Apparently a new enum value was added to the " + + "'CreationCollisionOption' type and we need to make sure that the policy still makes sense."); + + try + { + // Open file and set up default options for opening it + IStorageFile windowsRuntimeFile = await rootDirectory + .CreateFileAsync(relativePath, creationCollisionOption) + .AsTask() + .ConfigureAwait(false); + + long offset = 0; + + // If the specified option was 'OpenIfExists', then we will try to append, otherwise we will overwrite + if (creationCollisionOption is CreationCollisionOption.OpenIfExists) + { + BasicProperties fileProperties = await windowsRuntimeFile + .GetBasicPropertiesAsync() + .AsTask() + .ConfigureAwait(false); + + ulong fileSize = fileProperties.Size; + + Debug.Assert(fileSize <= long.MaxValue, ".NET streams assume that file sizes are not larger than 'long.MaxValue'."); + + offset = checked((long)fileSize); + } + + // Now open a file with the correct options + return await OpenStreamForWriteWithOffsetAsync(windowsRuntimeFile, offset).ConfigureAwait(false); + } + catch (Exception exception) when (exception is not IOException) + { + // Throw an 'IOException' (see notes above). We use a filter here to avoid unnecessarily + // re-throwing if we already have an 'IOException', since here we're also calling the + // 'OpenStreamForWriteWithOffsetAsync' helper, which will always throw that exception already. + WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); + + return null; + } + } + + return OpenStreamForWriteCoreAsync(rootDirectory, relativePath, creationCollisionOption); + } + + /// + /// Creates a for the specified . + /// + /// The to create a file handle for. + /// The mode to open the file with. + /// The mode to open the file with. + /// The to open the file with. + /// A for the specified storage file, or if the operation failed. + /// Thrown if is . + public static SafeFileHandle? CreateSafeFileHandle( + this IStorageFile windowsRuntimeFile, + FileAccess access = FileAccess.ReadWrite, + FileShare share = FileShare.Read, + FileOptions options = FileOptions.None) + { + ArgumentNullException.ThrowIfNull(windowsRuntimeFile); + + return global::WindowsRuntime.InteropServices.IStorageItemHandleAccessMethods.Create( + windowsRuntimeFile, + access, + share, + options); + } + + /// + /// Creates a for a file in the specified . + /// + /// The that contains the file. + /// The path, relative to , of the file to create a handle for. + /// The to use when opening the file. + /// A for the specified file, or if the operation failed. + public static SafeFileHandle? CreateSafeFileHandle( + this IStorageFolder rootDirectory, + string relativePath, + FileMode mode) + { + return rootDirectory.CreateSafeFileHandle(relativePath, mode, (mode is FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite)); + } + + /// + /// Creates a for a file in the specified . + /// + /// The that contains the file. + /// The path, relative to , of the file to create a handle for. + /// The to use when opening the file. + /// The mode to open the file with. + /// The mode to open the file with. + /// The to open the file with. + /// A for the specified file, or if the operation failed. + /// Thrown if or is . + public static SafeFileHandle? CreateSafeFileHandle( + this IStorageFolder rootDirectory, + string relativePath, + FileMode mode, + FileAccess access, + FileShare share = FileShare.Read, + FileOptions options = FileOptions.None) + { + ArgumentNullException.ThrowIfNull(rootDirectory); + ArgumentNullException.ThrowIfNull(relativePath); + + return global::WindowsRuntime.InteropServices.IStorageFolderHandleAccessMethods.Create( + rootDirectory, + relativePath, + mode, + access, + share, + options); + } + + /// + /// The offset to set in the returned stream. + [SupportedOSPlatform("windows10.0.10240.0")] + private static async Task OpenStreamForWriteWithOffsetAsync(IStorageFile windowsRuntimeFile, long offset) + { + Debug.Assert(windowsRuntimeFile is not null); + Debug.Assert(offset >= 0); + + try + { + IRandomAccessStream windowsRuntimeStream = await windowsRuntimeFile + .OpenAsync(FileAccessMode.ReadWrite) + .AsTask() + .ConfigureAwait(false); + + Stream managedStream = windowsRuntimeStream.AsStreamForWrite(); + + managedStream.Position = offset; + + return managedStream; + } + catch (Exception exception) + { + // Throw an 'IOException' (see notes above) + WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); + + return null; + } + } + } +#nullable restore +} diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml.Controls.Primitives/Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Controls.Primitives/Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml.Controls.Primitives/Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Controls.Primitives/Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.KeyTime.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.KeyTime.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.KeyTime.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.KeyTime.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml.Media.Media3D/Windows.UI.Xaml.Media.Media3D.Matrix3D.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Media3D/Windows.UI.Xaml.Media.Media3D.Matrix3D.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml.Media.Media3D/Windows.UI.Xaml.Media.Media3D.Matrix3D.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Media3D/Windows.UI.Xaml.Media.Media3D.Matrix3D.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml.Media/Windows.UI.Xaml.Media.Matrix.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media/Windows.UI.Xaml.Media.Matrix.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml.Media/Windows.UI.Xaml.Media.Matrix.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media/Windows.UI.Xaml.Media.Matrix.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.System.DispatcherQueueSynchronizationContext.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.System.DispatcherQueueSynchronizationContext.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.System.DispatcherQueueSynchronizationContext.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.System.DispatcherQueueSynchronizationContext.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.CornerRadius.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.CornerRadius.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.CornerRadius.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.CornerRadius.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.Duration.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Duration.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.Duration.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Duration.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.Thickness.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Thickness.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.Thickness.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Thickness.cs diff --git a/src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.SR.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml._SR.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI.Xaml/Windows.UI.Xaml.SR.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml._SR.cs diff --git a/src/cswinrt/strings/additions/Windows.UI/Windows.UI.Color.cs b/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI/Windows.UI.Color.cs similarity index 100% rename from src/cswinrt/strings/additions/Windows.UI/Windows.UI.Color.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI/Windows.UI.Color.cs diff --git a/src/cswinrt/strings/ComInteropExtensions.cs b/src/WinRT.Projection.Generator.Writer/Resources/Base/ComInteropExtensions.cs similarity index 100% rename from src/cswinrt/strings/ComInteropExtensions.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Base/ComInteropExtensions.cs diff --git a/src/cswinrt/strings/InspectableVftbl.cs b/src/WinRT.Projection.Generator.Writer/Resources/Base/InspectableVftbl.cs similarity index 100% rename from src/cswinrt/strings/InspectableVftbl.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Base/InspectableVftbl.cs diff --git a/src/cswinrt/strings/ReferenceInterfaceEntries.cs b/src/WinRT.Projection.Generator.Writer/Resources/Base/ReferenceInterfaceEntries.cs similarity index 100% rename from src/cswinrt/strings/ReferenceInterfaceEntries.cs rename to src/WinRT.Projection.Generator.Writer/Resources/Base/ReferenceInterfaceEntries.cs diff --git a/src/WinRT.Projection.Generator.Writer/WinRT.Projection.Generator.Writer.csproj b/src/WinRT.Projection.Generator.Writer/WinRT.Projection.Generator.Writer.csproj new file mode 100644 index 0000000000..7d9ab03a6c --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/WinRT.Projection.Generator.Writer.csproj @@ -0,0 +1,59 @@ + + + net10.0 + 14.0 + enable + true + true + true + + + true + + + WindowsRuntime.ProjectionGenerator.Writer + + + $(VersionString) + + + true + true + latest + latest-all + true + strict + true + + + $(NoWarn);CS1591;CS1573;CS1574;CS1712;CS1734;IDE0004;IDE0005;IDE0010;IDE0011;IDE0022;IDE0028;IDE0046;IDE0057;IDE0072;IDE0078;IDE0090;IDE0270;IDE0290;IDE0300;IDE0301;IDE0305;IDE0306;IDE0058;IDE0008;IDE0007;IDE0150;IDE0042;IDE0017;IDE0019;IDE0021;IDE0040;IDE0044;IDE0050;IDE0052;IDE0055;IDE0059;IDE0060;IDE0063;IDE0066;IDE0083;IDE0130;IDE0180 + + + + + + + + + + + + + + + + + + + diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Abi.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Abi.cs new file mode 100644 index 0000000000..28c420d5b2 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Abi.cs @@ -0,0 +1,6346 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// ABI emission helpers (structs, enums, delegates, interfaces, classes). +/// Mirrors the C++ write_abi_* family. Initial port: emits the foundational +/// ABI scaffolding only; full marshaller/vtable emission to be filled in later. +/// +internal static partial class CodeWriters +{ + /// Mirrors C++ is_type_blittable partially. + public static bool IsTypeBlittable(TypeDefinition type) + { + TypeCategory cat = TypeCategorization.GetCategory(type); + if (cat == TypeCategory.Enum) { return true; } + if (cat != TypeCategory.Struct) { return false; } + // Mirrors C++ is_type_blittable (code_writers.h:81-124, struct_type branch): if the + // struct itself has a mapped-type entry, return based on its RequiresMarshaling flag + // BEFORE walking fields. This is critical for XAML structs like Duration / KeyTime / + // RepeatBehavior which are self-mapped with RequiresMarshaling=false but have a + // TimeSpan field (Windows.Foundation.TimeSpan -> System.TimeSpan with RequiresMarshaling=true). + // Without this check, the field walk would incorrectly classify them as non-blittable. + string ns = type.Namespace?.Value ?? string.Empty; + string name = type.Name?.Value ?? string.Empty; + if (MappedTypes.Get(ns, name) is { } mapping) + { + return !mapping.RequiresMarshaling; + } + // Walk fields - all must be blittable + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + if (!IsFieldTypeBlittable(field.Signature.FieldType)) { return false; } + } + return true; + } + + private static bool IsFieldTypeBlittable(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) + { + // Mirror C++ is_type_blittable for fundamental_type: + // return (type != fundamental_type::String); + // i.e. ALL fundamentals (including Boolean, Char) are considered blittable here; + // only String is non-blittable. Object isn't a fundamental in C++; handled below. + return corlib.ElementType switch + { + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String => false, + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object => false, + _ => true + }; + } + // For TypeRef/TypeDef, resolve and check blittability. + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature todr) + { + string fNs = todr.Type?.Namespace?.Value ?? string.Empty; + string fName = todr.Type?.Name?.Value ?? string.Empty; + // System.Guid is a fundamental blittable type (mirrors C++ guid_type which falls + // through to the [&](auto&&) catch-all returning true in is_type_blittable). + // Same applies to System.IntPtr / UIntPtr (used in some struct layouts). + if (fNs == "System" && (fName == "Guid" || fName == "IntPtr" || fName == "UIntPtr")) + { + return true; + } + // Mapped struct types: blittable iff the mapping does NOT require marshalling + // (mirrors C++ is_type_blittable for mapped struct_type case). + MappedType? mapped = MappedTypes.Get(fNs, fName); + if (mapped is not null && mapped.RequiresMarshaling) { return false; } + if (todr.Type is TypeDefinition td) + { + return IsTypeBlittable(td); + } + // Cross-module: try metadata cache. + if (todr.Type is TypeReference tr && _cacheRef is not null) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + TypeDefinition? resolved = _cacheRef.Find(ns + "." + name); + if (resolved is not null) { return IsTypeBlittable(resolved); } + } + return false; + } + return false; + } + + /// + /// Resolves a to its + /// , handling both in-assembly (already a TypeDefinition) and + /// cross-assembly/TypeRef-row references via the metadata cache. Returns null when + /// the reference cannot be resolved. + /// + private static TypeDefinition? TryResolveStructTypeDef(AsmResolver.DotNet.Signatures.TypeDefOrRefSignature tdr) + { + if (tdr.Type is TypeDefinition td) { return td; } + if (tdr.Type is TypeReference tr && _cacheRef is not null) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + return _cacheRef.Find(ns + "." + name); + } + return null; + } + + /// Mirrors C++ write_abi_enum. + public static void WriteAbiEnum(TypeWriter w, TypeDefinition type) + { + // The C++ version emits: write_struct_and_enum_marshaller_class, write_interface_entries_impl, + // write_struct_and_enum_com_wrappers_marshaller_attribute_impl, write_reference_impl. + // For now, emit a minimal marshaller class so the ComWrappersMarshaller attribute reference resolves. + string name = type.Name?.Value ?? string.Empty; + WriteStructEnumMarshallerClass(w, type); + WriteReferenceImpl(w, type); + + // In component mode, the C++ tool also emits the authoring metadata wrapper for enums. + if (w.Settings.Component) + { + WriteAuthoringMetadataType(w, type); + } + } + + /// Mirrors C++ write_abi_struct. + public static void WriteAbiStruct(TypeWriter w, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + + // Emit the underlying ABI struct only when not blittable AND not a mapped struct + // (mapped structs like Duration/KeyTime/RepeatBehavior have addition files that + // replace the public struct's field layout, so a per-field ABI struct can't be + // built directly from the projected type). + bool blittable = IsTypeBlittable(type); + string typeNs = type.Namespace?.Value ?? string.Empty; + string typeNm = type.Name?.Value ?? string.Empty; + bool isMappedStruct = MappedTypes.Get(typeNs, typeNm) is not null; + if (!blittable && !isMappedStruct) + { + // Mirror C++ write_abi_struct: in component mode emit metadata typename + mapped + // type attribute; otherwise emit the ComWrappers attribute. Both branches then + // emit [WindowsRuntimeClassName] + the struct definition with public ABI fields. + if (w.Settings.Component) + { + WriteWinRTMetadataTypeNameAttribute(w, type); + WriteWinRTMappedTypeAttribute(w, type); + } + else + { + WriteComWrapperMarshallerAttribute(w, type); + } + WriteValueTypeWinRTClassNameAttribute(w, type); + w.Write(Helpers.InternalAccessibility(w.Settings)); + w.Write(" unsafe struct "); + WriteTypedefName(w, type, TypedefNameType.ABI, false); + w.Write("\n{\n"); + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; + w.Write("public "); + // Truth uses void* for string and Nullable fields, the ABI struct for + // mapped value types (DateTime/TimeSpan), and the projected type for everything + // else (including enums and bool — their C# layout matches the WinRT ABI directly). + if (IsString(ft) || TryGetNullablePrimitiveMarshallerName(ft, out _)) + { + w.Write("void*"); + } + else if (IsMappedAbiValueType(ft)) + { + w.Write(GetMappedAbiTypeName(ft)); + } + else if (ft is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature tdr + && TryResolveStructTypeDef(tdr) is TypeDefinition fieldTd + && TypeCategorization.GetCategory(fieldTd) == TypeCategory.Struct + && !IsTypeBlittable(fieldTd)) + { + // Mirror C++ write_abi_type: non-blittable struct field uses ABI typedef name. + WriteTypedefName(w, fieldTd, TypedefNameType.ABI, false); + } + else + { + WriteProjectedSignature(w, ft, false); + } + w.Write(" "); + w.Write(field.Name?.Value ?? string.Empty); + w.Write(";\n"); + } + w.Write("}\n\n"); + } + else if (blittable && w.Settings.Component) + { + // For blittable component structs, the C++ tool emits the authoring metadata wrapper + // (a 'file static class T {}' with [WindowsRuntimeMetadataTypeName]/[WindowsRuntimeMappedType]/ + // [WindowsRuntimeReferenceType]/[ComWrappersMarshaller]/[WindowsRuntimeClassName]). + WriteAuthoringMetadataType(w, type); + } + + WriteStructEnumMarshallerClass(w, type); + WriteReferenceImpl(w, type); + } + + /// Mirrors C++ write_abi_delegate. + public static void WriteAbiDelegate(TypeWriter w, TypeDefinition type) + { + // Mirror the C++ tool's ordering exactly: + // write_delegate_marshaller + // write_delegate_vtbl + // write_native_delegate + // write_delegate_comwrappers_callback + // write_delegates_interface_entries_impl + // write_delegate_com_wrappers_marshaller_attribute_impl + // write_delegate_impl + // write_reference_impl + // (component) write_authoring_metadata_type + WriteDelegateMarshallerOnly(w, type); + WriteDelegateVftbl(w, type); + WriteNativeDelegate(w, type); + WriteDelegateComWrappersCallback(w, type); + WriteDelegateInterfaceEntriesImpl(w, type); + WriteDelegateComWrappersMarshallerAttribute(w, type); + WriteDelegateImpl(w, type); + WriteReferenceImpl(w, type); + + // In component mode, the C++ tool also emits the authoring metadata wrapper for delegates. + if (w.Settings.Component) + { + WriteAuthoringMetadataType(w, type); + } + } + + /// Emits the <DelegateName>Impl static class providing the CCW vtable for a delegate. + private static void WriteDelegateImpl(TypeWriter w, TypeDefinition type) + { + if (type.GenericParameters.Count > 0) { return; } + MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); + if (invoke is null) { return; } + MethodSig sig = new(invoke); + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + string iidExpr = w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, type))); + + w.Write("\ninternal static unsafe class "); + w.Write(nameStripped); + w.Write("Impl\n{\n"); + w.Write(" [FixedAddressValueType]\n"); + w.Write(" private static readonly "); + w.Write(nameStripped); + w.Write("Vftbl Vftbl;\n\n"); + w.Write(" static "); + w.Write(nameStripped); + w.Write("Impl()\n {\n"); + w.Write(" *(IUnknownVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IUnknownVftbl*)IUnknownImpl.Vtable;\n"); + w.Write(" Vftbl.Invoke = &Invoke;\n"); + w.Write(" }\n\n"); + w.Write(" public static nint Vtable\n {\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => (nint)Unsafe.AsPointer(in Vftbl);\n }\n\n"); + + w.Write("[UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]\n"); + w.Write("private static int Invoke("); + WriteAbiParameterTypesPointer(w, sig, includeParamNames: true); + w.Write(")"); + + // Reuse the interface Do_Abi body emitter: delegates dispatch via __target.Invoke(...), + // which is exactly the same shape as interface CCW dispatch. Pass the delegate's + // projected name as 'ifaceFullName' and "Invoke" as 'methodName'. + string projectedDelegateForBody = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); + if (!projectedDelegateForBody.StartsWith("global::", System.StringComparison.Ordinal)) { projectedDelegateForBody = "global::" + projectedDelegateForBody; } + EmitDoAbiBodyIfSimple(w, sig, projectedDelegateForBody, "Invoke"); + w.Write("\n"); + + w.Write(" public static ref readonly Guid IID\n {\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => ref "); + w.Write(iidExpr); + w.Write(";\n }\n}\n"); + } + + + /// + /// Returns the interop assembly path for an array marshaller of a given element type. + /// The interop generator names array marshallers ABI.<typeNamespace>.<<assembly>ElementName>ArrayMarshaller + /// (typeNamespace prefix outside the brackets, and the element inside the brackets uses just the + /// type name without its namespace because depth=0 in the interop generator's AppendRawTypeName). + /// + private static string GetArrayMarshallerInteropPath(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature elementType, string encodedElement) + { + // The 'encodedElement' passed in uses the depth>0 form (assembly + hyphenated namespace + name), + // but inside the array brackets the interop generator uses the depth=0 form (assembly + just name). + // Re-encode the element with the top-level form for accurate matching. + string topLevelElement = EncodeArrayElementName(elementType); + // Resolve the element's namespace to determine the path prefix. + string ns = GetMappedNamespace(elementType); + if (string.IsNullOrEmpty(ns)) + { + return "ABI.<" + topLevelElement + ">ArrayMarshaller, WinRT.Interop"; + } + return "ABI." + ns + ".<" + topLevelElement + ">ArrayMarshaller, WinRT.Interop"; + } + + /// Returns the (possibly mapped) namespace of a type signature, or 'System' for fundamentals. + private static string GetMappedNamespace(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + // Fundamentals (string, bool, int, etc.) live in 'System' for ArrayMarshaller path purposes. + if (sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature) { return "System"; } + AsmResolver.DotNet.ITypeDefOrRef? td = null; + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature tds) { td = tds.Type; } + else if (sig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) { td = gi.GenericType; } + if (td is null) { return string.Empty; } + string typeNs = td.Namespace?.Value ?? string.Empty; + string typeName = td.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(typeNs, typeName); + return mapped is not null ? mapped.MappedNamespace : typeNs; + } + + /// + /// Encodes the array element type name as the interop generator's AppendRawTypeName at depth=0: + /// fundamentals use their short C# name; typedefs use just the type name (no namespace) prefixed + /// with the assembly marker; generic instances include their assembly marker, name, and type arguments. + /// + private static string EncodeArrayElementName(AsmResolver.DotNet.Signatures.TypeSignature elementType) + { + System.Text.StringBuilder sb = new(); + EncodeArrayElementNameInto(sb, elementType); + return sb.ToString(); + } + + private static void EncodeArrayElementNameInto(System.Text.StringBuilder sb, AsmResolver.DotNet.Signatures.TypeSignature sig) + { + // Special case for System.Guid: matches C++ guid_type handler in write_interop_dll_type_name. + // The depth=0 (top-level array element) form drops the namespace prefix and uses just the + // assembly marker + type name, so for Guid this becomes "<#corlib>Guid". + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature gtd + && gtd.Type?.Namespace?.Value == "System" + && gtd.Type?.Name?.Value == "Guid") + { + sb.Append("<#corlib>Guid"); + return; + } + switch (sig) + { + case AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib: + EncodeFundamental(sb, corlib, TypedefNameType.Projected); + return; + case AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td: + EncodeArrayElementForTypeDef(sb, td.Type, generic_args: null); + return; + case AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi: + EncodeArrayElementForTypeDef(sb, gi.GenericType, generic_args: gi.TypeArguments); + return; + default: + sb.Append(sig.FullName); + return; + } + } + + private static void EncodeArrayElementForTypeDef(System.Text.StringBuilder sb, AsmResolver.DotNet.ITypeDefOrRef type, System.Collections.Generic.IList? generic_args) + { + string typeNs = type.Namespace?.Value ?? string.Empty; + string typeName = type.Name?.Value ?? string.Empty; + // Apply mapped-type remapping (e.g. Windows.Foundation.IReference -> System.Nullable). + MappedType? mapped = MappedTypes.Get(typeNs, typeName); + if (mapped is not null) + { + typeNs = mapped.MappedNamespace; + typeName = mapped.MappedName; + } + // Replace generic arity backtick with apostrophe. + typeName = typeName.Replace('`', '\''); + + // Assembly marker prefix. Pass the type so that third-party (e.g. component-authored) + // types resolve to their actual assembly name (e.g. ) instead of + // defaulting to <#Windows>. + sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); + // Top-level: just the type name (no namespace). + sb.Append(typeName); + + // Generic arguments use the standard EncodeInteropTypeNameInto (depth > 0). + if (generic_args is { Count: > 0 }) + { + sb.Append('<'); + for (int i = 0; i < generic_args.Count; i++) + { + if (i > 0) { sb.Append('|'); } + EncodeInteropTypeNameInto(sb, generic_args[i], TypedefNameType.Projected); + } + sb.Append('>'); + } + } + + /// Mirrors C++ write_delegate_vtbl. + private static void WriteDelegateVftbl(TypeWriter w, TypeDefinition type) + { + if (type.GenericParameters.Count > 0) { return; } + MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); + if (invoke is null) { return; } + MethodSig sig = new(invoke); + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + + w.Write("\n[StructLayout(LayoutKind.Sequential)]\n"); + w.Write("internal unsafe struct "); + w.Write(nameStripped); + w.Write("Vftbl\n{\n"); + w.Write(" public delegate* unmanaged[MemberFunction] QueryInterface;\n"); + w.Write(" public delegate* unmanaged[MemberFunction] AddRef;\n"); + w.Write(" public delegate* unmanaged[MemberFunction] Release;\n"); + w.Write(" public delegate* unmanaged[MemberFunction]<"); + WriteAbiParameterTypesPointer(w, sig); + w.Write(", int> Invoke;\n"); + w.Write("}\n"); + } + + /// Mirrors C++ write_native_delegate. + private static void WriteNativeDelegate(TypeWriter w, TypeDefinition type) + { + if (type.GenericParameters.Count > 0) { return; } + MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); + if (invoke is null) { return; } + MethodSig sig = new(invoke); + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + + w.Write("\npublic static unsafe class "); + w.Write(nameStripped); + w.Write("NativeDelegate\n{\n"); + + w.Write(" public static unsafe "); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(nameStripped); + w.Write("Invoke(this WindowsRuntimeObjectReference thisReference"); + if (sig.Params.Count > 0) { w.Write(", "); } + WriteParameterList(w, sig); + w.Write(")"); + + // Reuse the interface caller body emitter. Delegate Invoke is at vtable slot 3 + // (after QI/AddRef/Release). Functionally equivalent to the truth's + // 'var abiInvoke = ((Vftbl*)*(void***)ThisPtr)->Invoke;' form, just routed + // through the slot-indexed dispatch shared with interface CCW callers. + EmitAbiMethodBodyIfSimple(w, sig, slot: 3, isNoExcept: Helpers.IsNoExcept(invoke)); + + w.Write("}\n"); + } + + /// Mirrors C++ write_delegates_interface_entries_impl. + private static void WriteDelegateInterfaceEntriesImpl(TypeWriter w, TypeDefinition type) + { + if (type.GenericParameters.Count > 0) { return; } + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + string iidExpr = w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, type))); + string iidRefExpr = w.WriteTemp("%", new System.Action(_ => WriteIidReferenceExpression(w, type))); + + w.Write("\nfile static class "); + w.Write(nameStripped); + w.Write("InterfaceEntriesImpl\n{\n"); + w.Write(" [FixedAddressValueType]\n"); + w.Write(" public static readonly DelegateReferenceInterfaceEntries Entries;\n\n"); + w.Write(" static "); + w.Write(nameStripped); + w.Write("InterfaceEntriesImpl()\n {\n"); + w.Write(" Entries.Delegate.IID = "); + w.Write(iidExpr); + w.Write(";\n"); + w.Write(" Entries.Delegate.Vtable = "); + w.Write(nameStripped); + w.Write("Impl.Vtable;\n"); + w.Write(" Entries.DelegateReference.IID = "); + w.Write(iidRefExpr); + w.Write(";\n"); + w.Write(" Entries.DelegateReference.Vtable = "); + w.Write(nameStripped); + w.Write("ReferenceImpl.Vtable;\n"); + w.Write(" Entries.IPropertyValue.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IPropertyValue;\n"); + w.Write(" Entries.IPropertyValue.Vtable = global::WindowsRuntime.InteropServices.IPropertyValueImpl.OtherTypeVtable;\n"); + w.Write(" Entries.IStringable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable;\n"); + w.Write(" Entries.IStringable.Vtable = global::WindowsRuntime.InteropServices.IStringableImpl.Vtable;\n"); + w.Write(" Entries.IWeakReferenceSource.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IWeakReferenceSource;\n"); + w.Write(" Entries.IWeakReferenceSource.Vtable = global::WindowsRuntime.InteropServices.IWeakReferenceSourceImpl.Vtable;\n"); + w.Write(" Entries.IMarshal.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IMarshal;\n"); + w.Write(" Entries.IMarshal.Vtable = global::WindowsRuntime.InteropServices.IMarshalImpl.Vtable;\n"); + w.Write(" Entries.IAgileObject.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IAgileObject;\n"); + w.Write(" Entries.IAgileObject.Vtable = global::WindowsRuntime.InteropServices.IAgileObjectImpl.Vtable;\n"); + w.Write(" Entries.IInspectable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IInspectable;\n"); + w.Write(" Entries.IInspectable.Vtable = global::WindowsRuntime.InteropServices.IInspectableImpl.Vtable;\n"); + w.Write(" Entries.IUnknown.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IUnknown;\n"); + w.Write(" Entries.IUnknown.Vtable = global::WindowsRuntime.InteropServices.IUnknownImpl.Vtable;\n"); + w.Write(" }\n}\n"); + } + + /// Mirrors C++ write_temp_delegate_event_source_subclass. + public static void WriteTempDelegateEventSourceSubclass(TypeWriter w, TypeDefinition type) + { + // Skip generic delegates: only non-generic delegates get a per-delegate EventSource subclass. + // Generic delegates (e.g. EventHandler) use the generic EventHandlerEventSource directly. + if (type.GenericParameters.Count > 0) { return; } + + MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); + if (invoke is null) { return; } + MethodSig sig = new(invoke); + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + + // Compute the projected type name (with global::) used as the generic argument. + string projectedName = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); + if (!projectedName.StartsWith("global::", System.StringComparison.Ordinal)) + { + projectedName = "global::" + projectedName; + } + + w.Write("\npublic sealed unsafe class "); + w.Write(nameStripped); + w.Write("EventSource : EventSource<"); + w.Write(projectedName); + w.Write(">\n{\n"); + w.Write(" /// \n"); + w.Write(" public "); + w.Write(nameStripped); + w.Write("EventSource(WindowsRuntimeObjectReference nativeObjectReference, int index)\n : base(nativeObjectReference, index)\n {\n }\n\n"); + w.Write(" /// \n"); + w.Write(" protected override WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); + w.Write(projectedName); + w.Write(" value)\n {\n return "); + w.Write(nameStripped); + w.Write("Marshaller.ConvertToUnmanaged(value);\n }\n\n"); + w.Write(" /// \n"); + w.Write(" protected override EventSourceState<"); + w.Write(projectedName); + w.Write("> CreateEventSourceState()\n {\n return new EventState(GetNativeObjectReferenceThisPtrUnsafe(), Index);\n }\n\n"); + w.Write(" private sealed class EventState : EventSourceState<"); + w.Write(projectedName); + w.Write(">\n {\n"); + w.Write(" /// \n"); + w.Write(" public EventState(void* thisPtr, int index)\n : base(thisPtr, index)\n {\n }\n\n"); + w.Write(" /// \n"); + w.Write(" protected override "); + w.Write(projectedName); + w.Write(" GetEventInvoke()\n {\n"); + // Build parameter name list for the lambda. Lambda's parameter list MUST match the + // delegate's signature exactly, including in/out/ref modifiers - otherwise CS1676 fires + // when calling TargetDelegate.Invoke. Mirror C++ write_parmaeters. + w.Write(" return ("); + for (int i = 0; i < sig.Params.Count; i++) + { + if (i > 0) { w.Write(", "); } + ParamCategory pc = ParamHelpers.GetParamCategory(sig.Params[i]); + if (pc == ParamCategory.Ref) { w.Write("in "); } + else if (pc == ParamCategory.Out || pc == ParamCategory.ReceiveArray) { w.Write("out "); } + string raw = sig.Params[i].Parameter.Name ?? "p"; + w.Write(Helpers.IsKeyword(raw) ? "@" + raw : raw); + } + w.Write(") => TargetDelegate.Invoke("); + for (int i = 0; i < sig.Params.Count; i++) + { + if (i > 0) { w.Write(", "); } + ParamCategory pc = ParamHelpers.GetParamCategory(sig.Params[i]); + if (pc == ParamCategory.Ref) { w.Write("in "); } + else if (pc == ParamCategory.Out || pc == ParamCategory.ReceiveArray) { w.Write("out "); } + string raw = sig.Params[i].Parameter.Name ?? "p"; + w.Write(Helpers.IsKeyword(raw) ? "@" + raw : raw); + } + w.Write(");\n"); + w.Write(" }\n }\n}\n"); + } + + /// Mirrors C++ write_abi_class. + public static void WriteAbiClass(TypeWriter w, TypeDefinition type) + { + // Static classes don't get a *Marshaller (no instances). + if (TypeCategorization.IsStatic(type)) { return; } + // Mirror C++ write_abi_class: wrap class marshaller emission in #nullable enable/disable. + w.Write("#nullable enable\n"); + if (w.Settings.Component) + { + // Mirror C++ write_component_class_marshaller + write_authoring_metadata_type. + WriteComponentClassMarshaller(w, type); + WriteAuthoringMetadataType(w, type); + } + else + { + // Emit a ComWrappers marshaller class so the attribute reference resolves + WriteClassMarshallerStub(w, type); + } + w.Write("#nullable disable\n"); + } + + /// + /// Emits the simpler component-mode class marshaller. Mirrors C++ + /// write_component_class_marshaller. + /// + private static void WriteComponentClassMarshaller(TypeWriter w, TypeDefinition type) + { + string nameStripped = Helpers.StripBackticks(type.Name?.Value ?? string.Empty); + string typeNs = type.Namespace?.Value ?? string.Empty; + string projectedType = $"global::{typeNs}.{nameStripped}"; + + ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); + + // Mirror C++ write_component_class_marshaller: if the default interface is a generic + // instantiation (e.g. IDictionary), emit an UnsafeAccessor extern declaration + // inside ConvertToUnmanaged that fetches the IID via WinRT.Interop's InterfaceIIDs class + // (since the IID for a generic instantiation is computed at runtime). The IID expression + // in the call then becomes '(null)' instead of a static InterfaceIIDs reference. + AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? defaultGenericInst = null; + if (defaultIface is AsmResolver.DotNet.TypeSpecification spec + && spec.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) + { + defaultGenericInst = gi; + } + + string defaultIfaceIid; + if (defaultGenericInst is not null) + { + // Call the accessor: '>(null)'. + string accessorName = BuildIidPropertyNameForGenericInterface(w, defaultGenericInst); + defaultIfaceIid = accessorName + "(null)"; + } + else + { + defaultIfaceIid = defaultIface is not null + ? w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, defaultIface))) + : "default(global::System.Guid)"; + } + + w.Write("\npublic static unsafe class "); + w.Write(nameStripped); + w.Write("Marshaller\n{\n"); + w.Write(" public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); + w.Write(projectedType); + w.Write(" value)\n {\n"); + if (defaultGenericInst is not null) + { + // Emit the UnsafeAccessor declaration (uses 'object?' since component-mode + // marshallers run inside #nullable enable). + string accessorBlock = w.WriteTemp("%", new System.Action(_ => EmitUnsafeAccessorForIid(w, defaultGenericInst, isInNullableContext: true))); + // Re-emit each line indented by 8 spaces. + string[] accessorLines = accessorBlock.TrimEnd('\n').Split('\n'); + foreach (string accessorLine in accessorLines) + { + w.Write(" "); + w.Write(accessorLine); + w.Write("\n"); + } + } + w.Write(" return WindowsRuntimeInterfaceMarshaller<"); + w.Write(projectedType); + w.Write(">.ConvertToUnmanaged(value, "); + w.Write(defaultIfaceIid); + w.Write(");\n }\n\n"); + w.Write(" public static "); + w.Write(projectedType); + w.Write("? ConvertToManaged(void* value)\n {\n"); + w.Write(" return ("); + w.Write(projectedType); + w.Write("?) WindowsRuntimeObjectMarshaller.ConvertToManaged(value);\n }\n}\n"); + } + + /// + /// Emits the metadata wrapper type file static class <Name> {} with the conditional + /// set of attributes required for the type's category. Mirrors C++ + /// write_authoring_metadata_type. + /// + private static void WriteAuthoringMetadataType(TypeWriter w, TypeDefinition type) + { + string nameStripped = Helpers.StripBackticks(type.Name?.Value ?? string.Empty); + string typeNs = type.Namespace?.Value ?? string.Empty; + string projectedType = string.IsNullOrEmpty(typeNs) ? $"global::{nameStripped}" : $"global::{typeNs}.{nameStripped}"; + string fullName = string.IsNullOrEmpty(typeNs) ? nameStripped : $"{typeNs}.{nameStripped}"; + TypeCategory category = TypeCategorization.GetCategory(type); + + // [WindowsRuntimeReferenceType(typeof(?))] for non-delegate, non-class types + // (i.e. enums, structs, interfaces). + if (category != TypeCategory.Delegate && category != TypeCategory.Class) + { + w.Write("[WindowsRuntimeReferenceType(typeof("); + w.Write(projectedType); + w.Write("?))]\n"); + } + + // [ABI..ComWrappersMarshaller] for non-struct, non-class types + // (delegates, enums, interfaces). + if (category != TypeCategory.Struct && category != TypeCategory.Class) + { + w.Write("[ABI."); + w.Write(typeNs); + w.Write("."); + w.Write(nameStripped); + w.Write("ComWrappersMarshaller]\n"); + } + + // [WindowsRuntimeClassName("Windows.Foundation.IReference`1<.>")] for non-class types. + if (category != TypeCategory.Class) + { + w.Write("[WindowsRuntimeClassName(\"Windows.Foundation.IReference`1<"); + w.Write(fullName); + w.Write(">\")]\n"); + } + + w.Write("[WindowsRuntimeMetadataTypeName(\""); + w.Write(fullName); + w.Write("\")]\n"); + w.Write("[WindowsRuntimeMappedType(typeof("); + w.Write(projectedType); + w.Write("))]\n"); + w.Write("file static class "); + w.Write(nameStripped); + w.Write(" {}\n"); + } + + /// Mirrors C++ write_abi_interface. + public static void WriteAbiInterface(TypeWriter w, TypeDefinition type) + { + // Generic interfaces are handled by interopgen + if (type.GenericParameters.Count > 0) { return; } + + // The C++ also emits write_static_abi_classes here - we emit a basic stub for now + WriteInterfaceMarshallerStub(w, type); + + // For internal projections, just the static ABI methods class is enough. + if (TypeCategorization.IsProjectionInternal(type)) { return; } + + WriteInterfaceVftbl(w, type); + WriteInterfaceImpl(w, type); + WriteInterfaceIdicImpl(w, type); + WriteInterfaceMarshaller(w, type); + } + + /// Mirrors C++ emit_impl_type. + public static bool EmitImplType(TypeWriter w, TypeDefinition type) + { + if (w.Settings.Component) { return true; } + if (TypeCategorization.IsExclusiveTo(type) && !w.Settings.PublicExclusiveTo) + { + // Mirror C++ emit_impl_type: only emit Impl for exclusive-to interfaces if at least + // one interface impl on the exclusive_to class is marked [Overridable] and matches + // this interface. Otherwise the Impl wouldn't be reachable as a CCW. + TypeDefinition? exclusiveToType = GetExclusiveToType(type); + if (exclusiveToType is null) { return true; } + bool hasOverridable = false; + foreach (InterfaceImplementation impl in exclusiveToType.Interfaces) + { + if (impl.Interface is null) { continue; } + TypeDefinition? ifaceTd = ResolveInterfaceTypeDef(impl.Interface); + if (ifaceTd == type && Helpers.IsOverridable(impl)) { hasOverridable = true; break; } + } + return hasOverridable; + } + return true; + } + + /// + /// Returns the parent class for an interface marked [ExclusiveToAttribute(typeof(T))]. + /// Mirrors C++ get_exclusive_to_type. + /// + internal static TypeDefinition? GetExclusiveToType(TypeDefinition iface) + { + if (_cacheRef is null) { return null; } + for (int i = 0; i < iface.CustomAttributes.Count; i++) + { + CustomAttribute attr = iface.CustomAttributes[i]; + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + if (attrType is null) { continue; } + if (attrType.Namespace?.Value != "Windows.Foundation.Metadata" || + attrType.Name?.Value != "ExclusiveToAttribute") { continue; } + if (attr.Signature is null) { continue; } + for (int j = 0; j < attr.Signature.FixedArguments.Count; j++) + { + AsmResolver.DotNet.Signatures.CustomAttributeArgument arg = attr.Signature.FixedArguments[j]; + if (arg.Element is AsmResolver.DotNet.Signatures.TypeSignature sig) + { + string fullName = sig.FullName ?? string.Empty; + TypeDefinition? td = _cacheRef.Find(fullName); + if (td is not null) { return td; } + } + else if (arg.Element is string s) + { + TypeDefinition? td = _cacheRef.Find(s); + if (td is not null) { return td; } + } + } + } + return null; + } + + /// Resolves an InterfaceImpl's interface reference to a TypeDefinition (same module or via metadata cache). + private static TypeDefinition? ResolveInterfaceTypeDef(ITypeDefOrRef ifaceRef) + { + if (ifaceRef is TypeDefinition td) { return td; } + if (ifaceRef is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) + { + ITypeDefOrRef? gen = gi.GenericType; + if (gen is TypeDefinition gtd) { return gtd; } + if (gen is TypeReference gtr && _cacheRef is not null) + { + string ns = gtr.Namespace?.Value ?? string.Empty; + string nm = gtr.Name?.Value ?? string.Empty; + return _cacheRef.Find(ns + "." + nm); + } + } + if (ifaceRef is TypeReference tr && _cacheRef is not null) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string nm = tr.Name?.Value ?? string.Empty; + return _cacheRef.Find(ns + "." + nm); + } + return null; + } + + /// Mirrors C++ get_vmethod_name. + public static string GetVMethodName(TypeDefinition type, MethodDefinition method) + { + // Index of method in the type's method list + int index = 0; + foreach (MethodDefinition m in type.Methods) + { + if (m == method) { break; } + index++; + } + return (method.Name?.Value ?? string.Empty) + "_" + index.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + /// Mirrors C++ write_abi_parameter_types_pointer. + public static void WriteAbiParameterTypesPointer(TypeWriter w, MethodSig sig) + { + WriteAbiParameterTypesPointer(w, sig, includeParamNames: false); + } + + /// + /// Writes the ABI parameter types for a vtable function pointer signature, optionally + /// including parameter names (for method declarations vs. function pointer type lists). + /// + public static void WriteAbiParameterTypesPointer(TypeWriter w, MethodSig sig, bool includeParamNames) + { + // void* thisPtr, then each param's ABI type, then return type pointer + w.Write("void*"); + if (includeParamNames) { w.Write(" thisPtr"); } + for (int i = 0; i < sig.Params.Count; i++) + { + w.Write(", "); + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (p.Type is AsmResolver.DotNet.Signatures.SzArrayTypeSignature sz) + { + // length pointer + value pointer. Mirrors C++ write_abi_signature for SzArray + // input params (code_writers.h:1305) which always emits "uint __%Size, void* %" + // regardless of element type. + if (includeParamNames) + { + w.Write("uint "); + w.Write("__"); + w.Write(p.Parameter.Name ?? "param"); + w.Write("Size, void* "); + Helpers.WriteEscapedIdentifier(w, p.Parameter.Name ?? "param"); + } + else + { + w.Write("uint, void*"); + } + _ = sz; + } + else if (p.Type is AsmResolver.DotNet.Signatures.ByReferenceTypeSignature br) + { + // Special case: 'out T[]' is a ReceiveArray ABI signature: (uint* size, T** data). + if (br.BaseType is AsmResolver.DotNet.Signatures.SzArrayTypeSignature brSz && cat == ParamCategory.ReceiveArray) + { + bool isRefElemBr = IsString(brSz.BaseType) || IsRuntimeClassOrInterface(brSz.BaseType) || IsObject(brSz.BaseType) || IsGenericInstance(brSz.BaseType); + if (includeParamNames) + { + w.Write("uint* __"); + w.Write(p.Parameter.Name ?? "param"); + w.Write("Size, "); + if (isRefElemBr) { w.Write("void*** "); } + else + { + WriteAbiType(w, TypeSemanticsFactory.Get(brSz.BaseType)); + w.Write("** "); + } + Helpers.WriteEscapedIdentifier(w, p.Parameter.Name ?? "param"); + } + else + { + w.Write("uint*, "); + if (isRefElemBr) { w.Write("void***"); } + else + { + WriteAbiType(w, TypeSemanticsFactory.Get(brSz.BaseType)); + w.Write("**"); + } + } + } + else + { + WriteAbiType(w, TypeSemanticsFactory.Get(br.BaseType)); + w.Write("*"); + if (includeParamNames) + { + w.Write(" "); + Helpers.WriteEscapedIdentifier(w, p.Parameter.Name ?? "param"); + } + } + } + else + { + WriteAbiType(w, TypeSemanticsFactory.Get(p.Type)); + if (cat is ParamCategory.Out or ParamCategory.Ref) { w.Write("*"); } + if (includeParamNames) + { + w.Write(" "); + Helpers.WriteEscapedIdentifier(w, p.Parameter.Name ?? "param"); + } + } + } + // Return parameter + if (sig.ReturnType is not null) + { + w.Write(", "); + string retName = GetReturnParamName(sig); + string retSizeName = GetReturnSizeParamName(sig); + // Special handling for SzArray return types: WinRT projects them as a (uint*, T**) pair. + if (sig.ReturnType is AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz) + { + if (includeParamNames) + { + w.Write("uint* "); + w.Write(retSizeName); + w.Write(", "); + WriteAbiType(w, TypeSemanticsFactory.Get(retSz.BaseType)); + w.Write("** "); + w.Write(retName); + } + else + { + w.Write("uint*, "); + WriteAbiType(w, TypeSemanticsFactory.Get(retSz.BaseType)); + w.Write("**"); + } + } + else + { + WriteAbiType(w, TypeSemanticsFactory.Get(sig.ReturnType)); + w.Write("*"); + if (includeParamNames) { w.Write(' '); w.Write(retName); } + } + } + } + + /// + /// Returns the metadata-derived name for the return parameter, or the C++ default __return_value__. + /// Mirrors method_signature::return_param_name() in helpers.h. + /// + internal static string GetReturnParamName(MethodSig sig) + { + string? n = sig.ReturnParam?.Name?.Value; + if (string.IsNullOrEmpty(n)) { return "__return_value__"; } + return Helpers.IsKeyword(n) ? "@" + n : n; + } + + /// + /// Returns the local-variable name for the return parameter on the server side. Mirrors C++ + /// abi_marshaler::get_marshaler_local() which prefixes __ to the param name. + /// + internal static string GetReturnLocalName(MethodSig sig) + { + return "__" + GetReturnParamName(sig); + } + + /// Returns '__<returnName>Size' (matches C++ '__%Size' convention) — by default '____return_value__Size' for the standard '__return_value__' return param. + internal static string GetReturnSizeParamName(MethodSig sig) + { + // Mirrors C++ 'write_abi_parameter_types_pointer' which writes '__%Size' over the return param name. + return "__" + GetReturnParamName(sig) + "Size"; + } + + /// Mirrors C++ write_interface_vftbl. + public static void WriteInterfaceVftbl(TypeWriter w, TypeDefinition type) + { + if (!EmitImplType(w, type)) { return; } + if (type.GenericParameters.Count > 0) { return; } + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + + w.Write("\n[StructLayout(LayoutKind.Sequential)]\n"); + w.Write("internal unsafe struct "); + w.Write(nameStripped); + w.Write("Vftbl\n{\n"); + w.Write("public delegate* unmanaged[MemberFunction] QueryInterface;\n"); + w.Write("public delegate* unmanaged[MemberFunction] AddRef;\n"); + w.Write("public delegate* unmanaged[MemberFunction] Release;\n"); + w.Write("public delegate* unmanaged[MemberFunction] GetIids;\n"); + w.Write("public delegate* unmanaged[MemberFunction] GetRuntimeClassName;\n"); + w.Write("public delegate* unmanaged[MemberFunction] GetTrustLevel;\n"); + + foreach (MethodDefinition method in type.Methods) + { + string vm = GetVMethodName(type, method); + MethodSig sig = new(method); + w.Write("public delegate* unmanaged[MemberFunction]<"); + WriteAbiParameterTypesPointer(w, sig); + w.Write(", int> "); + w.Write(vm); + w.Write(";\n"); + } + w.Write("}\n"); + } + + /// Mirrors C++ write_interface_impl (simplified). + public static void WriteInterfaceImpl(TypeWriter w, TypeDefinition type) + { + if (!EmitImplType(w, type)) { return; } + if (type.GenericParameters.Count > 0) { return; } + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + + w.Write("\npublic static unsafe class "); + w.Write(nameStripped); + w.Write("Impl\n{\n"); + w.Write("[FixedAddressValueType]\n"); + w.Write("private static readonly "); + w.Write(nameStripped); + w.Write("Vftbl Vftbl;\n\n"); + + w.Write("static "); + w.Write(nameStripped); + w.Write("Impl()\n{\n"); + w.Write(" *(IInspectableVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IInspectableVftbl*)IInspectableImpl.Vtable;\n"); + foreach (MethodDefinition method in type.Methods) + { + string vm = GetVMethodName(type, method); + w.Write(" Vftbl."); + w.Write(vm); + w.Write(" = &Do_Abi_"); + w.Write(vm); + w.Write(";\n"); + } + w.Write("}\n\n"); + + w.Write("public static ref readonly Guid IID\n{\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => ref "); + WriteIidGuidReference(w, type); + w.Write(";\n}\n\n"); + + w.Write("public static nint Vtable\n{\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => (nint)Unsafe.AsPointer(in Vftbl);\n}\n\n"); + + // Do_Abi_* implementations: emit real bodies for simple primitive cases, + // throw null! for everything else (deferred — needs full per-parameter marshalling). + // Mirror C++: in component mode, exclusive-to interfaces dispatch to the OWNING class + // type (not the interface) since the authored class IS the implementation. This is what + // 'write_method_abi_invoke' produces because 'method.Parent()' is treated through + // 'does_abi_interface_implement_ccw_interface' for authoring scenarios. + // + // EXCEPTION: static factory interfaces ([Static] attr on the class) and activation + // factory interfaces ([Activatable(typeof(IFooFactory))]) are implemented by the + // generated 'ABI.Impl..'/' types, NOT by the user runtime + // class. For those, the dispatch target must be 'global::ABI.Impl..'. + TypeDefinition? exclusiveToOwner = null; + bool exclusiveIsFactoryOrStatic = false; + if (w.Settings.Component) + { + MetadataCache? cache = GetMetadataCache(); + if (cache is not null) + { + exclusiveToOwner = Helpers.GetExclusiveToType(type, cache); + if (exclusiveToOwner is not null) + { + foreach (KeyValuePair kv in AttributedTypes.Get(exclusiveToOwner, cache)) + { + if (kv.Value.Type == type && (kv.Value.Statics || kv.Value.Activatable)) + { + exclusiveIsFactoryOrStatic = true; + break; + } + } + } + } + } + + string ifaceFullName; + if (exclusiveToOwner is not null && !exclusiveIsFactoryOrStatic) + { + string ownerNs = exclusiveToOwner.Namespace?.Value ?? string.Empty; + string ownerNm = Helpers.StripBackticks(exclusiveToOwner.Name?.Value ?? string.Empty); + ifaceFullName = string.IsNullOrEmpty(ownerNs) + ? "global::" + ownerNm + : "global::" + ownerNs + "." + ownerNm; + } + else if (exclusiveToOwner is not null && exclusiveIsFactoryOrStatic) + { + // Factory/static interfaces in authoring mode are implemented by the generated + // 'global::ABI.Impl..' type that the activation factory CCW exposes. + string ifaceNs = type.Namespace?.Value ?? string.Empty; + string ifaceNm = Helpers.StripBackticks(type.Name?.Value ?? string.Empty); + ifaceFullName = string.IsNullOrEmpty(ifaceNs) + ? "global::ABI.Impl." + ifaceNm + : "global::ABI.Impl." + ifaceNs + "." + ifaceNm; + } + else + { + ifaceFullName = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); + if (!ifaceFullName.StartsWith("global::", System.StringComparison.Ordinal)) { ifaceFullName = "global::" + ifaceFullName; } + } + + // Build a map of event add/remove methods to their event so we can emit the table field + // and the proper Do_Abi_add_*/Do_Abi_remove_* bodies (mirrors C++ write_event_abi_invoke). + System.Collections.Generic.Dictionary? eventMap = BuildEventMethodMap(type); + + // Build sets of property accessors and event accessors so the first loop below can + // iterate "regular" methods (non-property, non-event) only. C++ emits Do_Abi bodies in + // this order: methods first, then properties (setter before getter per write_property_abi_invoke + // at code_writers.h:8245), then events. Mine previously emitted them in pure metadata + // (slot) order which matched neither truth nor C++. + System.Collections.Generic.HashSet propertyAccessors = new(); + foreach (PropertyDefinition prop in type.Properties) + { + if (prop.GetMethod is MethodDefinition g) { propertyAccessors.Add(g); } + if (prop.SetMethod is MethodDefinition s) { propertyAccessors.Add(s); } + } + + // Local helper to emit a single Do_Abi method body for a given MethodDefinition. + void EmitOneDoAbi(MethodDefinition method) + { + string vm = GetVMethodName(type, method); + MethodSig sig = new(method); + string mname = method.Name?.Value ?? string.Empty; + + // If this method is an event add accessor, emit the per-event ConditionalWeakTable + // before the Do_Abi method (mirrors C++ ordering). + if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt) && evt.AddMethod == method) + { + EmitEventTableField(w, evt, type, ifaceFullName); + } + + w.Write("[UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]\n"); + w.Write("private static unsafe int Do_Abi_"); + w.Write(vm); + w.Write("("); + WriteAbiParameterTypesPointer(w, sig, includeParamNames: true); + w.Write(")"); + + if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt2)) + { + if (evt2.AddMethod == method) + { + EmitDoAbiAddEvent(w, evt2, sig, ifaceFullName); + } + else + { + EmitDoAbiRemoveEvent(w, evt2, sig, ifaceFullName); + } + } + else + { + EmitDoAbiBodyIfSimple(w, sig, ifaceFullName, mname); + } + } + + // 1. Regular methods (non-property, non-event), in metadata order. + foreach (MethodDefinition method in type.Methods) + { + if (propertyAccessors.Contains(method)) { continue; } + if (eventMap is not null && eventMap.ContainsKey(method)) { continue; } + EmitOneDoAbi(method); + } + + // 2. Properties, in metadata order. Setter before getter per write_property_abi_invoke. + foreach (PropertyDefinition prop in type.Properties) + { + if (prop.SetMethod is MethodDefinition s) { EmitOneDoAbi(s); } + if (prop.GetMethod is MethodDefinition g) { EmitOneDoAbi(g); } + } + + // 3. Events, in metadata order. Add then Remove (matches metadata order from BuildEventMethodMap). + foreach (EventDefinition evt in type.Events) + { + if (evt.AddMethod is MethodDefinition a) { EmitOneDoAbi(a); } + if (evt.RemoveMethod is MethodDefinition r) { EmitOneDoAbi(r); } + } + w.Write("}\n"); + } + + /// Build a method-to-event map for add/remove accessors of a type. + private static System.Collections.Generic.Dictionary? BuildEventMethodMap(TypeDefinition type) + { + if (type.Events.Count == 0) { return null; } + System.Collections.Generic.Dictionary map = new(); + foreach (EventDefinition evt in type.Events) + { + if (evt.AddMethod is MethodDefinition add) { map[add] = evt; } + if (evt.RemoveMethod is MethodDefinition rem) { map[rem] = evt; } + } + return map; + } + + /// + /// Emits the per-event ConditionalWeakTable<TInterface, EventRegistrationTokenTable<THandler>> + /// backing field property. Mirrors the table emission in C++ write_event_abi_invoke. + /// The is the dispatch target type for the CCW (computed by + /// the caller in EmitDoAbiBodyIfSimple) — for instance events on authored classes this is + /// the runtime class type, NOT the ABI.Impl interface. + /// + private static void EmitEventTableField(TypeWriter w, EventDefinition evt, TypeDefinition iface, string ifaceFullName) + { + string evName = evt.Name?.Value ?? "Event"; + string evtType = w.WriteTemp("%", new System.Action(_ => WriteEventType(w, evt))); + + w.Write("\nprivate static ConditionalWeakTable<"); + w.Write(ifaceFullName); + w.Write(", EventRegistrationTokenTable<"); + w.Write(evtType); + w.Write(">> _"); + w.Write(evName); + w.Write("\n{\n"); + w.Write(" [MethodImpl(MethodImplOptions.AggressiveInlining)]\n"); + w.Write(" get\n {\n"); + w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); + w.Write(" static ConditionalWeakTable<"); + w.Write(ifaceFullName); + w.Write(", EventRegistrationTokenTable<"); + w.Write(evtType); + w.Write(">> MakeTable()\n {\n"); + w.Write(" _ = global::System.Threading.Interlocked.CompareExchange(ref field, [], null);\n\n"); + w.Write(" return global::System.Threading.Volatile.Read(in field);\n"); + w.Write(" }\n\n"); + w.Write(" return global::System.Threading.Volatile.Read(in field) ?? MakeTable();\n }\n}\n"); + } + + /// + /// Emits the body of the Do_Abi_add_<EventName>_N method. Mirrors the corresponding + /// branch in C++ write_event_abi_invoke. + /// + private static void EmitDoAbiAddEvent(TypeWriter w, EventDefinition evt, MethodSig sig, string ifaceFullName) + { + string evName = evt.Name?.Value ?? "Event"; + // Handler is the (last) input parameter of the add method. The emitted parameter name in the + // signature comes from WriteAbiParameterTypesPointer which uses the metadata name verbatim. + string handlerRawName = sig.Params.Count > 0 ? (sig.Params[^1].Parameter.Name ?? "handler") : "handler"; + string handlerRef = Helpers.IsKeyword(handlerRawName) ? "@" + handlerRawName : handlerRawName; + + // The cookie/token return parameter takes the metadata return param name (matches truth). + string cookieName = GetReturnParamName(sig); + + AsmResolver.DotNet.Signatures.TypeSignature evtTypeSig = evt.EventType!.ToTypeSignature(false); + bool isGeneric = evtTypeSig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature; + + w.Write("\n{\n"); + w.Write(" *"); + w.Write(cookieName); + w.Write(" = default;\n"); + w.Write(" try\n {\n"); + w.Write(" var __this = ComInterfaceDispatch.GetInstance<"); + w.Write(ifaceFullName); + w.Write(">((ComInterfaceDispatch*)thisPtr);\n"); + + if (isGeneric) + { + string interopTypeName = EncodeInteropTypeName(evtTypeSig, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, evtTypeSig, false))); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); + w.Write(" static extern "); + w.Write(projectedTypeName); + w.Write(" ConvertToManaged([UnsafeAccessorType(\""); + w.Write(interopTypeName); + w.Write("\")] object _, void* value);\n"); + w.Write(" var __handler = ConvertToManaged(null, "); + w.Write(handlerRef); + w.Write(");\n"); + } + else + { + w.Write(" var __handler = "); + WriteTypeName(w, TypeSemanticsFactory.Get(evtTypeSig), TypedefNameType.ABI, false); + w.Write("Marshaller.ConvertToManaged("); + w.Write(handlerRef); + w.Write(");\n"); + } + + w.Write(" *"); + w.Write(cookieName); + w.Write(" = _"); + w.Write(evName); + w.Write(".GetOrCreateValue(__this).AddEventHandler(__handler);\n"); + w.Write(" __this."); + w.Write(evName); + w.Write(" += __handler;\n"); + w.Write(" return 0;\n }\n"); + w.Write(" catch (Exception __exception__)\n {\n"); + w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__);\n }\n}\n"); + } + + /// + /// Emits the body of the Do_Abi_remove_<EventName>_N method. Mirrors the corresponding + /// branch in C++ write_event_abi_invoke. + /// + private static void EmitDoAbiRemoveEvent(TypeWriter w, EventDefinition evt, MethodSig sig, string ifaceFullName) + { + string evName = evt.Name?.Value ?? "Event"; + string tokenRawName = sig.Params.Count > 0 ? (sig.Params[^1].Parameter.Name ?? "token") : "token"; + string tokenRef = Helpers.IsKeyword(tokenRawName) ? "@" + tokenRawName : tokenRawName; + + w.Write("\n{\n"); + w.Write(" try\n {\n"); + w.Write(" var __this = ComInterfaceDispatch.GetInstance<"); + w.Write(ifaceFullName); + w.Write(">((ComInterfaceDispatch*)thisPtr);\n"); + w.Write(" if(__this is not null && _"); + w.Write(evName); + w.Write(".TryGetValue(__this, out var __table) && __table.RemoveEventHandler("); + w.Write(tokenRef); + w.Write(", out var __handler))\n {\n"); + w.Write(" __this."); + w.Write(evName); + w.Write(" -= __handler;\n"); + w.Write(" }\n"); + w.Write(" return 0;\n }\n"); + w.Write(" catch (Exception __exception__)\n {\n"); + w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__);\n }\n}\n"); + } + + /// + /// Emits a real Do_Abi (CCW) body for the cases we can handle. Mirrors C++ + /// write_abi_method_call_marshalers (code_writers.h:6682) which + /// unconditionally emits a real body via the abi_marshaler abstraction + /// for every WinRT-valid signature. + /// + private static void EmitDoAbiBodyIfSimple(TypeWriter w, MethodSig sig, string ifaceFullName, string methodName) + { + AsmResolver.DotNet.Signatures.TypeSignature? rt = sig.ReturnType; + + // String params drive whether we need HString header allocation in the body. + bool hasStringParams = false; + foreach (ParamInfo p in sig.Params) + { + if (IsString(p.Type)) { hasStringParams = true; break; } + } + bool returnIsReceiveArrayDoAbi = rt is AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSzAbi + && (IsBlittablePrimitive(retSzAbi.BaseType) || IsAnyStruct(retSzAbi.BaseType) + || IsString(retSzAbi.BaseType) || IsRuntimeClassOrInterface(retSzAbi.BaseType) || IsObject(retSzAbi.BaseType) + || IsComplexStruct(retSzAbi.BaseType)); + bool returnIsHResultExceptionDoAbi = rt is not null && IsHResultException(rt); + bool returnIsString = rt is not null && IsString(rt); + bool returnIsRefType = rt is not null && (IsRuntimeClassOrInterface(rt) || IsObject(rt) || IsGenericInstance(rt)); + bool returnIsGenericInstance = rt is not null && IsGenericInstance(rt); + bool returnIsBlittableStruct = rt is not null && IsAnyStruct(rt); + + bool isGetter = methodName.StartsWith("get_", System.StringComparison.Ordinal); + bool isSetter = methodName.StartsWith("put_", System.StringComparison.Ordinal); + bool isAddEvent = methodName.StartsWith("add_", System.StringComparison.Ordinal); + bool isRemoveEvent = methodName.StartsWith("remove_", System.StringComparison.Ordinal); + + if (isAddEvent || isRemoveEvent) + { + // Events go through dedicated EmitDoAbiAddEvent / EmitDoAbiRemoveEvent paths + // upstream (see lines 1153-1159). If we reach here for an event accessor it's a + // generator bug. Defensive guard against future regressions. + throw new System.InvalidOperationException( + $"EmitDoAbiBodyIfSimple: unexpectedly called for event accessor '{methodName}' " + + $"on '{ifaceFullName}'. Events should dispatch through EmitDoAbiAddEvent / EmitDoAbiRemoveEvent."); + } + + w.Write("\n{\n"); + string retParamName = GetReturnParamName(sig); + string retSizeParamName = GetReturnSizeParamName(sig); + // The local name for the unmarshalled return value mirrors C++ + // 'abi_marshaler::get_marshaler_local()' which prefixes '__' to the param name. + // For the default '__return_value__' param this becomes '____return_value__'. + string retLocalName = "__" + retParamName; + + // Mirror C++ ordering: emit any [UnsafeAccessor] static local function declarations + // at the TOP of the method body (before local declarations and the try block). The + // actual call sites later in the body just reference the already-declared accessor. + // For a generic-instance return type, the accessor is named ConvertToUnmanaged_. + // Skip Nullable returns: those use Marshaller.BoxToUnmanaged at the call site + // instead of the generic-instance UnsafeAccessor (V3-M7). + if (returnIsGenericInstance && !(rt is not null && IsNullableT(rt))) + { + string interopTypeName = EncodeInteropTypeName(rt!, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt!, false))); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); + w.Write(" static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_"); + w.Write(retParamName); + w.Write("([UnsafeAccessorType(\""); + w.Write(interopTypeName); + w.Write("\")] object _, "); + w.Write(projectedTypeName); + w.Write(" value);\n\n"); + } + + // Hoist [UnsafeAccessor] declarations for Out generic-instance params: + // ConvertToUnmanaged_ wraps the projected value into a WindowsRuntimeObjectReferenceValue. + // The body's writeback later references these already-declared accessors. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.Out) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); + if (!IsGenericInstance(uOut)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string interopTypeName = EncodeInteropTypeName(uOut, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, uOut, false))); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); + w.Write(" static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_"); + w.Write(raw); + w.Write("([UnsafeAccessorType(\""); + w.Write(interopTypeName); + w.Write("\")] object _, "); + w.Write(projectedTypeName); + w.Write(" value);\n\n"); + } + + // Mirror C++ ordering: hoist [UnsafeAccessor] declarations for ReceiveArray (out T[]) + // ConvertToUnmanaged_ and the return-array ConvertToUnmanaged_ to the + // top of the method body, before locals and the try block. The actual call sites later + // in the body reference these already-declared accessors. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.ReceiveArray) { continue; } + string raw = p.Parameter.Name ?? "param"; + AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(sza.BaseType)))); + string elementInteropArg = EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); + string marshallerPath = GetArrayMarshallerInteropPath(w, sza.BaseType, elementInteropArg); + string elementAbi = IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType) + ? "void*" + : IsComplexStruct(sza.BaseType) + ? GetAbiStructTypeName(w, sza.BaseType) + : IsAnyStruct(sza.BaseType) + ? GetBlittableStructAbiType(w, sza.BaseType) + : GetAbiPrimitiveType(sza.BaseType); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); + w.Write(" static extern void ConvertToUnmanaged_"); + w.Write(raw); + w.Write("([UnsafeAccessorType(\""); + w.Write(marshallerPath); + w.Write("\")] object _, ReadOnlySpan<"); + w.Write(elementProjected); + w.Write("> span, out uint length, out "); + w.Write(elementAbi); + w.Write("* data);\n\n"); + } + if (returnIsReceiveArrayDoAbi && rt is AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSzHoist) + { + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(retSzHoist.BaseType)))); + string elementAbi = IsString(retSzHoist.BaseType) || IsRuntimeClassOrInterface(retSzHoist.BaseType) || IsObject(retSzHoist.BaseType) + ? "void*" + : IsComplexStruct(retSzHoist.BaseType) + ? GetAbiStructTypeName(w, retSzHoist.BaseType) + : IsAnyStruct(retSzHoist.BaseType) + ? GetBlittableStructAbiType(w, retSzHoist.BaseType) + : GetAbiPrimitiveType(retSzHoist.BaseType); + string elementInteropArg = EncodeInteropTypeName(retSzHoist.BaseType, TypedefNameType.Projected); + string marshallerPath = GetArrayMarshallerInteropPath(w, retSzHoist.BaseType, elementInteropArg); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); + w.Write(" static extern void ConvertToUnmanaged_"); + w.Write(retParamName); + w.Write("([UnsafeAccessorType(\""); + w.Write(marshallerPath); + w.Write("\")] object _, ReadOnlySpan<"); + w.Write(elementProjected); + w.Write("> span, out uint length, out "); + w.Write(elementAbi); + w.Write("* data);\n\n"); + } + + // Mirror C++ ordering: declare the return local first with default value, then zero + // the OUT pointer(s). The actual assignment happens inside the try block. + if (rt is not null) + { + if (returnIsString) + { + w.Write(" string "); + w.Write(retLocalName); + w.Write(" = default;\n"); + } + else if (returnIsRefType) + { + string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt, false))); + w.Write(" "); + w.Write(projected); + w.Write(" "); + w.Write(retLocalName); + w.Write(" = default;\n"); + } + else if (returnIsReceiveArrayDoAbi) + { + string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt, false))); + w.Write(" "); + w.Write(projected); + w.Write(" "); + w.Write(retLocalName); + w.Write(" = default;\n"); + } + else + { + string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt, false))); + w.Write(" "); + w.Write(projected); + w.Write(" "); + w.Write(retLocalName); + w.Write(" = default;\n"); + } + } + + if (rt is not null) + { + if (returnIsReceiveArrayDoAbi) + { + w.Write(" *"); + w.Write(retParamName); + w.Write(" = default;\n"); + w.Write(" *"); + w.Write(retSizeParamName); + w.Write(" = default;\n"); + } + else + { + w.Write(" *"); + w.Write(retParamName); + w.Write(" = default;\n"); + } + } + // For each out parameter, clear the destination and declare a local. + // NOTE: Ref params (WinRT 'in T' / 'ref const T') are READ-ONLY inputs from the caller's + // perspective. Do NOT zero * (it's the input value) and do NOT declare a local + // (we read directly via *). + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.Out) { continue; } + string raw = p.Parameter.Name ?? "param"; + string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write(" *"); + w.Write(ptr); + w.Write(" = default;\n"); + } + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.Out) { continue; } + string raw = p.Parameter.Name ?? "param"; + // Use the projected (non-ABI) type for the local variable. + // Strip ByRef and CustomModifier wrappers to get the underlying base type. + AsmResolver.DotNet.Signatures.TypeSignature underlying = StripByRefAndCustomModifiers(p.Type); + string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, underlying, false))); + w.Write(" "); + w.Write(projected); + w.Write(" __"); + w.Write(raw); + w.Write(" = default;\n"); + } + // For each ReceiveArray parameter (out T[]), zero the destination + size out pointers + // and declare a managed array local. The managed call passes 'out __' and after + // the call we copy to the ABI buffer via UnsafeAccessor. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.ReceiveArray) { continue; } + string raw = p.Parameter.Name ?? "param"; + string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; + AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(sza.BaseType)))); + w.Write(" *"); + w.Write(ptr); + w.Write(" = default;\n"); + w.Write(" *__"); + w.Write(raw); + w.Write("Size = default;\n"); + w.Write(" "); + w.Write(elementProjected); + w.Write("[] __"); + w.Write(raw); + w.Write(" = default;\n"); + } + // For each blittable array (PassArray / FillArray) parameter, declare a Span local that + // wraps the (length, pointer) pair from the ABI signature. + // For non-blittable element types (string/runtime class/object), declare InlineArray16 + + // ArrayPool fallback then CopyToManaged via UnsafeAccessor. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature sz) { continue; } + string raw = p.Parameter.Name ?? "param"; + string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(sz.BaseType)))); + bool isBlittableElem = IsBlittablePrimitive(sz.BaseType) || IsAnyStruct(sz.BaseType); + if (isBlittableElem) + { + w.Write(" "); + w.Write(cat == ParamCategory.PassArray ? "ReadOnlySpan<" : "Span<"); + w.Write(elementProjected); + w.Write("> __"); + w.Write(raw); + w.Write(" = new("); + w.Write(ptr); + w.Write(", (int)__"); + w.Write(raw); + w.Write("Size);\n"); + } + else + { + // Non-blittable element: InlineArray16 + ArrayPool with size from ABI. + w.Write("\n Unsafe.SkipInit(out InlineArray16<"); + w.Write(elementProjected); + w.Write("> __"); + w.Write(raw); + w.Write("_inlineArray);\n"); + w.Write(" "); + w.Write(elementProjected); + w.Write("[] __"); + w.Write(raw); + w.Write("_arrayFromPool = null;\n"); + w.Write(" Span<"); + w.Write(elementProjected); + w.Write("> __"); + w.Write(raw); + w.Write(" = __"); + w.Write(raw); + w.Write("Size <= 16\n ? __"); + w.Write(raw); + w.Write("_inlineArray[..(int)__"); + w.Write(raw); + w.Write("Size]\n : (__"); + w.Write(raw); + w.Write("_arrayFromPool = global::System.Buffers.ArrayPool<"); + w.Write(elementProjected); + w.Write(">.Shared.Rent((int)__"); + w.Write(raw); + w.Write("Size));\n"); + } + } + w.Write(" try\n {\n"); + + // For non-blittable PassArray params (read-only input arrays), emit CopyToManaged_ + // via UnsafeAccessor to convert the native ABI buffer into the managed Span the + // delegate sees. For FillArray params, the buffer is fresh storage the user delegate + // fills — the post-call writeback loop handles that. (Mirrors C++ which only emits the + // pre-call CopyToManaged for PassArray, see code_writers.h:7772 write_copy_to_managed.) + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szArr.BaseType)))); + string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + // For complex structs, the data param is the ABI struct pointer (e.g. BasicStruct*). + // The Do_Abi parameter we receive is void* (per V3R3-M8), so the call-site needs an + // explicit (T*) cast to bridge the type. For ref-types (string/runtime-class/object), + // the data param is void** and the cast is (void**). + string dataParamType; + string dataCastExpr; + if (IsComplexStruct(szArr.BaseType)) + { + string abiStructName = GetAbiStructTypeName(w, szArr.BaseType); + dataParamType = abiStructName + "* data"; + dataCastExpr = "(" + abiStructName + "*)" + ptr; + } + else + { + dataParamType = "void** data"; + dataCastExpr = "(void**)" + ptr; + } + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToManaged\")]\n"); + w.Write(" static extern void CopyToManaged_"); + w.Write(raw); + w.Write("([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); + w.Write("\")] object _, uint length, "); + w.Write(dataParamType); + w.Write(", Span<"); + w.Write(elementProjected); + w.Write("> span);\n"); + w.Write(" CopyToManaged_"); + w.Write(raw); + w.Write("(null, __"); + w.Write(raw); + w.Write("Size, "); + w.Write(dataCastExpr); + w.Write(", __"); + w.Write(raw); + w.Write(");\n"); + } + + // For generic instance ABI input parameters, emit local UnsafeAccessor delegates and locals + // first so the call site can reference them. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + if (IsNullableT(p.Type)) + { + // Nullable param (server-side): use Marshaller.UnboxToManaged. Mirrors truth pattern. + string rawName = p.Parameter.Name ?? "param"; + string callName = Helpers.IsKeyword(rawName) ? "@" + rawName : rawName; + AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(p.Type)!; + string innerMarshaller = GetNullableInnerMarshallerName(w, inner); + w.Write(" var __arg_"); + w.Write(rawName); + w.Write(" = "); + w.Write(innerMarshaller); + w.Write(".UnboxToManaged("); + w.Write(callName); + w.Write(");\n"); + } + else if (IsGenericInstance(p.Type)) + { + string rawName = p.Parameter.Name ?? "param"; + string callName = Helpers.IsKeyword(rawName) ? "@" + rawName : rawName; + string interopTypeName = EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, p.Type, false))); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); + w.Write(" static extern "); + w.Write(projectedTypeName); + w.Write(" ConvertToManaged_arg_"); + w.Write(rawName); + w.Write("([UnsafeAccessorType(\""); + w.Write(interopTypeName); + w.Write("\")] object _, void* value);\n"); + w.Write(" var __arg_"); + w.Write(rawName); + w.Write(" = ConvertToManaged_arg_"); + w.Write(rawName); + w.Write("(null, "); + w.Write(callName); + w.Write(");\n"); + } + } + + if (returnIsString) + { + w.Write(" "); + w.Write(retLocalName); + w.Write(" = "); + } + else if (returnIsRefType) + { + w.Write(" "); + w.Write(retLocalName); + w.Write(" = "); + } + else if (returnIsReceiveArrayDoAbi) + { + // For T[] return: assign to existing local. + w.Write(" "); + w.Write(retLocalName); + w.Write(" = "); + } + else if (rt is not null) + { + w.Write(" "); + w.Write(retLocalName); + w.Write(" = "); + } + else + { + w.Write(" "); + } + + if (isGetter) + { + string propName = methodName.Substring(4); + w.Write("ComInterfaceDispatch.GetInstance<"); + w.Write(ifaceFullName); + w.Write(">((ComInterfaceDispatch*)thisPtr)."); + w.Write(propName); + w.Write(";\n"); + } + else if (isSetter) + { + string propName = methodName.Substring(4); + w.Write("ComInterfaceDispatch.GetInstance<"); + w.Write(ifaceFullName); + w.Write(">((ComInterfaceDispatch*)thisPtr)."); + w.Write(propName); + w.Write(" = "); + EmitDoAbiParamArgConversion(w, sig.Params[0]); + w.Write(";\n"); + } + else + { + w.Write("ComInterfaceDispatch.GetInstance<"); + w.Write(ifaceFullName); + w.Write(">((ComInterfaceDispatch*)thisPtr)."); + w.Write(methodName); + w.Write("("); + for (int i = 0; i < sig.Params.Count; i++) + { + if (i > 0) { w.Write(",\n "); } + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat == ParamCategory.Out) + { + string raw = p.Parameter.Name ?? "param"; + w.Write("out __"); + w.Write(raw); + } + else if (cat == ParamCategory.Ref) + { + // WinRT 'in T' / 'ref const T' is a read-only by-ref input on the ABI side + // (pointer to a value the native caller owns). On the C# delegate / interface + // side it's projected as 'in T'. Read directly from * via the appropriate + // marshaller — DO NOT zero or write back. + string raw = p.Parameter.Name ?? "param"; + string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; + AsmResolver.DotNet.Signatures.TypeSignature uRef = StripByRefAndCustomModifiers(p.Type); + if (IsString(uRef)) + { + w.Write("HStringMarshaller.ConvertToManaged(*"); + w.Write(ptr); + w.Write(")"); + } + else if (IsObject(uRef)) + { + w.Write("WindowsRuntimeObjectMarshaller.ConvertToManaged(*"); + w.Write(ptr); + w.Write(")"); + } + else if (IsRuntimeClassOrInterface(uRef)) + { + w.Write(GetMarshallerFullName(w, uRef)); + w.Write(".ConvertToManaged(*"); + w.Write(ptr); + w.Write(")"); + } + else if (IsMappedAbiValueType(uRef)) + { + w.Write(GetMappedMarshallerName(uRef)); + w.Write(".ConvertToManaged(*"); + w.Write(ptr); + w.Write(")"); + } + else if (IsHResultException(uRef)) + { + w.Write("global::ABI.System.ExceptionMarshaller.ConvertToManaged(*"); + w.Write(ptr); + w.Write(")"); + } + else if (IsComplexStruct(uRef)) + { + w.Write(GetMarshallerFullName(w, uRef)); + w.Write(".ConvertToManaged(*"); + w.Write(ptr); + w.Write(")"); + } + else if (IsAnyStruct(uRef) || IsBlittablePrimitive(uRef) || IsEnumType(uRef)) + { + // Blittable/almost-blittable: ABI layout matches projected layout. + w.Write("*"); + w.Write(ptr); + } + else + { + w.Write("*"); + w.Write(ptr); + } + } + else if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) + { + string raw = p.Parameter.Name ?? "param"; + w.Write("__"); + w.Write(raw); + } + else if (cat == ParamCategory.ReceiveArray) + { + string raw = p.Parameter.Name ?? "param"; + w.Write("out __"); + w.Write(raw); + } + else + { + EmitDoAbiParamArgConversion(w, p); + } + } + w.Write(");\n"); + } + // After call: write back out params to caller's pointer. + // NOTE: Ref params (WinRT 'in T') are read-only inputs — never written back. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.Out) { continue; } + string raw = p.Parameter.Name ?? "param"; + string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; + AsmResolver.DotNet.Signatures.TypeSignature underlying = StripByRefAndCustomModifiers(p.Type); + w.Write(" *"); + w.Write(ptr); + w.Write(" = "); + // String: HStringMarshaller.ConvertToUnmanaged + if (IsString(underlying)) + { + w.Write("HStringMarshaller.ConvertToUnmanaged(__"); + w.Write(raw); + w.Write(")"); + } + // Object/runtime class: .ConvertToUnmanaged(...).DetachThisPtrUnsafe() + else if (IsObject(underlying)) + { + w.Write("WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(__"); + w.Write(raw); + w.Write(").DetachThisPtrUnsafe()"); + } + else if (IsRuntimeClassOrInterface(underlying)) + { + w.Write(GetMarshallerFullName(w, underlying)); + w.Write(".ConvertToUnmanaged(__"); + w.Write(raw); + w.Write(").DetachThisPtrUnsafe()"); + } + // Generic instance (e.g. IEnumerable): use the hoisted UnsafeAccessor + // 'ConvertToUnmanaged_' declared at the top of the method body. + else if (IsGenericInstance(underlying)) + { + w.Write("ConvertToUnmanaged_"); + w.Write(raw); + w.Write("(null, __"); + w.Write(raw); + w.Write(").DetachThisPtrUnsafe()"); + } + // For enums, function pointer signature uses the projected enum type, no cast needed. + // For bool, cast to byte. For char, cast to ushort. + else if (IsEnumType(underlying)) + { + w.Write("__"); + w.Write(raw); + } + else if (underlying is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibBool && + corlibBool.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) + { + w.Write("__"); + w.Write(raw); + } + else if (underlying is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibChar && + corlibChar.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) + { + w.Write("__"); + w.Write(raw); + } + // Non-blittable struct (e.g. authored BasicStruct with string fields): marshal + // the local managed value through Marshaller.ConvertToUnmanaged before + // writing it into the *out ABI struct slot. Mirrors C++ marshaler.write_marshal_from_managed + // (code_writers.h:7901-7910): "Marshaller.ConvertToUnmanaged(local)". + else if (IsComplexStruct(underlying)) + { + w.Write(GetMarshallerFullName(w, underlying)); + w.Write(".ConvertToUnmanaged(__"); + w.Write(raw); + w.Write(")"); + } + else + { + w.Write("__"); + w.Write(raw); + } + w.Write(";\n"); + } + // After call: for ReceiveArray params, emit ConvertToUnmanaged_ call (the + // [UnsafeAccessor] declaration was hoisted to the top of the method body). + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.ReceiveArray) { continue; } + string raw = p.Parameter.Name ?? "param"; + string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write(" ConvertToUnmanaged_"); + w.Write(raw); + w.Write("(null, __"); + w.Write(raw); + w.Write(", out *__"); + w.Write(raw); + w.Write("Size, out *"); + w.Write(ptr); + w.Write(");\n"); + } + // After call: for non-blittable FillArray params (Span where T is string/runtime + // class/object/non-blittable struct), copy the managed delegate's writes back into the + // native ABI buffer. Mirrors C++ write_marshal_from_managed (code_writers.h:7852-7869) + // which emits 'CopyToUnmanaged_(null, __, __Size, (T*))'. + // Blittable element types don't need this — the Span wraps the native buffer directly. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szFA) { continue; } + // Blittable element types: Span wraps the native buffer; no copy-back needed. + if (IsBlittablePrimitive(szFA.BaseType) || IsAnyStruct(szFA.BaseType)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szFA.BaseType)))); + string elementInteropArg = EncodeInteropTypeName(szFA.BaseType, TypedefNameType.Projected); + // Determine the ABI element type for the data pointer cast. + // - Strings / runtime classes / objects: void** + // - HResult exception: global::ABI.System.Exception* + // - Mapped value types (DateTime/TimeSpan): global::ABI.System.{DateTimeOffset/TimeSpan}* + // - Complex structs: * + string dataParamType; + string dataCastType; + if (IsString(szFA.BaseType) || IsRuntimeClassOrInterface(szFA.BaseType) || IsObject(szFA.BaseType)) + { + dataParamType = "void** data"; + dataCastType = "(void**)"; + } + else if (IsHResultException(szFA.BaseType)) + { + dataParamType = "global::ABI.System.Exception* data"; + dataCastType = "(global::ABI.System.Exception*)"; + } + else if (IsMappedAbiValueType(szFA.BaseType)) + { + string abiName = GetMappedAbiTypeName(szFA.BaseType); + dataParamType = abiName + "* data"; + dataCastType = "(" + abiName + "*)"; + } + else + { + string abiStructName = GetAbiStructTypeName(w, szFA.BaseType); + dataParamType = abiStructName + "* data"; + dataCastType = "(" + abiStructName + "*)"; + } + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToUnmanaged\")]\n"); + w.Write(" static extern void CopyToUnmanaged_"); + w.Write(raw); + w.Write("([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, szFA.BaseType, elementInteropArg)); + w.Write("\")] object _, ReadOnlySpan<"); + w.Write(elementProjected); + w.Write("> span, uint length, "); + w.Write(dataParamType); + w.Write(");\n"); + w.Write(" CopyToUnmanaged_"); + w.Write(raw); + w.Write("(null, __"); + w.Write(raw); + w.Write(", __"); + w.Write(raw); + w.Write("Size, "); + w.Write(dataCastType); + w.Write(ptr); + w.Write(");\n"); + } + if (rt is not null) + { + if (returnIsHResultExceptionDoAbi) + { + w.Write(" *"); + w.Write(retParamName); + w.Write(" = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged("); + w.Write(retLocalName); + w.Write(");\n"); + } + else if (returnIsString) + { + w.Write(" *"); + w.Write(retParamName); + w.Write(" = HStringMarshaller.ConvertToUnmanaged("); + w.Write(retLocalName); + w.Write(");\n"); + } + else if (returnIsRefType) + { + if (rt is not null && IsNullableT(rt)) + { + // Nullable return (server-side): use Marshaller.BoxToUnmanaged. + AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(rt)!; + string innerMarshaller = GetNullableInnerMarshallerName(w, inner); + w.Write(" *"); + w.Write(retParamName); + w.Write(" = "); + w.Write(innerMarshaller); + w.Write(".BoxToUnmanaged("); + w.Write(retLocalName); + w.Write(").DetachThisPtrUnsafe();\n"); + } + else if (returnIsGenericInstance) + { + // Generic instance return: use the UnsafeAccessor static local function declared at + // the top of the method body via the M12 hoisting pass; just emit the call here. + w.Write(" *"); + w.Write(retParamName); + w.Write(" = ConvertToUnmanaged_"); + w.Write(retParamName); + w.Write("(null, "); + w.Write(retLocalName); + w.Write(").DetachThisPtrUnsafe();\n"); + } + else + { + w.Write(" *"); + w.Write(retParamName); + w.Write(" = "); + EmitMarshallerConvertToUnmanaged(w, rt!, retLocalName); + w.Write(".DetachThisPtrUnsafe();\n"); + } + } + else if (returnIsReceiveArrayDoAbi) + { + // Return-receive-array: emit ConvertToUnmanaged_ call (declaration + // was hoisted to the top of the method body). + w.Write(" ConvertToUnmanaged_"); + w.Write(retParamName); + w.Write("(null, "); + w.Write(retLocalName); + w.Write(", out *"); + w.Write(retSizeParamName); + w.Write(", out *"); + w.Write(retParamName); + w.Write(");\n"); + } + else if (IsMappedAbiValueType(rt)) + { + // Mapped value type return (DateTime/TimeSpan): convert via marshaller. + w.Write(" *"); + w.Write(retParamName); + w.Write(" = "); + w.Write(GetMappedMarshallerName(rt)); + w.Write(".ConvertToUnmanaged("); + w.Write(retLocalName); + w.Write(");\n"); + } + else if (IsSystemType(rt)) + { + // System.Type return (server-side): convert managed System.Type to ABI Type struct. + w.Write(" *"); + w.Write(retParamName); + w.Write(" = global::ABI.System.TypeMarshaller.ConvertToUnmanaged("); + w.Write(retLocalName); + w.Write(");\n"); + } + else if (IsComplexStruct(rt)) + { + // Complex struct return (server-side): convert managed struct to ABI struct via marshaller. + w.Write(" *"); + w.Write(retParamName); + w.Write(" = "); + w.Write(GetMarshallerFullName(w, rt)); + w.Write(".ConvertToUnmanaged("); + w.Write(retLocalName); + w.Write(");\n"); + } + else if (returnIsBlittableStruct) + { + w.Write(" *"); + w.Write(retParamName); + w.Write(" = "); + w.Write(retLocalName); + w.Write(";\n"); + } + else + { + string abiType = GetAbiPrimitiveType(rt); + w.Write(" *"); + w.Write(retParamName); + w.Write(" = "); + if (rt is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && + corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) + { + w.Write(retLocalName); + w.Write(";\n"); + } + else if (rt is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib2 && + corlib2.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) + { + w.Write(retLocalName); + w.Write(";\n"); + } + else if (IsEnumType(rt)) + { + // Enum: function pointer signature uses the projected enum type, no cast needed. + w.Write(retLocalName); + w.Write(";\n"); + } + else + { + w.Write(retLocalName); + w.Write(";\n"); + } + } + } + w.Write(" return 0;\n }\n"); + w.Write(" catch (Exception __exception__)\n {\n"); + w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__);\n }\n"); + + // For non-blittable PassArray params, emit finally block with ArrayPool.Shared.Return. + bool hasNonBlittableArrayDoAbi = false; + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + hasNonBlittableArrayDoAbi = true; + break; + } + if (hasNonBlittableArrayDoAbi) + { + w.Write(" finally\n {\n"); + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szArr.BaseType)))); + w.Write("\n if (__"); + w.Write(raw); + w.Write("_arrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool<"); + w.Write(elementProjected); + w.Write(">.Shared.Return(__"); + w.Write(raw); + w.Write("_arrayFromPool);\n }\n"); + } + w.Write(" }\n"); + } + + w.Write("}\n\n"); + _ = hasStringParams; + } + + /// Converts an ABI parameter to its projected (managed) form for the Do_Abi call. + private static void EmitDoAbiParamArgConversion(TypeWriter w, ParamInfo p) + { + string rawName = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(rawName) ? "@" + rawName : rawName; + if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && + corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) + { + w.Write(pname); + } + else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib2 && + corlib2.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) + { + w.Write(pname); + } + else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibStr && + corlibStr.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String) + { + w.Write("HStringMarshaller.ConvertToManaged("); + w.Write(pname); + w.Write(")"); + } + else if (IsGenericInstance(p.Type)) + { + // Generic instance ABI parameter: caller already declared a local UnsafeAccessor + + // local var __arg_ that holds the converted value. + w.Write("__arg_"); + w.Write(rawName); + } + else if (IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type)) + { + EmitMarshallerConvertToManaged(w, p.Type, pname); + } + else if (IsMappedAbiValueType(p.Type)) + { + // Mapped value type input (DateTime/TimeSpan): the parameter is the ABI type; + // convert to the projected managed type via the marshaller. + w.Write(GetMappedMarshallerName(p.Type)); + w.Write(".ConvertToManaged("); + w.Write(pname); + w.Write(")"); + } + else if (IsSystemType(p.Type)) + { + // System.Type input (server-side): convert ABI Type struct to System.Type. + w.Write("global::ABI.System.TypeMarshaller.ConvertToManaged("); + w.Write(pname); + w.Write(")"); + } + else if (IsComplexStruct(p.Type)) + { + // Complex struct input (server-side): convert ABI struct to managed via marshaller. + w.Write(GetMarshallerFullName(w, p.Type)); + w.Write(".ConvertToManaged("); + w.Write(pname); + w.Write(")"); + } + else if (IsAnyStruct(p.Type)) + { + // Blittable / almost-blittable struct: pass directly (projected type == ABI type). + w.Write(pname); + } + else if (IsEnumType(p.Type)) + { + // Enum: param signature is already the projected enum type, no cast needed. + w.Write(pname); + } + else + { + w.Write(pname); + } + } + + /// Mirrors C++ write_interface_idic_impl. + public static void WriteInterfaceIdicImpl(TypeWriter w, TypeDefinition type) + { + if (TypeCategorization.IsExclusiveTo(type) && !w.Settings.IdicExclusiveTo) { return; } + if (type.GenericParameters.Count > 0) { return; } + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + + w.Write("\n[DynamicInterfaceCastableImplementation]\n"); + WriteGuidAttribute(w, type); + w.Write("\n"); + w.Write("file interface "); + w.Write(nameStripped); + w.Write(" : "); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + w.Write("\n{\n"); + // Emit DIM bodies that dispatch through the static ABI Methods class. + WriteInterfaceIdicImplMembers(w, type); + w.Write("\n}\n"); + } + + /// + /// Emits explicit-interface DIM (default interface method) implementations for the IDIC + /// file interface. Mirrors C++ write_interface_members. + /// + private static void WriteInterfaceIdicImplMembers(TypeWriter w, TypeDefinition type) + { + HashSet visited = new(); + WriteInterfaceIdicImplMembersForInterface(w, type, visited); + + // Also walk required (inherited) interfaces and emit members for each one. + // Mirrors C++ write_required_interface_members_for_abi_type. + WriteInterfaceIdicImplMembersForRequiredInterfaces(w, type, visited); + } + + private static void WriteInterfaceIdicImplMembersForRequiredInterfaces( + TypeWriter w, TypeDefinition type, HashSet visited) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) { continue; } + TypeDefinition? required = ResolveInterfaceTypeDef(impl.Interface); + if (required is null) { continue; } + if (!visited.Add(required)) { continue; } + string rNs = required.Namespace?.Value ?? string.Empty; + string rName = required.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(rNs, rName); + if (mapped is not null && mapped.HasCustomMembersOutput) + { + // Mapped to a BCL interface (IBindableVector -> IList, IBindableIterable -> IEnumerable, etc.). + // Emit explicit-interface DIM forwarders for the BCL members so the DIC shim + // satisfies them when queried via casts like '((IList)(WindowsRuntimeObject)this)'. + EmitDicShimMappedBclForwarders(w, rName); + // IBindableVector's IList forwarders already include the IEnumerable.GetEnumerator + // forwarder (since IList : IEnumerable). Pre-add IBindableIterable to the visited + // set so we don't emit a second GetEnumerator forwarder for it. We also walk the + // required interfaces so any other (deeper) inherited mapped interface is covered. + if (rName == "IBindableVector") + { + foreach (InterfaceImplementation impl2 in required.Interfaces) + { + if (impl2.Interface is null) { continue; } + TypeDefinition? r2 = ResolveInterfaceTypeDef(impl2.Interface); + if (r2 is not null) { visited.Add(r2); } + } + } + continue; + } + // Special case: IObservableMap`2 and IObservableVector`1 are NOT mapped to BCL + // interfaces (they retain WinRT names) but they DO need to forward their inherited + // IDictionary/IList members for cast-based dispatch. Mirrors C++ which uses + // write_dictionary_members_using_idic / write_list_members_using_idic when walking + // these interfaces in write_required_interface_members_for_abi_type. + if (rNs == "Windows.Foundation.Collections" && rName == "IObservableMap`2") + { + if (impl.Interface is TypeSpecification tsMap && tsMap.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature giMap && giMap.TypeArguments.Count == 2) + { + string keyText = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, TypeSemanticsFactory.Get(giMap.TypeArguments[0]), TypedefNameType.Projected, true))); + string valueText = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, TypeSemanticsFactory.Get(giMap.TypeArguments[1]), TypedefNameType.Projected, true))); + EmitDicShimIObservableMapForwarders(w, keyText, valueText); + // Mark the inherited IMap`2 / IIterable`1 as visited so they aren't re-emitted. + foreach (InterfaceImplementation impl2 in required.Interfaces) + { + if (impl2.Interface is null) { continue; } + TypeDefinition? r2 = ResolveInterfaceTypeDef(impl2.Interface); + if (r2 is not null) { visited.Add(r2); } + } + } + continue; + } + if (rNs == "Windows.Foundation.Collections" && rName == "IObservableVector`1") + { + if (impl.Interface is TypeSpecification tsVec && tsVec.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature giVec && giVec.TypeArguments.Count == 1) + { + string elementText = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, TypeSemanticsFactory.Get(giVec.TypeArguments[0]), TypedefNameType.Projected, true))); + EmitDicShimIObservableVectorForwarders(w, elementText); + foreach (InterfaceImplementation impl2 in required.Interfaces) + { + if (impl2.Interface is null) { continue; } + TypeDefinition? r2 = ResolveInterfaceTypeDef(impl2.Interface); + if (r2 is not null) { visited.Add(r2); } + } + } + continue; + } + // Skip generic interfaces with unbound params (we can't substitute T at this layer). + if (required.GenericParameters.Count > 0) { continue; } + // Recurse first so deepest-base is emitted before nearer-base (matches deduplication). + WriteInterfaceIdicImplMembersForRequiredInterfaces(w, required, visited); + WriteInterfaceIdicImplMembersForInheritedInterface(w, required); + } + } + + /// + /// Emits IDictionary<K,V> / ICollection<KVP> / IEnumerable<KVP> + + /// IObservableMap<K,V>.MapChanged forwarders for a DIC file interface that inherits + /// from Windows.Foundation.Collections.IObservableMap<K,V>. Mirrors C++ + /// write_dictionary_members_using_idic(true) + the IObservableMap event forwarder. + /// + private static void EmitDicShimIObservableMapForwarders(TypeWriter w, string keyText, string valueText) + { + string target = $"((global::System.Collections.Generic.IDictionary<{keyText}, {valueText}>)(WindowsRuntimeObject)this)"; + string self = $"global::System.Collections.Generic.IDictionary<{keyText}, {valueText}>."; + string icoll = $"global::System.Collections.Generic.ICollection>."; + w.Write("\n"); + w.Write($"ICollection<{keyText}> {self}Keys => {target}.Keys;\n"); + w.Write($"ICollection<{valueText}> {self}Values => {target}.Values;\n"); + w.Write($"int {icoll}Count => {target}.Count;\n"); + w.Write($"bool {icoll}IsReadOnly => {target}.IsReadOnly;\n"); + w.Write($"{valueText} {self}this[{keyText} key] \n"); + w.Write("{\n"); + w.Write($"get => {target}[key];\n"); + w.Write($"set => {target}[key] = value;\n"); + w.Write("}\n"); + w.Write($"void {self}Add({keyText} key, {valueText} value) => {target}.Add(key, value);\n"); + w.Write($"bool {self}ContainsKey({keyText} key) => {target}.ContainsKey(key);\n"); + w.Write($"bool {self}Remove({keyText} key) => {target}.Remove(key);\n"); + w.Write($"bool {self}TryGetValue({keyText} key, out {valueText} value) => {target}.TryGetValue(key, out value);\n"); + w.Write($"void {icoll}Add(KeyValuePair<{keyText}, {valueText}> item) => {target}.Add(item);\n"); + w.Write($"void {icoll}Clear() => {target}.Clear();\n"); + w.Write($"bool {icoll}Contains(KeyValuePair<{keyText}, {valueText}> item) => {target}.Contains(item);\n"); + w.Write($"void {icoll}CopyTo(KeyValuePair<{keyText}, {valueText}>[] array, int arrayIndex) => {target}.CopyTo(array, arrayIndex);\n"); + w.Write($"bool ICollection>.Remove(KeyValuePair<{keyText}, {valueText}> item) => {target}.Remove(item);\n"); + // Enumerable forwarders. + w.Write("\n"); + w.Write($"IEnumerator> IEnumerable>.GetEnumerator() => {target}.GetEnumerator();\n"); + w.Write("IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n"); + // IObservableMap.MapChanged event forwarder. + string obsTarget = $"((global::Windows.Foundation.Collections.IObservableMap<{keyText}, {valueText}>)(WindowsRuntimeObject)this)"; + string obsSelf = $"global::Windows.Foundation.Collections.IObservableMap<{keyText}, {valueText}>."; + w.Write("\n"); + w.Write($"event global::Windows.Foundation.Collections.MapChangedEventHandler<{keyText}, {valueText}> {obsSelf}MapChanged\n"); + w.Write("{\n"); + w.Write($"add => {obsTarget}.MapChanged += value;\n"); + w.Write($"remove => {obsTarget}.MapChanged -= value;\n"); + w.Write("}\n"); + } + + /// + /// Emits IList<T> / ICollection<T> / IEnumerable<T> + + /// IObservableVector<T>.VectorChanged forwarders for a DIC file interface that inherits + /// from Windows.Foundation.Collections.IObservableVector<T>. Mirrors C++ + /// write_list_members_using_idic(true) + the IObservableVector event forwarder. + /// + private static void EmitDicShimIObservableVectorForwarders(TypeWriter w, string elementText) + { + string target = $"((global::System.Collections.Generic.IList<{elementText}>)(WindowsRuntimeObject)this)"; + string self = $"global::System.Collections.Generic.IList<{elementText}>."; + string icoll = $"global::System.Collections.Generic.ICollection<{elementText}>."; + w.Write("\n"); + w.Write($"int {icoll}Count => {target}.Count;\n"); + w.Write($"bool {icoll}IsReadOnly => {target}.IsReadOnly;\n"); + w.Write($"{elementText} {self}this[int index]\n"); + w.Write("{\n"); + w.Write($"get => {target}[index];\n"); + w.Write($"set => {target}[index] = value;\n"); + w.Write("}\n"); + w.Write($"int {self}IndexOf({elementText} item) => {target}.IndexOf(item);\n"); + w.Write($"void {self}Insert(int index, {elementText} item) => {target}.Insert(index, item);\n"); + w.Write($"void {self}RemoveAt(int index) => {target}.RemoveAt(index);\n"); + w.Write($"void {icoll}Add({elementText} item) => {target}.Add(item);\n"); + w.Write($"void {icoll}Clear() => {target}.Clear();\n"); + w.Write($"bool {icoll}Contains({elementText} item) => {target}.Contains(item);\n"); + w.Write($"void {icoll}CopyTo({elementText}[] array, int arrayIndex) => {target}.CopyTo(array, arrayIndex);\n"); + w.Write($"bool {icoll}Remove({elementText} item) => {target}.Remove(item);\n"); + w.Write("\n"); + w.Write($"IEnumerator<{elementText}> IEnumerable<{elementText}>.GetEnumerator() => {target}.GetEnumerator();\n"); + w.Write("IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n"); + // IObservableVector.VectorChanged event forwarder. + string obsTarget = $"((global::Windows.Foundation.Collections.IObservableVector<{elementText}>)(WindowsRuntimeObject)this)"; + string obsSelf = $"global::Windows.Foundation.Collections.IObservableVector<{elementText}>."; + w.Write("\n"); + w.Write($"event global::Windows.Foundation.Collections.VectorChangedEventHandler<{elementText}> {obsSelf}VectorChanged\n"); + w.Write("{\n"); + w.Write($"add => {obsTarget}.VectorChanged += value;\n"); + w.Write($"remove => {obsTarget}.VectorChanged -= value;\n"); + w.Write("}\n"); + } + + /// + /// Emits explicit-interface DIM thunks for an *inherited* (required) interface on a DIC + /// file interface shim. Each member becomes a thin + /// => ((IParent)(WindowsRuntimeObject)this).Member delegating thunk so that DIC + /// re-dispatches through the parent's own DIC shim. Mirrors the C++ tool's emission for + /// inherited-interface members in DIC shims. + /// + private static void WriteInterfaceIdicImplMembersForInheritedInterface(TypeWriter w, TypeDefinition type) + { + // The CCW interface name (the projected interface name with global:: prefix). For the + // delegating thunks we cast through this same projected interface type. + string ccwIfaceName = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); + if (!ccwIfaceName.StartsWith("global::", System.StringComparison.Ordinal)) { ccwIfaceName = "global::" + ccwIfaceName; } + + foreach (MethodDefinition method in type.Methods) + { + if (Helpers.IsSpecial(method)) { continue; } + MethodSig sig = new(method); + string mname = method.Name?.Value ?? string.Empty; + + w.Write("\n"); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(ccwIfaceName); + w.Write("."); + w.Write(mname); + w.Write("("); + WriteParameterList(w, sig); + w.Write(") => (("); + w.Write(ccwIfaceName); + w.Write(")(WindowsRuntimeObject)this)."); + w.Write(mname); + w.Write("("); + for (int i = 0; i < sig.Params.Count; i++) + { + if (i > 0) { w.Write(", "); } + WriteParameterNameWithModifier(w, sig.Params[i]); + } + w.Write(");\n"); + } + + foreach (PropertyDefinition prop in type.Properties) + { + (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); + string pname = prop.Name?.Value ?? string.Empty; + string propType = WritePropType(w, prop); + + w.Write("\n"); + w.Write(propType); + w.Write(" "); + w.Write(ccwIfaceName); + w.Write("."); + w.Write(pname); + if (getter is not null && setter is null) + { + // Read-only: single-line expression body. + w.Write(" => (("); + w.Write(ccwIfaceName); + w.Write(")(WindowsRuntimeObject)this)."); + w.Write(pname); + w.Write(";\n"); + } + else + { + w.Write("\n{\n"); + if (getter is not null) + { + w.Write(" get => (("); + w.Write(ccwIfaceName); + w.Write(")(WindowsRuntimeObject)this)."); + w.Write(pname); + w.Write(";\n"); + } + if (setter is not null) + { + w.Write(" set => (("); + w.Write(ccwIfaceName); + w.Write(")(WindowsRuntimeObject)this)."); + w.Write(pname); + w.Write(" = value;\n"); + } + w.Write("}\n"); + } + } + + foreach (EventDefinition evt in type.Events) + { + string evtName = evt.Name?.Value ?? string.Empty; + w.Write("\nevent "); + WriteEventType(w, evt); + w.Write(" "); + w.Write(ccwIfaceName); + w.Write("."); + w.Write(evtName); + w.Write("\n{\n"); + w.Write(" add => (("); + w.Write(ccwIfaceName); + w.Write(")(WindowsRuntimeObject)this)."); + w.Write(evtName); + w.Write(" += value;\n"); + w.Write(" remove => (("); + w.Write(ccwIfaceName); + w.Write(")(WindowsRuntimeObject)this)."); + w.Write(evtName); + w.Write(" -= value;\n"); + w.Write("}\n"); + } + } + + /// + /// Emits explicit-interface DIM forwarders on a DIC file interface shim for the BCL + /// members that come from a system-collection-mapped required WinRT interface + /// (e.g. IBindableVector maps to IList, so we must satisfy IList, + /// ICollection, and IEnumerable members on the shim). The forwarders all + /// re-cast through (WindowsRuntimeObject)this so the DIC machinery can re-dispatch + /// to the real BCL adapter shim. + /// + private static void EmitDicShimMappedBclForwarders(TypeWriter w, string mappedWinRTInterfaceName) + { + switch (mappedWinRTInterfaceName) + { + case "IClosable": + // IClosable maps to IDisposable. Forward Dispose() to the + // WindowsRuntimeObject base which has the actual implementation. + w.Write("\nvoid global::System.IDisposable.Dispose() => ((global::System.IDisposable)(WindowsRuntimeObject)this).Dispose();\n"); + break; + case "IBindableVector": + // IList covers IList, ICollection, and IEnumerable members. + w.Write("\n"); + w.Write("int global::System.Collections.ICollection.Count => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Count;\n"); + w.Write("bool global::System.Collections.ICollection.IsSynchronized => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsSynchronized;\n"); + w.Write("object global::System.Collections.ICollection.SyncRoot => ((global::System.Collections.IList)(WindowsRuntimeObject)this).SyncRoot;\n"); + w.Write("void global::System.Collections.ICollection.CopyTo(Array array, int index) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).CopyTo(array, index);\n\n"); + w.Write("object global::System.Collections.IList.this[int index]\n{\n"); + w.Write("get => ((global::System.Collections.IList)(WindowsRuntimeObject)this)[index];\n"); + w.Write("set => ((global::System.Collections.IList)(WindowsRuntimeObject)this)[index] = value;\n}\n"); + w.Write("bool global::System.Collections.IList.IsFixedSize => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsFixedSize;\n"); + w.Write("bool global::System.Collections.IList.IsReadOnly => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsReadOnly;\n"); + w.Write("int global::System.Collections.IList.Add(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Add(value);\n"); + w.Write("void global::System.Collections.IList.Clear() => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Clear();\n"); + w.Write("bool global::System.Collections.IList.Contains(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Contains(value);\n"); + w.Write("int global::System.Collections.IList.IndexOf(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IndexOf(value);\n"); + w.Write("void global::System.Collections.IList.Insert(int index, object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Insert(index, value);\n"); + w.Write("void global::System.Collections.IList.Remove(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Remove(value);\n"); + w.Write("void global::System.Collections.IList.RemoveAt(int index) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).RemoveAt(index);\n\n"); + w.Write("IEnumerator IEnumerable.GetEnumerator() => ((global::System.Collections.IList)(WindowsRuntimeObject)this).GetEnumerator();\n"); + break; + case "IBindableIterable": + w.Write("\n"); + w.Write("IEnumerator IEnumerable.GetEnumerator() => ((global::System.Collections.IEnumerable)(WindowsRuntimeObject)this).GetEnumerator();\n"); + break; + } + } + + private static void WriteInterfaceIdicImplMembersForInterface(TypeWriter w, TypeDefinition type, HashSet visited) + { + // The CCW interface name (the projected interface name with global:: prefix). + string ccwIfaceName = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); + if (!ccwIfaceName.StartsWith("global::", System.StringComparison.Ordinal)) { ccwIfaceName = "global::" + ccwIfaceName; } + // The static ABI Methods class name. + string abiClass = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.StaticAbiClass, true))); + if (!abiClass.StartsWith("global::", System.StringComparison.Ordinal)) { abiClass = "global::" + abiClass; } + + foreach (MethodDefinition method in type.Methods) + { + if (Helpers.IsSpecial(method)) { continue; } + MethodSig sig = new(method); + string mname = method.Name?.Value ?? string.Empty; + + w.Write("\nunsafe "); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(ccwIfaceName); + w.Write("."); + w.Write(mname); + w.Write("("); + WriteParameterList(w, sig); + w.Write(")\n{\n"); + w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); + w.Write(ccwIfaceName); + w.Write(").TypeHandle);\n "); + if (sig.ReturnType is not null) { w.Write("return "); } + w.Write(abiClass); + w.Write("."); + w.Write(mname); + w.Write("(_obj"); + for (int i = 0; i < sig.Params.Count; i++) + { + w.Write(", "); + WriteParameterNameWithModifier(w, sig.Params[i]); + } + w.Write(");\n}\n"); + } + + foreach (PropertyDefinition prop in type.Properties) + { + (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); + string pname = prop.Name?.Value ?? string.Empty; + string propType = WritePropType(w, prop); + + w.Write("\nunsafe "); + w.Write(propType); + w.Write(" "); + w.Write(ccwIfaceName); + w.Write("."); + w.Write(pname); + w.Write("\n{\n"); + if (getter is not null) + { + w.Write(" get\n {\n"); + w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); + w.Write(ccwIfaceName); + w.Write(").TypeHandle);\n"); + w.Write(" return "); + w.Write(abiClass); + w.Write("."); + w.Write(pname); + w.Write("(_obj);\n }\n"); + } + if (setter is not null) + { + // If the property has only a setter on this interface BUT a base interface declares + // the getter (so the C# interface decl emits 'get; set;'), C# requires an explicit + // interface impl to provide both accessors. Emit a synthetic getter that delegates + // to the base interface where the getter actually lives. Mirrors C++ + // code_writers.h:7052-7062. + if (getter is null) + { + TypeDefinition? baseIfaceWithGetter = FindPropertyInterfaceInBases(type, pname); + if (baseIfaceWithGetter is not null) + { + w.Write(" get { return (("); + WriteInterfaceTypeNameForCcw(w, baseIfaceWithGetter); + w.Write(")(WindowsRuntimeObject)this)."); + w.Write(pname); + w.Write("; }\n"); + } + } + w.Write(" set\n {\n"); + w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); + w.Write(ccwIfaceName); + w.Write(").TypeHandle);\n"); + w.Write(" "); + w.Write(abiClass); + w.Write("."); + w.Write(pname); + w.Write("(_obj, value);\n }\n"); + } + w.Write("}\n"); + } + + // Events: emit explicit interface event implementations on the IDIC interface that + // dispatch through the static ABI Methods class's event accessor (returns an EventSource). + // Mirrors C++ write_interface_members event handling (calls EventName(thisRef, _obj).Subscribe/Unsubscribe). + foreach (EventDefinition evt in type.Events) + { + string evtName = evt.Name?.Value ?? string.Empty; + w.Write("\nevent "); + WriteEventType(w, evt); + w.Write(" "); + w.Write(ccwIfaceName); + w.Write("."); + w.Write(evtName); + w.Write("\n{\n"); + // add accessor + w.Write(" add\n {\n"); + w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); + w.Write(ccwIfaceName); + w.Write(").TypeHandle);\n "); + w.Write(abiClass); + w.Write("."); + w.Write(evtName); + w.Write("((WindowsRuntimeObject)this, _obj).Subscribe(value);\n }\n"); + // remove accessor + w.Write(" remove\n {\n"); + w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); + w.Write(ccwIfaceName); + w.Write(").TypeHandle);\n "); + w.Write(abiClass); + w.Write("."); + w.Write(evtName); + w.Write("((WindowsRuntimeObject)this, _obj).Unsubscribe(value);\n }\n"); + w.Write("}\n"); + } + } + + /// Mirrors C++ write_interface_marshaller. + public static void WriteInterfaceMarshaller(TypeWriter w, TypeDefinition type) + { + if (TypeCategorization.IsExclusiveTo(type)) { return; } + if (type.GenericParameters.Count > 0) { return; } + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + + w.Write("\n#nullable enable\n"); + w.Write("public static unsafe class "); + w.Write(nameStripped); + w.Write("Marshaller\n{\n"); + w.Write(" public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + w.Write(" value)\n {\n"); + w.Write(" return WindowsRuntimeInterfaceMarshaller<"); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + w.Write(">.ConvertToUnmanaged(value, "); + WriteIidGuidReference(w, type); + w.Write(");\n }\n\n"); + w.Write(" public static "); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + w.Write("? ConvertToManaged(void* value)\n {\n"); + w.Write(" return ("); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + w.Write("?) WindowsRuntimeObjectMarshaller.ConvertToManaged(value);\n }\n}\n"); + w.Write("#nullable disable\n"); + } + + /// Mirrors C++ write_iid_guid for use by ABI helpers. + public static void WriteIidGuidReference(TypeWriter w, TypeDefinition type) + { + if (type.GenericParameters.Count != 0) + { + // Generic interface IID - call the unsafe accessor + WriteIidGuidPropertyName(w, type); + w.Write("(null)"); + return; + } + string ns = type.Namespace?.Value ?? string.Empty; + string nm = type.Name?.Value ?? string.Empty; + if (MappedTypes.Get(ns, nm) is { } m && m.MappedName == "IStringable") + { + w.Write("global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable"); + return; + } + w.Write("global::ABI.InterfaceIIDs."); + WriteIidGuidPropertyName(w, type); + } + + /// + /// Writes a marshaller class for a struct or enum (mirrors C++ write_struct_and_enum_marshaller_class). + /// + private static void WriteStructEnumMarshallerClass(TypeWriter w, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + TypeCategory cat = TypeCategorization.GetCategory(type); + bool blittable = IsTypeBlittable(type); + // "Almost-blittable" includes blittable + bool/char fields. Excludes string/object fields. + // Use the same predicate as IsAnyStruct (which is now scoped to almost-blittable). + AsmResolver.DotNet.Signatures.TypeDefOrRefSignature sig = type.ToTypeSignature(false) is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td2 ? td2 : null!; + bool almostBlittable = cat == TypeCategory.Struct && (sig is null || IsAnyStruct(sig)); + bool isEnum = cat == TypeCategory.Enum; + // Complex structs are non-almost-blittable structs with reference fields (string, object, etc.). + bool isComplexStruct = cat == TypeCategory.Struct && !almostBlittable; + // Detect Nullable reference fields to determine whether the struct's BoxToUnmanaged + // call needs CreateComInterfaceFlags.TrackerSupport (mirrors C++ use_tracker_object_support + // which returns true for IReference`1 generic instances). + bool hasReferenceFields = false; + if (isComplexStruct) + { + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; + if (TryGetNullablePrimitiveMarshallerName(ft, out _)) { hasReferenceFields = true; } + } + } + + // For structs that are mapped (e.g. Duration, KeyTime, RepeatBehavior — they have + // EmitAbi=true and an addition file that completely replaces the public struct), skip + // the per-field ConvertToUnmanaged/ConvertToManaged because the projected struct's + // public fields don't match the WinMD field layout. The truth marshaller for these + // contains only BoxToUnmanaged/UnboxToManaged. + string typeNs = type.Namespace?.Value ?? string.Empty; + string typeNm = type.Name?.Value ?? string.Empty; + bool isMappedStruct = isComplexStruct && MappedTypes.Get(typeNs, typeNm) is not null; + if (isMappedStruct) { isComplexStruct = false; } + + w.Write("public static unsafe class "); + w.Write(nameStripped); + w.Write("Marshaller\n{\n"); + + if (isComplexStruct) + { + // ConvertToUnmanaged: build ABI struct from projected struct via per-field marshalling. + w.Write(" public static "); + WriteTypedefName(w, type, TypedefNameType.ABI, false); + w.Write(" ConvertToUnmanaged("); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(" value)\n {\n"); + w.Write(" return new() {\n"); + bool first = true; + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + string fname = field.Name?.Value ?? ""; + AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; + if (!first) { w.Write(",\n"); } + first = false; + w.Write(" "); + w.Write(fname); + w.Write(" = "); + if (IsString(ft)) + { + w.Write("HStringMarshaller.ConvertToUnmanaged(value."); + w.Write(fname); + w.Write(")"); + } + else if (IsMappedAbiValueType(ft)) + { + w.Write(GetMappedMarshallerName(ft)); + w.Write(".ConvertToUnmanaged(value."); + w.Write(fname); + w.Write(")"); + } + else if (IsHResultException(ft)) + { + // Mapped value type 'HResult' (excluded from IsMappedAbiValueType because + // it's "treated specially in many places", but for nested struct fields the + // marshalling is identical: use ABI.System.ExceptionMarshaller). + w.Write("global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged(value."); + w.Write(fname); + w.Write(")"); + } + else if (ft is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature ftd + && TryResolveStructTypeDef(ftd) is TypeDefinition fieldStructTd + && TypeCategorization.GetCategory(fieldStructTd) == TypeCategory.Struct + && !IsTypeBlittable(fieldStructTd)) + { + // Nested non-blittable struct: marshal via its Marshaller. + w.Write(Helpers.StripBackticks(fieldStructTd.Name?.Value ?? string.Empty)); + w.Write("Marshaller.ConvertToUnmanaged(value."); + w.Write(fname); + w.Write(")"); + } + else if (TryGetNullablePrimitiveMarshallerName(ft, out string? nullableMarshaller)) + { + w.Write(nullableMarshaller!); + w.Write(".BoxToUnmanaged(value."); + w.Write(fname); + w.Write(").DetachThisPtrUnsafe()"); + } + else + { + w.Write("value."); + w.Write(fname); + } + } + w.Write("\n };\n }\n"); + + // ConvertToManaged: construct projected struct via constructor accepting the marshalled fields. + w.Write(" public static "); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(" ConvertToManaged("); + WriteTypedefName(w, type, TypedefNameType.ABI, false); + // Mirror C++ write_convert_to_managed_method_struct (code_writers.h:4536-4540): + // - In component mode: emit object initializer with named field assignments + // (positional ctor not always available on authored types). + // - In non-component mode: emit positional constructor (matches the auto-generated + // primary constructor on projected struct types). + bool useObjectInitializer = w.Settings.Component; + w.Write(" value)\n {\n"); + w.Write(" return new "); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(useObjectInitializer ? "(){\n" : "(\n"); + first = true; + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + string fname = field.Name?.Value ?? ""; + AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; + if (!first) { w.Write(",\n"); } + first = false; + w.Write(" "); + if (useObjectInitializer) + { + w.Write(fname); + w.Write(" = "); + } + if (IsString(ft)) + { + w.Write("HStringMarshaller.ConvertToManaged(value."); + w.Write(fname); + w.Write(")"); + } + else if (IsMappedAbiValueType(ft)) + { + w.Write(GetMappedMarshallerName(ft)); + w.Write(".ConvertToManaged(value."); + w.Write(fname); + w.Write(")"); + } + else if (IsHResultException(ft)) + { + // Mapped value type 'HResult' (excluded from IsMappedAbiValueType because + // it's "treated specially in many places", but for nested struct fields the + // marshalling is identical: use ABI.System.ExceptionMarshaller). + w.Write("global::ABI.System.ExceptionMarshaller.ConvertToManaged(value."); + w.Write(fname); + w.Write(")"); + } + else if (ft is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature ftd2 + && TryResolveStructTypeDef(ftd2) is TypeDefinition fieldStructTd2 + && TypeCategorization.GetCategory(fieldStructTd2) == TypeCategory.Struct + && !IsTypeBlittable(fieldStructTd2)) + { + // Nested non-blittable struct: convert via its Marshaller. + w.Write(Helpers.StripBackticks(fieldStructTd2.Name?.Value ?? string.Empty)); + w.Write("Marshaller.ConvertToManaged(value."); + w.Write(fname); + w.Write(")"); + } + else if (TryGetNullablePrimitiveMarshallerName(ft, out string? nullableMarshaller)) + { + w.Write(nullableMarshaller!); + w.Write(".UnboxToManaged(value."); + w.Write(fname); + w.Write(")"); + } + else + { + w.Write("value."); + w.Write(fname); + } + } + w.Write(useObjectInitializer ? "\n };\n }\n" : "\n );\n }\n"); + + // Dispose: free non-blittable fields. + w.Write(" public static void Dispose("); + WriteTypedefName(w, type, TypedefNameType.ABI, false); + w.Write(" value)\n {\n"); + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + string fname = field.Name?.Value ?? ""; + AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; + if (IsString(ft)) + { + w.Write(" HStringMarshaller.Free(value."); + w.Write(fname); + w.Write(");\n"); + } + else if (IsHResultException(ft)) + { + // HResult/Exception field has no per-value resources to release + // (the ABI representation is just an int HRESULT). Skip Dispose entirely. + continue; + } + else if (IsMappedAbiValueType(ft)) + { + // Mapped value types (DateTime/TimeSpan) have no per-value resources to + // release — the ABI representation is just an int64. Mirror C++ + // set_skip_disposer_if_needed (code_writers.h:6431-6440) which explicitly + // skips the disposer for global::ABI.System.{DateTimeOffset,TimeSpan,Exception}. + continue; + } + else if (ft is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature ftd3 + && TryResolveStructTypeDef(ftd3) is TypeDefinition fieldStructTd3 + && TypeCategorization.GetCategory(fieldStructTd3) == TypeCategory.Struct + && !IsTypeBlittable(fieldStructTd3)) + { + // Nested non-blittable struct: dispose via its Marshaller. + // Mirror C++: this site always uses the fully-qualified marshaller name. + string nestedNs = fieldStructTd3.Namespace?.Value ?? string.Empty; + string nestedNm = Helpers.StripBackticks(fieldStructTd3.Name?.Value ?? string.Empty); + w.Write(" global::ABI."); + w.Write(nestedNs); + w.Write("."); + w.Write(nestedNm); + w.Write("Marshaller.Dispose(value."); + w.Write(fname); + w.Write(");\n"); + } + else if (TryGetNullablePrimitiveMarshallerName(ft, out _)) + { + w.Write(" WindowsRuntimeUnknownMarshaller.Free(value."); + w.Write(fname); + w.Write(");\n"); + } + } + w.Write(" }\n"); + } + + // BoxToUnmanaged: same pattern for all (enum, almost-blittable, complex). + // Truth uses CreateComInterfaceFlags.TrackerSupport when the struct has reference type + // fields (Nullable, etc.) to avoid GC issues with the boxed managed object reference. + w.Write(" public static WindowsRuntimeObjectReferenceValue BoxToUnmanaged("); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + if (isEnum || almostBlittable || isComplexStruct) + { + w.Write("? value)\n {\n"); + w.Write(" return WindowsRuntimeValueTypeMarshaller.BoxToUnmanaged(value, CreateComInterfaceFlags."); + w.Write(hasReferenceFields ? "TrackerSupport" : "None"); + w.Write(", in "); + WriteIidReferenceExpression(w, type); + w.Write(");\n }\n"); + } + else + { + // Mapped struct (Duration/KeyTime/etc.): BoxToUnmanaged is still required because the + // public projected type still routes through this marshaller (it just lacks per-field + // ConvertToUnmanaged/ConvertToManaged because the field layout doesn't match). + w.Write("? value)\n {\n"); + w.Write(" return WindowsRuntimeValueTypeMarshaller.BoxToUnmanaged(value, CreateComInterfaceFlags.None, in "); + WriteIidReferenceExpression(w, type); + w.Write(");\n }\n"); + } + + // UnboxToManaged: simple for almost-blittable; for complex, unbox to ABI struct then ConvertToManaged. + w.Write(" public static "); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + if (isEnum || almostBlittable) + { + w.Write("? UnboxToManaged(void* value)\n {\n"); + w.Write(" return WindowsRuntimeValueTypeMarshaller.UnboxToManaged<"); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(">(value);\n }\n"); + } + else if (isComplexStruct) + { + w.Write("? UnboxToManaged(void* value)\n {\n"); + w.Write(" "); + WriteTypedefName(w, type, TypedefNameType.ABI, false); + w.Write("? abi = WindowsRuntimeValueTypeMarshaller.UnboxToManaged<"); + WriteTypedefName(w, type, TypedefNameType.ABI, false); + w.Write(">(value);\n"); + w.Write(" return abi.HasValue ? ConvertToManaged(abi.GetValueOrDefault()) : null;\n }\n"); + } + else + { + // Mapped struct: unbox directly to projected type (no per-field ConvertToManaged needed + // because the projected struct's field layout matches the WinMD struct layout). + w.Write("? UnboxToManaged(void* value)\n {\n"); + w.Write(" return WindowsRuntimeValueTypeMarshaller.UnboxToManaged<"); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(">(value);\n }\n"); + } + + w.Write("}\n\n"); + + // Emit the InterfaceEntriesImpl static class and the proper ComWrappersMarshallerAttribute + // class derived from WindowsRuntimeComWrappersMarshallerAttribute (matches truth). + // For enums and almost-blittable structs, GetOrCreateComInterfaceForObject uses None. + // For complex structs (with reference fields), it uses TrackerSupport. + // For complex structs, CreateObject converts via the *Marshaller.ConvertToManaged after + // unboxing to the ABI struct. + if (isEnum || almostBlittable || isComplexStruct) + { + string iidRefExpr = w.WriteTemp("%", new System.Action(_ => WriteIidReferenceExpression(w, type))); + + // InterfaceEntriesImpl + w.Write("file static class "); + w.Write(nameStripped); + w.Write("InterfaceEntriesImpl\n{\n"); + w.Write(" [FixedAddressValueType]\n"); + w.Write(" public static readonly ReferenceInterfaceEntries Entries;\n\n"); + w.Write(" static "); + w.Write(nameStripped); + w.Write("InterfaceEntriesImpl()\n {\n"); + w.Write(" Entries.IReferenceValue.IID = "); + w.Write(iidRefExpr); + w.Write(";\n"); + w.Write(" Entries.IReferenceValue.Vtable = "); + w.Write(nameStripped); + w.Write("ReferenceImpl.Vtable;\n"); + w.Write(" Entries.IPropertyValue.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IPropertyValue;\n"); + w.Write(" Entries.IPropertyValue.Vtable = global::WindowsRuntime.InteropServices.IPropertyValueImpl.OtherTypeVtable;\n"); + w.Write(" Entries.IStringable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable;\n"); + w.Write(" Entries.IStringable.Vtable = global::WindowsRuntime.InteropServices.IStringableImpl.Vtable;\n"); + w.Write(" Entries.IWeakReferenceSource.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IWeakReferenceSource;\n"); + w.Write(" Entries.IWeakReferenceSource.Vtable = global::WindowsRuntime.InteropServices.IWeakReferenceSourceImpl.Vtable;\n"); + w.Write(" Entries.IMarshal.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IMarshal;\n"); + w.Write(" Entries.IMarshal.Vtable = global::WindowsRuntime.InteropServices.IMarshalImpl.Vtable;\n"); + w.Write(" Entries.IAgileObject.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IAgileObject;\n"); + w.Write(" Entries.IAgileObject.Vtable = global::WindowsRuntime.InteropServices.IAgileObjectImpl.Vtable;\n"); + w.Write(" Entries.IInspectable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IInspectable;\n"); + w.Write(" Entries.IInspectable.Vtable = global::WindowsRuntime.InteropServices.IInspectableImpl.Vtable;\n"); + w.Write(" Entries.IUnknown.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IUnknown;\n"); + w.Write(" Entries.IUnknown.Vtable = global::WindowsRuntime.InteropServices.IUnknownImpl.Vtable;\n"); + w.Write(" }\n}\n\n"); + + // Mirror C++ write_abi_struct: in component mode the ComWrappers marshaller attribute + // is NOT emitted for STRUCTS (the attribute is supplied by cswinrtgen instead). Enums + // and other types still emit it from write_abi_enum/etc. + if (w.Settings.Component && cat == TypeCategory.Struct) { return; } + + // ComWrappersMarshallerAttribute (full body) + w.Write("internal sealed unsafe class "); + w.Write(nameStripped); + w.Write("ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute\n{\n"); + w.Write(" public override void* GetOrCreateComInterfaceForObject(object value)\n {\n"); + w.Write(" return WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags."); + w.Write(hasReferenceFields ? "TrackerSupport" : "None"); + w.Write(");\n }\n\n"); + w.Write(" public override ComInterfaceEntry* ComputeVtables(out int count)\n {\n"); + w.Write(" count = sizeof(ReferenceInterfaceEntries) / sizeof(ComInterfaceEntry);\n"); + w.Write(" return (ComInterfaceEntry*)Unsafe.AsPointer(in "); + w.Write(nameStripped); + w.Write("InterfaceEntriesImpl.Entries);\n }\n\n"); + w.Write(" public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); + w.Write(" wrapperFlags = CreatedWrapperFlags.NonWrapping;\n"); + if (isComplexStruct) + { + w.Write(" return "); + w.Write(nameStripped); + w.Write("Marshaller.ConvertToManaged(WindowsRuntimeValueTypeMarshaller.UnboxToManagedUnsafe<"); + WriteTypedefName(w, type, TypedefNameType.ABI, true); + w.Write(">(value, in "); + w.Write(iidRefExpr); + w.Write("));\n"); + } + else + { + w.Write(" return WindowsRuntimeValueTypeMarshaller.UnboxToManagedUnsafe<"); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(">(value, in "); + w.Write(iidRefExpr); + w.Write(");\n"); + } + w.Write(" }\n}\n"); + } + else + { + // Fallback: keep the placeholder class so consumer attribute references resolve. + w.Write("internal sealed class "); + w.Write(nameStripped); + w.Write("ComWrappersMarshallerAttribute : global::System.Attribute\n{\n}\n"); + } + } + + /// + /// Writes a marshaller stub for a delegate. + /// + /// + /// Emits just the <Name>Marshaller class for a delegate. Mirrors C++ + /// write_delegate_marshaller. + /// + private static void WriteDelegateMarshallerOnly(TypeWriter w, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + string typeNs = type.Namespace?.Value ?? string.Empty; + string fullProjected = $"global::{typeNs}.{nameStripped}"; + string iidExpr = w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, type))); + + w.Write("\npublic static unsafe class "); + w.Write(nameStripped); + w.Write("Marshaller\n{\n"); + w.Write(" public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); + w.Write(fullProjected); + w.Write(" value)\n {\n"); + w.Write(" return WindowsRuntimeDelegateMarshaller.ConvertToUnmanaged(value, in "); + w.Write(iidExpr); + w.Write(");\n }\n\n"); + w.Write("#nullable enable\n"); + w.Write(" public static "); + w.Write(fullProjected); + w.Write("? ConvertToManaged(void* value)\n {\n"); + w.Write(" return ("); + w.Write(fullProjected); + w.Write("?)WindowsRuntimeDelegateMarshaller.ConvertToManaged<"); + w.Write(nameStripped); + w.Write("ComWrappersCallback>(value);\n }\n"); + w.Write("#nullable disable\n"); + w.Write("}\n"); + } + + /// + /// Emits the <Name>ComWrappersCallback file-scoped class for a delegate. + /// Mirrors C++ write_delegate_comwrappers_callback. Generic delegates are not emitted + /// here at all — the higher-level dispatch in ProjectionGenerator filters out generic + /// types from ABI emission (mirrors C++ main.cpp:412: + /// if (distance(type.GenericParam()) != 0) { continue; }). Open generic delegates + /// can't compile this body anyway because the projected type would have unbound generic + /// parameters. + /// + private static void WriteDelegateComWrappersCallback(TypeWriter w, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + string typeNs = type.Namespace?.Value ?? string.Empty; + string fullProjected = $"global::{typeNs}.{nameStripped}"; + string iidExpr = w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, type))); + + MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); + bool nativeSupported = invoke is not null && IsDelegateInvokeNativeSupported(new MethodSig(invoke)); + + w.Write("\nfile abstract unsafe class "); + w.Write(nameStripped); + w.Write("ComWrappersCallback : IWindowsRuntimeObjectComWrappersCallback\n{\n"); + w.Write(" /// \n"); + w.Write(" public static object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); + w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe(\n"); + w.Write(" externalComObject: value,\n"); + w.Write(" iid: in "); + w.Write(iidExpr); + w.Write(",\n wrapperFlags: out wrapperFlags);\n\n"); + // Always emit the body. The 'valueReference.Invoke' extension method always + // exists (in NativeDelegate); even when its body is itself a stub, this path compiles + // and matches the truth, which never emits 'throw null!' for CreateObject. + w.Write(" return new "); + w.Write(fullProjected); + w.Write("(valueReference."); + w.Write(nameStripped); + w.Write("Invoke);\n"); + _ = nativeSupported; + w.Write(" }\n}\n"); + } + + /// + /// Emits the <Name>ComWrappersMarshallerAttribute class. Mirrors C++ + /// write_delegate_com_wrappers_marshaller_attribute_impl. Generic delegates are not + /// emitted here at all (filtered out in ProjectionGenerator). + /// + private static void WriteDelegateComWrappersMarshallerAttribute(TypeWriter w, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + string iidRefExpr = w.WriteTemp("%", new System.Action(_ => WriteIidReferenceExpression(w, type))); + + w.Write("\ninternal sealed unsafe class "); + w.Write(nameStripped); + w.Write("ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute\n{\n"); + w.Write(" /// \n"); + w.Write(" public override void* GetOrCreateComInterfaceForObject(object value)\n {\n"); + w.Write(" return WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags.TrackerSupport);\n"); + w.Write(" }\n\n"); + w.Write(" /// \n"); + w.Write(" public override ComInterfaceEntry* ComputeVtables(out int count)\n {\n"); + w.Write(" count = sizeof(DelegateReferenceInterfaceEntries) / sizeof(ComInterfaceEntry);\n\n"); + w.Write(" return (ComInterfaceEntry*)Unsafe.AsPointer(in "); + w.Write(nameStripped); + w.Write("InterfaceEntriesImpl.Entries);\n }\n\n"); + w.Write(" /// \n"); + w.Write(" public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); + w.Write(" wrapperFlags = CreatedWrapperFlags.NonWrapping;\n"); + w.Write(" return WindowsRuntimeDelegateMarshaller.UnboxToManaged<"); + w.Write(nameStripped); + w.Write("ComWrappersCallback>(value, in "); + w.Write(iidRefExpr); + w.Write(")!;\n }\n"); + w.Write("}\n"); + } + + /// True if EmitNativeDelegateBody can emit a real (non-throw) body for this signature. + private static bool IsDelegateInvokeNativeSupported(MethodSig sig) + { + AsmResolver.DotNet.Signatures.TypeSignature? rt = sig.ReturnType; + if (rt is not null) + { + if (IsHResultException(rt)) { return false; } + if (!(IsBlittablePrimitive(rt) || IsAnyStruct(rt) || IsString(rt) || IsRuntimeClassOrInterface(rt) || IsObject(rt) || IsGenericInstance(rt) || IsComplexStruct(rt))) { return false; } + } + foreach (ParamInfo p in sig.Params) + { + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) + { + if (p.Type is AsmResolver.DotNet.Signatures.SzArrayTypeSignature szP) + { + if (IsBlittablePrimitive(szP.BaseType)) { continue; } + if (IsAnyStruct(szP.BaseType)) { continue; } + } + return false; + } + if (cat != ParamCategory.In) { return false; } + if (IsHResultException(p.Type)) { return false; } + if (IsBlittablePrimitive(p.Type)) { continue; } + if (IsAnyStruct(p.Type)) { continue; } + if (IsString(p.Type)) { continue; } + if (IsRuntimeClassOrInterface(p.Type)) { continue; } + if (IsObject(p.Type)) { continue; } + if (IsGenericInstance(p.Type)) { continue; } + if (IsComplexStruct(p.Type)) { continue; } + return false; + } + return true; + } + + /// + /// Writes the marshaller infrastructure for a runtime class: + /// * Public *Marshaller class with real ConvertToUnmanaged/ConvertToManaged bodies + /// * file-scoped *ComWrappersMarshallerAttribute (CreateObject implementation) + /// * file-scoped *ComWrappersCallback (IWindowsRuntimeObjectComWrappersCallback for sealed, + /// IWindowsRuntimeUnsealedObjectComWrappersCallback for unsealed) + /// Mirrors C++ write_class_marshaller, write_class_comwrappers_marshaller_attribute, + /// and write_class_comwrappers_callback. + /// + private static void WriteClassMarshallerStub(TypeWriter w, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + string typeNs = type.Namespace?.Value ?? string.Empty; + string fullProjected = $"global::{typeNs}.{nameStripped}"; + + // Get the IID expression for the default interface (used by CreateObject). + ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); + string defaultIfaceIid = defaultIface is not null + ? w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, defaultIface))) + : "default(global::System.Guid)"; + + // Determine the marshalingType expression from the class's [MarshalingBehaviorAttribute] + // (mirrors C++ get_marshaling_type_name). This is used by both the marshaller attribute and the + // callback (the C++ code uses the same value for both). + string marshalingType = GetMarshalingTypeName(type); + + bool isSealed = type.IsSealed; + + // For unsealed classes, the ConvertToUnmanaged path needs to know whether the default interface is + // exclusive-to (mirrors C++ logic). + TypeDefinition? defaultIfaceTd = defaultIface is null ? null : ResolveInterfaceTypeDef(defaultIface); + bool defaultIfaceIsExclusive = defaultIfaceTd is not null && TypeCategorization.IsExclusiveTo(defaultIfaceTd); + + // Public *Marshaller class + w.Write("public static unsafe class "); + w.Write(nameStripped); + w.Write("Marshaller\n{\n"); + w.Write(" public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); + w.Write(fullProjected); + w.Write(" value)\n {\n"); + if (isSealed) + { + // For projected sealed runtime classes, the RCW type is always unwrappable. + w.Write(" if (value is not null)\n {\n"); + w.Write(" return WindowsRuntimeComWrappersMarshal.UnwrapObjectReferenceUnsafe(value).AsValue();\n"); + w.Write(" }\n"); + } + else if (!defaultIfaceIsExclusive && defaultIface is not null) + { + string defIfaceTypeName = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, TypeSemanticsFactory.Get(defaultIface.ToTypeSignature(false)), TypedefNameType.Projected, false))); + w.Write(" if (value is IWindowsRuntimeInterface<"); + w.Write(defIfaceTypeName); + w.Write("> windowsRuntimeInterface)\n {\n"); + w.Write(" return windowsRuntimeInterface.GetInterface();\n"); + w.Write(" }\n"); + } + else + { + w.Write(" if (value is not null)\n {\n"); + w.Write(" return value.GetDefaultInterface();\n"); + w.Write(" }\n"); + } + w.Write(" return default;\n }\n\n"); + w.Write(" public static "); + w.Write(fullProjected); + w.Write("? ConvertToManaged(void* value)\n {\n"); + w.Write(" return ("); + w.Write(fullProjected); + w.Write("?)"); + w.Write(isSealed ? "WindowsRuntimeObjectMarshaller" : "WindowsRuntimeUnsealedObjectMarshaller"); + w.Write(".ConvertToManaged<"); + w.Write(nameStripped); + w.Write("ComWrappersCallback>(value);\n }\n}\n\n"); + + // file-scoped *ComWrappersMarshallerAttribute - implements WindowsRuntimeComWrappersMarshallerAttribute.CreateObject + w.Write("file sealed unsafe class "); + w.Write(nameStripped); + w.Write("ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute\n{\n"); + EmitUnsafeAccessorForDefaultIfaceIfGeneric(w, defaultIface); + w.Write(" public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); + w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReference(\n"); + w.Write(" externalComObject: value,\n"); + w.Write(" iid: "); + w.Write(defaultIfaceIid); + w.Write(",\n"); + w.Write(" marshalingType: "); + w.Write(marshalingType); + w.Write(",\n"); + w.Write(" wrapperFlags: out wrapperFlags);\n\n"); + w.Write(" return new "); + w.Write(fullProjected); + w.Write("(valueReference);\n }\n}\n\n"); + + if (isSealed) + { + // file-scoped *ComWrappersCallback - implements IWindowsRuntimeObjectComWrappersCallback + w.Write("file sealed unsafe class "); + w.Write(nameStripped); + w.Write("ComWrappersCallback : IWindowsRuntimeObjectComWrappersCallback\n{\n"); + EmitUnsafeAccessorForDefaultIfaceIfGeneric(w, defaultIface); + w.Write(" public static object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); + w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe(\n"); + w.Write(" externalComObject: value,\n"); + w.Write(" iid: "); + w.Write(defaultIfaceIid); + w.Write(",\n"); + w.Write(" marshalingType: "); + w.Write(marshalingType); + w.Write(",\n"); + w.Write(" wrapperFlags: out wrapperFlags);\n\n"); + w.Write(" return new "); + w.Write(fullProjected); + w.Write("(valueReference);\n }\n}\n"); + } + else + { + // file-scoped *ComWrappersCallback - implements IWindowsRuntimeUnsealedObjectComWrappersCallback + string nonProjectedRcn = $"{typeNs}.{nameStripped}"; + w.Write("file sealed unsafe class "); + w.Write(nameStripped); + w.Write("ComWrappersCallback : IWindowsRuntimeUnsealedObjectComWrappersCallback\n{\n"); + EmitUnsafeAccessorForDefaultIfaceIfGeneric(w, defaultIface); + + // TryCreateObject (non-projected runtime class name match) + w.Write(" public static unsafe bool TryCreateObject(\n"); + w.Write(" void* value,\n"); + w.Write(" ReadOnlySpan runtimeClassName,\n"); + w.Write(" [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? wrapperObject,\n"); + w.Write(" out CreatedWrapperFlags wrapperFlags)\n {\n"); + w.Write(" if (runtimeClassName.SequenceEqual(\""); + w.Write(nonProjectedRcn); + w.Write("\".AsSpan()))\n {\n"); + w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe(\n"); + w.Write(" externalComObject: value,\n"); + w.Write(" iid: "); + w.Write(defaultIfaceIid); + w.Write(",\n"); + w.Write(" marshalingType: "); + w.Write(marshalingType); + w.Write(",\n"); + w.Write(" wrapperFlags: out wrapperFlags);\n\n"); + w.Write(" wrapperObject = new "); + w.Write(fullProjected); + w.Write("(valueReference);\n return true;\n }\n\n"); + w.Write(" wrapperObject = null;\n wrapperFlags = CreatedWrapperFlags.None;\n return false;\n }\n\n"); + + // CreateObject (fallback) + w.Write(" public static unsafe object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); + w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe(\n"); + w.Write(" externalComObject: value,\n"); + w.Write(" iid: "); + w.Write(defaultIfaceIid); + w.Write(",\n"); + w.Write(" marshalingType: "); + w.Write(marshalingType); + w.Write(",\n"); + w.Write(" wrapperFlags: out wrapperFlags);\n\n"); + w.Write(" return new "); + w.Write(fullProjected); + w.Write("(valueReference);\n }\n}\n"); + } + } + + /// + /// Emits the [UnsafeAccessor] declaration for the default interface IID inside a file-scoped + /// ComWrappers class. Only emits if the default interface is a generic instantiation. + /// Mirrors C++ write_class_comwrappers_marshaller_attribute / write_class_comwrappers_callback + /// behavior of inserting write_unsafe_accessor_for_iid at the top of the class body. + /// + private static void EmitUnsafeAccessorForDefaultIfaceIfGeneric(TypeWriter w, ITypeDefOrRef? defaultIface) + { + if (defaultIface is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + EmitUnsafeAccessorForIid(w, gi); + } + } + + /// + /// Writes a minimal interface 'Methods' static class with method body emission. + /// Mirrors C++ write_static_abi_methods: void/no-args methods and + /// blittable-primitive-return/no-args methods get real implementations; everything else + /// remains as 'throw null!' stubs (deferred — needs full per-parameter marshalling). + /// + private static void WriteInterfaceMarshallerStub(TypeWriter w, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + // Mirrors C++ write_static_abi_classes: visibility is internal if the interface is + // exclusive to a class (and not opted into PublicExclusiveTo) or if it's marked + // [ProjectionInternal]; public otherwise. + bool useInternal = (TypeCategorization.IsExclusiveTo(type) && !w.Settings.PublicExclusiveTo) + || TypeCategorization.IsProjectionInternal(type); + + // Fast ABI: if this interface is a non-default exclusive-to interface of a fast-abi + // class, skip emitting it entirely — its members are merged into the default + // interface's Methods class. Mirrors C++ code_writers.h:9082-9089 + // (write_static_abi_classes early return on contains_other_interface(iface)). + if (IsFastAbiOtherInterface(type)) { return; } + + // If the interface is exclusive-to a class that's been excluded from the projection, + // skip emitting the entire *Methods class — it would be dead code (the owning class + // is manually projected in WinRT.Runtime, e.g. IColorHelperStatics for ColorHelper, + // IColorsStatics for Colors, IFontWeightsStatics for FontWeights). The C++ tool also + // omits these because their owning class is not projected. + if (TypeCategorization.IsExclusiveTo(type)) + { + TypeDefinition? owningClass = GetExclusiveToType(type); + if (owningClass is not null && !w.Settings.Filter.Includes(owningClass)) + { + return; + } + } + + // Mirrors C++ skip_exclusive_events: events on exclusive interfaces (used by the class) + // are inlined in the RCW class, so we skip emitting them in the Methods type. + bool skipExclusiveEvents = false; + if (TypeCategorization.IsExclusiveTo(type) && !w.Settings.PublicExclusiveTo) + { + TypeDefinition? classType = GetExclusiveToType(type); + if (classType is not null) + { + foreach (InterfaceImplementation impl in classType.Interfaces) + { + TypeDefinition? implDef = ResolveInterfaceTypeDef(impl.Interface!); + if (implDef is not null && implDef == type) + { + skipExclusiveEvents = true; + break; + } + } + } + } + + // Fast ABI: if this interface is the default interface of a fast-abi class, the + // generated Methods class must include the merged members of the default interface + // PLUS each [ExclusiveTo] non-default interface in vtable order, with progressively + // increasing slot indices. Mirrors C++ code_writers.h:9113-9128. + // For non-fast-abi interfaces, the segment list is just [(type, INSPECTABLE_METHOD_COUNT, skipExclusiveEvents)]. + const int InspectableMethodCount = 6; + var segments = new List<(TypeDefinition Iface, int StartSlot, bool SkipEvents)>(); + var fastAbi = GetFastAbiClassForInterface(type); + bool isFastAbiDefault = fastAbi is not null && fastAbi.Value.Default is not null + && InterfacesEqualByName(fastAbi.Value.Default, type); + if (isFastAbiDefault) + { + int slot = InspectableMethodCount; + // Default interface: skip its events (they're inlined in the RCW class). + segments.Add((type, slot, true)); + slot += CountMethods(type) + GetClassHierarchyIndex(fastAbi!.Value.Class); + foreach (TypeDefinition other in fastAbi.Value.Others) + { + segments.Add((other, slot, false)); + slot += CountMethods(other); + } + } + else + { + segments.Add((type, InspectableMethodCount, skipExclusiveEvents)); + } + + // Skip emission if the entire merged class would be empty. + bool hasAnyMember = false; + foreach ((TypeDefinition seg, int _, bool segSkipEvents) in segments) + { + if (HasEmittableMembers(seg, segSkipEvents)) { hasAnyMember = true; break; } + } + if (!hasAnyMember) { return; } + + w.Write(useInternal ? "internal static class " : "public static class "); + w.Write(nameStripped); + w.Write("Methods\n{\n"); + + foreach ((TypeDefinition iface, int startSlot, bool segSkipEvents) in segments) + { + EmitMethodsClassMembersFor(w, iface, startSlot, segSkipEvents); + } + + w.Write("}\n"); + } + + /// True if the interface has at least one non-special method, property, or non-skipped event. + private static bool HasEmittableMembers(TypeDefinition iface, bool skipExclusiveEvents) + { + foreach (MethodDefinition m in iface.Methods) + { + if (!Helpers.IsSpecial(m)) { return true; } + } + foreach (PropertyDefinition _ in iface.Properties) { return true; } + if (!skipExclusiveEvents) + { + foreach (EventDefinition _ in iface.Events) { return true; } + } + return false; + } + + /// Returns the number of methods (including special accessors) on the interface. + private static int CountMethods(TypeDefinition iface) + { + int count = 0; + foreach (MethodDefinition _ in iface.Methods) { count++; } + return count; + } + + /// Mirrors C++ get_class_hierarchy_index: distance from in inheritance. + private static int GetClassHierarchyIndex(TypeDefinition classType) + { + if (classType.BaseType is null) { return 0; } + string ns = classType.BaseType.Namespace?.Value ?? string.Empty; + string nm = classType.BaseType.Name?.Value ?? string.Empty; + if (ns == "System" && nm == "Object") { return 0; } + TypeDefinition? baseDef = classType.BaseType as TypeDefinition; + if (baseDef is null && _cacheRef is not null) + { + try { baseDef = classType.BaseType.Resolve(_cacheRef.RuntimeContext); } + catch { baseDef = null; } + baseDef ??= _cacheRef.Find(string.IsNullOrEmpty(ns) ? nm : (ns + "." + nm)); + } + if (baseDef is null) { return 0; } + return GetClassHierarchyIndex(baseDef) + 1; + } + + private static bool InterfacesEqualByName(TypeDefinition a, TypeDefinition b) + { + if (a == b) { return true; } + return (a.Namespace?.Value ?? string.Empty) == (b.Namespace?.Value ?? string.Empty) + && (a.Name?.Value ?? string.Empty) == (b.Name?.Value ?? string.Empty); + } + + /// + /// Emits the per-interface members (methods, properties, events) into an already-open Methods + /// static class. Used both for the standalone case and for the fast-abi merged emission. + /// + private static void EmitMethodsClassMembersFor(TypeWriter w, TypeDefinition type, int startSlot, bool skipExclusiveEvents) + { + // Build a map from each MethodDefinition to its WinMD vtable slot. + // Mirrors C++ get_vmethod_index: slot = (method.index() - vtable_base) + start_slot. + // In AsmResolver, type.Methods is iterated in MethodDef row order, so the position of each + // method in type.Methods (relative to the first method of the type) gives us the same value. + Dictionary methodSlot = new(); + { + int idx = 0; + foreach (MethodDefinition m in type.Methods) + { + methodSlot[m] = idx + startSlot; + idx++; + } + } + + // Emit non-special methods first (output order is unchanged from before; only the slot lookup changes). + foreach (MethodDefinition method in type.Methods) + { + if (Helpers.IsSpecial(method)) { continue; } + string mname = method.Name?.Value ?? string.Empty; + MethodSig sig = new(method); + + w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); + w.Write(" public static unsafe "); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(mname); + w.Write("(WindowsRuntimeObjectReference thisReference"); + if (sig.Params.Count > 0) { w.Write(", "); } + WriteParameterList(w, sig); + w.Write(")"); + + // Emit the body if we can handle this case. Slot comes from the method's WinMD index. + EmitAbiMethodBodyIfSimple(w, sig, methodSlot[method], isNoExcept: Helpers.IsNoExcept(method)); + } + + // Emit property accessors. Each getter / setter consumes one vtable slot — looked up from the underlying method. + foreach (PropertyDefinition prop in type.Properties) + { + string pname = prop.Name?.Value ?? string.Empty; + (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); + string propType = WritePropType(w, prop); + (MethodDefinition? gMethod, MethodDefinition? sMethod) = (getter, setter); + // Mirrors C++ helpers.h:46-49: the [NoException] check on properties applies to BOTH + // accessors of the property (the attribute is on the property itself, not on the + // individual accessors). + bool propIsNoExcept = Helpers.IsNoExcept(prop); + if (gMethod is not null) + { + MethodSig getSig = new(gMethod); + w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); + w.Write(" public static unsafe "); + w.Write(propType); + w.Write(" "); + w.Write(pname); + w.Write("(WindowsRuntimeObjectReference thisReference)"); + EmitAbiMethodBodyIfSimple(w, getSig, methodSlot[gMethod], isNoExcept: propIsNoExcept); + } + if (sMethod is not null) + { + MethodSig setSig = new(sMethod); + w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); + w.Write(" public static unsafe void "); + w.Write(pname); + w.Write("(WindowsRuntimeObjectReference thisReference, "); + // Mirrors C++ code_writers.h:7193 — setter parameter uses the is_set_property=true + // form of write_prop_type, which for SZ array types emits ReadOnlySpan instead + // of T[] (the getter's return-type form). + w.Write(WritePropType(w, prop, isSetProperty: true)); + w.Write(" value)"); + EmitAbiMethodBodyIfSimple(w, setSig, methodSlot[sMethod], paramNameOverride: "value", isNoExcept: propIsNoExcept); + } + } + + // Emit event member methods (returns an event source, takes thisObject + thisReference). + // Skip events on exclusive interfaces used by their class — they're inlined directly in + // the RCW class. (Mirrors C++ skip_exclusive_events.) + foreach (EventDefinition evt in type.Events) + { + if (skipExclusiveEvents) { continue; } + string evtName = evt.Name?.Value ?? string.Empty; + AsmResolver.DotNet.Signatures.TypeSignature evtSig = evt.EventType!.ToTypeSignature(false); + bool isGenericEvent = evtSig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature; + + // Use the add method's WinMD slot. Mirrors C++: events use the add_X method's vmethod_index. + (MethodDefinition? addMethod, MethodDefinition? _) = Helpers.GetEventMethods(evt); + int eventSlot = addMethod is not null && methodSlot.TryGetValue(addMethod, out int es) ? es : 0; + + // Build the projected event source type name. For non-generic delegate handlers, the + // EventSource subclass lives in the ABI namespace alongside this Methods class, so + // we need to use the ABI-qualified name. For generic handlers (Windows.Foundation.*EventHandler), + // it's mapped to global::WindowsRuntime.InteropServices.EventHandlerEventSource<...>. + string eventSourceProjectedFull; + if (isGenericEvent) + { + eventSourceProjectedFull = w.WriteTemp("%", new System.Action(_ => + WriteTypeName(w, TypeSemanticsFactory.Get(evtSig), TypedefNameType.EventSource, true))); + if (!eventSourceProjectedFull.StartsWith("global::", System.StringComparison.Ordinal)) + { + eventSourceProjectedFull = "global::" + eventSourceProjectedFull; + } + } + else + { + // Non-generic delegate handler: the EventSource lives in the same ABI namespace + // as this Methods class, so we use just the short name (matches truth output). + string delegateName = string.Empty; + if (evtSig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) + { + delegateName = td.Type?.Name?.Value ?? string.Empty; + delegateName = Helpers.StripBackticks(delegateName); + } + eventSourceProjectedFull = delegateName + "EventSource"; + } + string eventSourceInteropType = isGenericEvent + ? EncodeInteropTypeName(evtSig, TypedefNameType.EventSource) + ", WinRT.Interop" + : string.Empty; + + // Emit the per-event ConditionalWeakTable static field. + w.Write("\n private static ConditionalWeakTable _"); + w.Write(evtName); + w.Write("\n {\n"); + w.Write(" [MethodImpl(MethodImplOptions.AggressiveInlining)]\n"); + w.Write(" get\n {\n"); + w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); + w.Write(" static ConditionalWeakTable MakeTable()\n {\n"); + w.Write(" _ = global::System.Threading.Interlocked.CompareExchange(ref field, [], null);\n\n"); + w.Write(" return global::System.Threading.Volatile.Read(in field);\n"); + w.Write(" }\n\n"); + w.Write(" return global::System.Threading.Volatile.Read(in field) ?? MakeTable();\n }\n }\n"); + + // Emit the static method that returns the per-instance event source. + w.Write("\n public static "); + w.Write(eventSourceProjectedFull); + w.Write(" "); + w.Write(evtName); + w.Write("(object thisObject, WindowsRuntimeObjectReference thisReference)\n {\n"); + if (isGenericEvent && !string.IsNullOrEmpty(eventSourceInteropType)) + { + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.Constructor)]\n"); + w.Write(" [return: UnsafeAccessorType(\""); + w.Write(eventSourceInteropType); + w.Write("\")]\n"); + w.Write(" static extern object ctor(WindowsRuntimeObjectReference nativeObjectReference, int index);\n\n"); + w.Write(" return _"); + w.Write(evtName); + w.Write(".GetOrAdd(\n"); + w.Write(" key: thisObject,\n"); + w.Write(" valueFactory: static (_, thisReference) => Unsafe.As<"); + w.Write(eventSourceProjectedFull); + w.Write(">(ctor(thisReference, "); + w.Write(eventSlot.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write(")),\n"); + w.Write(" factoryArgument: thisReference);\n"); + } + else + { + // Non-generic delegate: directly construct. + w.Write(" return _"); + w.Write(evtName); + w.Write(".GetOrAdd(\n"); + w.Write(" key: thisObject,\n"); + w.Write(" valueFactory: static (_, thisReference) => new "); + w.Write(eventSourceProjectedFull); + w.Write("(thisReference, "); + w.Write(eventSlot.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write("),\n"); + w.Write(" factoryArgument: thisReference);\n"); + } + w.Write(" }\n"); + } + } + + /// + /// Emits a real method body for the cases we can fully marshal, otherwise emits + /// the 'throw null!' stub. Trailing newline is included. + /// + /// When true, the vtable call is emitted WITHOUT the + /// RestrictedErrorInfo.ThrowExceptionForHR(...) wrap. Mirrors C++ + /// code_writers.h:6725 which checks has_noexcept_attr + /// (is_noexcept(MethodDef) / is_noexcept(Property) in helpers.h:41-49): + /// methods/properties annotated with [Windows.Foundation.Metadata.NoExceptionAttribute] + /// (or remove-overload methods) contractually return S_OK, so the wrap is omitted. + private static void EmitAbiMethodBodyIfSimple(TypeWriter w, MethodSig sig, int slot, string? paramNameOverride = null, bool isNoExcept = false) + { + AsmResolver.DotNet.Signatures.TypeSignature? rt = sig.ReturnType; + + bool returnIsString = rt is not null && IsString(rt); + bool returnIsRefType = rt is not null && (IsRuntimeClassOrInterface(rt) || IsObject(rt) || IsGenericInstance(rt)); + bool returnIsAnyStruct = rt is not null && IsAnyStruct(rt); + bool returnIsComplexStruct = rt is not null && IsComplexStruct(rt); + bool returnIsReceiveArray = rt is AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSzCheck + && (IsBlittablePrimitive(retSzCheck.BaseType) || IsAnyStruct(retSzCheck.BaseType) + || IsString(retSzCheck.BaseType) || IsRuntimeClassOrInterface(retSzCheck.BaseType) || IsObject(retSzCheck.BaseType) + || IsComplexStruct(retSzCheck.BaseType) + || IsHResultException(retSzCheck.BaseType) + || IsMappedAbiValueType(retSzCheck.BaseType)); + bool returnIsHResultException = rt is not null && IsHResultException(rt); + + // Build the function pointer signature: void*, [paramAbiType...,] [retAbiType*,] int + System.Text.StringBuilder fp = new(); + fp.Append("void*"); + foreach (ParamInfo p in sig.Params) + { + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) + { + fp.Append(", uint, void*"); + continue; + } + if (cat == ParamCategory.Out) + { + AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); + fp.Append(", "); + if (IsString(uOut) || IsRuntimeClassOrInterface(uOut) || IsObject(uOut) || IsGenericInstance(uOut)) { fp.Append("void**"); } + else if (IsSystemType(uOut)) { fp.Append("global::ABI.System.Type*"); } + else if (IsComplexStruct(uOut)) { fp.Append(GetAbiStructTypeName(w, uOut)); fp.Append('*'); } + else if (IsAnyStruct(uOut)) { fp.Append(GetBlittableStructAbiType(w, uOut)); fp.Append('*'); } + else { fp.Append(GetAbiPrimitiveType(uOut)); fp.Append('*'); } + continue; + } + if (cat == ParamCategory.Ref) + { + AsmResolver.DotNet.Signatures.TypeSignature uRef = StripByRefAndCustomModifiers(p.Type); + fp.Append(", "); + if (IsComplexStruct(uRef)) { fp.Append(GetAbiStructTypeName(w, uRef)); fp.Append('*'); } + else if (IsAnyStruct(uRef)) { fp.Append(GetBlittableStructAbiType(w, uRef)); fp.Append('*'); } + else { fp.Append(GetAbiPrimitiveType(uRef)); fp.Append('*'); } + continue; + } + if (cat == ParamCategory.ReceiveArray) + { + AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); + fp.Append(", uint*, "); + if (IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType)) + { + fp.Append("void*"); + } + else if (IsHResultException(sza.BaseType)) + { + fp.Append("global::ABI.System.Exception"); + } + else if (IsMappedAbiValueType(sza.BaseType)) + { + fp.Append(GetMappedAbiTypeName(sza.BaseType)); + } + else if (IsComplexStruct(sza.BaseType)) { fp.Append(GetAbiStructTypeName(w, sza.BaseType)); } + else if (IsAnyStruct(sza.BaseType)) { fp.Append(GetBlittableStructAbiType(w, sza.BaseType)); } + else { fp.Append(GetAbiPrimitiveType(sza.BaseType)); } + fp.Append("**"); + continue; + } + fp.Append(", "); + if (IsHResultException(p.Type)) { fp.Append("global::ABI.System.Exception"); } + else if (IsString(p.Type) || IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type) || IsGenericInstance(p.Type)) { fp.Append("void*"); } + else if (IsSystemType(p.Type)) { fp.Append("global::ABI.System.Type"); } + else if (IsAnyStruct(p.Type)) { fp.Append(GetBlittableStructAbiType(w, p.Type)); } + else if (IsMappedAbiValueType(p.Type)) { fp.Append(GetMappedAbiTypeName(p.Type)); } + else if (IsComplexStruct(p.Type)) { fp.Append(GetAbiStructTypeName(w, p.Type)); } + else { fp.Append(GetAbiPrimitiveType(p.Type)); } + } + if (rt is not null) + { + if (returnIsReceiveArray) + { + AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)rt; + fp.Append(", uint*, "); + if (IsString(retSz.BaseType) || IsRuntimeClassOrInterface(retSz.BaseType) || IsObject(retSz.BaseType)) + { + fp.Append("void*"); + } + else if (IsComplexStruct(retSz.BaseType)) + { + fp.Append(GetAbiStructTypeName(w, retSz.BaseType)); + } + else if (IsHResultException(retSz.BaseType)) + { + fp.Append("global::ABI.System.Exception"); + } + else if (IsMappedAbiValueType(retSz.BaseType)) + { + fp.Append(GetMappedAbiTypeName(retSz.BaseType)); + } + else if (IsAnyStruct(retSz.BaseType)) + { + fp.Append(GetBlittableStructAbiType(w, retSz.BaseType)); + } + else + { + fp.Append(GetAbiPrimitiveType(retSz.BaseType)); + } + fp.Append("**"); + } + else if (returnIsHResultException) + { + fp.Append(", global::ABI.System.Exception*"); + } + else + { + fp.Append(", "); + if (returnIsString || returnIsRefType) { fp.Append("void**"); } + else if (rt is not null && IsSystemType(rt)) { fp.Append("global::ABI.System.Type*"); } + else if (returnIsAnyStruct) { fp.Append(GetBlittableStructAbiType(w, rt!)); fp.Append('*'); } + else if (returnIsComplexStruct) { fp.Append(GetAbiStructTypeName(w, rt!)); fp.Append('*'); } + else if (rt is not null && IsMappedAbiValueType(rt)) { fp.Append(GetMappedAbiTypeName(rt)); fp.Append('*'); } + else { fp.Append(GetAbiPrimitiveType(rt!)); fp.Append('*'); } + } + } + fp.Append(", int"); + + w.Write("\n {\n"); + w.Write(" using WindowsRuntimeObjectReferenceValue thisValue = thisReference.AsValue();\n"); + w.Write(" void* ThisPtr = thisValue.GetThisPtrUnsafe();\n"); + + // Declare 'using' marshaller values for ref-type parameters (these need disposing). + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + if (IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type)) + { + string localName = GetParamLocalName(p, paramNameOverride); + string callName = GetParamName(p, paramNameOverride); + w.Write(" using WindowsRuntimeObjectReferenceValue __"); + w.Write(localName); + w.Write(" = "); + EmitMarshallerConvertToUnmanaged(w, p.Type, callName); + w.Write(";\n"); + } + else if (IsNullableT(p.Type)) + { + // Nullable param: use Marshaller.BoxToUnmanaged. Mirrors truth pattern. + string localName = GetParamLocalName(p, paramNameOverride); + string callName = GetParamName(p, paramNameOverride); + AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(p.Type)!; + string innerMarshaller = GetNullableInnerMarshallerName(w, inner); + w.Write(" using WindowsRuntimeObjectReferenceValue __"); + w.Write(localName); + w.Write(" = "); + w.Write(innerMarshaller); + w.Write(".BoxToUnmanaged("); + w.Write(callName); + w.Write(");\n"); + } + else if (IsGenericInstance(p.Type)) + { + // Generic instance param: emit a local UnsafeAccessor delegate to get the marshaller method. + string localName = GetParamLocalName(p, paramNameOverride); + string callName = GetParamName(p, paramNameOverride); + string interopTypeName = EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, p.Type, false))); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); + w.Write(" static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_"); + w.Write(localName); + w.Write("([UnsafeAccessorType(\""); + w.Write(interopTypeName); + w.Write("\")] object _, "); + w.Write(projectedTypeName); + w.Write(" value);\n"); + w.Write(" using WindowsRuntimeObjectReferenceValue __"); + w.Write(localName); + w.Write(" = ConvertToUnmanaged_"); + w.Write(localName); + w.Write("(null, "); + w.Write(callName); + w.Write(");\n"); + } + } + // (String input params are now stack-allocated via the fast-path pinning pattern below; + // no separate void* local declaration or up-front allocation is needed.) + // Declare locals for HResult/Exception input parameters (converted up-front). + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + if (ParamHelpers.GetParamCategory(p) != ParamCategory.In) { continue; } + if (!IsHResultException(p.Type)) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + string callName = GetParamName(p, paramNameOverride); + w.Write(" global::ABI.System.Exception __"); + w.Write(localName); + w.Write(" = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged("); + w.Write(callName); + w.Write(");\n"); + } + // Declare locals for mapped value-type input parameters (DateTime/TimeSpan): convert via marshaller up-front. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + if (ParamHelpers.GetParamCategory(p) != ParamCategory.In) { continue; } + if (!IsMappedAbiValueType(p.Type)) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + string callName = GetParamName(p, paramNameOverride); + w.Write(" "); + w.Write(GetMappedAbiTypeName(p.Type)); + w.Write(" __"); + w.Write(localName); + w.Write(" = "); + w.Write(GetMappedMarshallerName(p.Type)); + w.Write(".ConvertToUnmanaged("); + w.Write(callName); + w.Write(");\n"); + } + // Declare locals for complex-struct input parameters (e.g. ProfileUsage with nested + // string/Nullable fields): default-initialize OUTSIDE try, assign inside try via marshaller, + // dispose in finally. Mirrors C++ behavior for non-blittable struct input params. + // Includes both 'in' (ParamCategory.In) and 'in T' (ParamCategory.Ref) forms. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.In && cat != ParamCategory.Ref) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature pType = StripByRefAndCustomModifiers(p.Type); + if (!IsComplexStruct(pType)) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + w.Write(" "); + w.Write(GetAbiStructTypeName(w, pType)); + w.Write(" __"); + w.Write(localName); + w.Write(" = default;\n"); + } + // Declare locals for Out parameters (need to be passed as &__ to the call). + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.Out) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); + w.Write(" "); + if (IsString(uOut) || IsRuntimeClassOrInterface(uOut) || IsObject(uOut) || IsGenericInstance(uOut)) { w.Write("void*"); } + else if (IsSystemType(uOut)) { w.Write("global::ABI.System.Type"); } + else if (IsComplexStruct(uOut)) { w.Write(GetAbiStructTypeName(w, uOut)); } + else if (IsAnyStruct(uOut)) { w.Write(GetBlittableStructAbiType(w, uOut)); } + else { w.Write(GetAbiPrimitiveType(uOut)); } + w.Write(" __"); + w.Write(localName); + w.Write(" = default;\n"); + } + // Declare locals for ReceiveArray params (uint length + element pointer). + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.ReceiveArray) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); + w.Write(" uint __"); + w.Write(localName); + w.Write("_length = default;\n"); + w.Write(" "); + // Element ABI type: void* for ref types; ABI struct for complex/blittable structs; + // primitive ABI otherwise. + if (IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType)) + { + w.Write("void*"); + } + else if (IsComplexStruct(sza.BaseType)) + { + w.Write(GetAbiStructTypeName(w, sza.BaseType)); + } + else if (IsAnyStruct(sza.BaseType)) + { + w.Write(GetBlittableStructAbiType(w, sza.BaseType)); + } + else + { + w.Write(GetAbiPrimitiveType(sza.BaseType)); + } + w.Write("* __"); + w.Write(localName); + w.Write("_data = default;\n"); + } + // Declare InlineArray16 + ArrayPool fallback for non-blittable PassArray params + // (runtime classes, objects, strings). Runtime class/object: just one InlineArray16. + // String: also needs InlineArray16 + InlineArray16 for pinned handles. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + // Non-blittable element type: emit InlineArray16 + ArrayPool. + // For mapped value types (DateTime/TimeSpan), use the ABI struct type. + // For complex structs (e.g. authored BasicStruct with reference fields), use the ABI + // struct type. For everything else (runtime classes, objects, strings), use nint. + string localName = GetParamLocalName(p, paramNameOverride); + string callName = GetParamName(p, paramNameOverride); + string storageT = IsMappedAbiValueType(szArr.BaseType) + ? GetMappedAbiTypeName(szArr.BaseType) + : IsComplexStruct(szArr.BaseType) + ? GetAbiStructTypeName(w, szArr.BaseType) + : IsHResultException(szArr.BaseType) + ? "global::ABI.System.Exception" + : "nint"; + w.Write("\n Unsafe.SkipInit(out InlineArray16<"); + w.Write(storageT); + w.Write("> __"); + w.Write(localName); + w.Write("_inlineArray);\n"); + w.Write(" "); + w.Write(storageT); + w.Write("[] __"); + w.Write(localName); + w.Write("_arrayFromPool = null;\n"); + w.Write(" Span<"); + w.Write(storageT); + w.Write("> __"); + w.Write(localName); + w.Write("_span = "); + w.Write(callName); + w.Write(".Length <= 16\n ? __"); + w.Write(localName); + w.Write("_inlineArray[.."); + w.Write(callName); + w.Write(".Length]\n : (__"); + w.Write(localName); + w.Write("_arrayFromPool = global::System.Buffers.ArrayPool<"); + w.Write(storageT); + w.Write(">.Shared.Rent("); + w.Write(callName); + w.Write(".Length));\n"); + + if (IsString(szArr.BaseType) && cat == ParamCategory.PassArray) + { + // Strings need an additional InlineArray16 + InlineArray16 (pinned handles). + // Only required for PassArray (managed -> HSTRING conversion); FillArray's native side + // fills HSTRING handles directly into the nint storage. + w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); + w.Write(localName); + w.Write("_inlineHeaderArray);\n"); + w.Write(" HStringHeader[] __"); + w.Write(localName); + w.Write("_headerArrayFromPool = null;\n"); + w.Write(" Span __"); + w.Write(localName); + w.Write("_headerSpan = "); + w.Write(callName); + w.Write(".Length <= 16\n ? __"); + w.Write(localName); + w.Write("_inlineHeaderArray[.."); + w.Write(callName); + w.Write(".Length]\n : (__"); + w.Write(localName); + w.Write("_headerArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); + w.Write(callName); + w.Write(".Length));\n"); + + w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); + w.Write(localName); + w.Write("_inlinePinnedHandleArray);\n"); + w.Write(" nint[] __"); + w.Write(localName); + w.Write("_pinnedHandleArrayFromPool = null;\n"); + w.Write(" Span __"); + w.Write(localName); + w.Write("_pinnedHandleSpan = "); + w.Write(callName); + w.Write(".Length <= 16\n ? __"); + w.Write(localName); + w.Write("_inlinePinnedHandleArray[.."); + w.Write(callName); + w.Write(".Length]\n : (__"); + w.Write(localName); + w.Write("_pinnedHandleArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); + w.Write(callName); + w.Write(".Length));\n"); + } + } + if (returnIsReceiveArray) + { + AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)rt!; + w.Write(" uint __retval_length = default;\n"); + w.Write(" "); + if (IsString(retSz.BaseType) || IsRuntimeClassOrInterface(retSz.BaseType) || IsObject(retSz.BaseType)) + { + w.Write("void*"); + } + else if (IsComplexStruct(retSz.BaseType)) + { + w.Write(GetAbiStructTypeName(w, retSz.BaseType)); + } + else if (IsHResultException(retSz.BaseType)) + { + w.Write("global::ABI.System.Exception"); + } + else if (IsMappedAbiValueType(retSz.BaseType)) + { + w.Write(GetMappedAbiTypeName(retSz.BaseType)); + } + else if (IsAnyStruct(retSz.BaseType)) + { + w.Write(GetBlittableStructAbiType(w, retSz.BaseType)); + } + else + { + w.Write(GetAbiPrimitiveType(retSz.BaseType)); + } + w.Write("* __retval_data = default;\n"); + } + else if (returnIsHResultException) + { + w.Write(" global::ABI.System.Exception __retval = default;\n"); + } + else if (returnIsString || returnIsRefType) + { + w.Write(" void* __retval = default;\n"); + } + else if (returnIsAnyStruct) + { + w.Write(" "); + w.Write(GetBlittableStructAbiType(w, rt!)); + w.Write(" __retval = default;\n"); + } + else if (returnIsComplexStruct) + { + w.Write(" "); + w.Write(GetAbiStructTypeName(w, rt!)); + w.Write(" __retval = default;\n"); + } + else if (rt is not null && IsMappedAbiValueType(rt)) + { + // Mapped value type return (e.g. DateTime/TimeSpan): use the ABI struct as __retval. + w.Write(" "); + w.Write(GetMappedAbiTypeName(rt)); + w.Write(" __retval = default;\n"); + } + else if (rt is not null && IsSystemType(rt)) + { + // System.Type return: use ABI Type struct as __retval. + w.Write(" global::ABI.System.Type __retval = default;\n"); + } + else if (rt is not null) + { + w.Write(" "); + w.Write(GetAbiPrimitiveType(rt)); + w.Write(" __retval = default;\n"); + } + + // Determine if we need a try/finally (for cleanup of string/refType return or receive array + // return or Out runtime class params). Input string params no longer need try/finally — + // they use the HString fast-path (stack-allocated HStringReference, no free needed). + bool hasOutNeedsCleanup = false; + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.Out) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); + if (IsString(uOut) || IsRuntimeClassOrInterface(uOut) || IsObject(uOut) || IsSystemType(uOut) || IsComplexStruct(uOut) || IsGenericInstance(uOut)) { hasOutNeedsCleanup = true; break; } + } + bool hasReceiveArray = false; + for (int i = 0; i < sig.Params.Count; i++) + { + if (ParamHelpers.GetParamCategory(sig.Params[i]) == ParamCategory.ReceiveArray) { hasReceiveArray = true; break; } + } + bool hasNonBlittablePassArray = false; + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if ((cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) + && p.Type is AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArrCheck + && !IsBlittablePrimitive(szArrCheck.BaseType) && !IsAnyStruct(szArrCheck.BaseType) + && !IsMappedAbiValueType(szArrCheck.BaseType)) + { + hasNonBlittablePassArray = true; break; + } + } + bool hasComplexStructInput = false; + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if ((cat == ParamCategory.In || cat == ParamCategory.Ref) && IsComplexStruct(StripByRefAndCustomModifiers(p.Type))) { hasComplexStructInput = true; break; } + } + // System.Type return: ABI.System.Type contains an HSTRING that must be disposed + // after marshalling to managed System.Type, otherwise the HSTRING leaks. Mirrors + // C++ abi_marshaler::write_dispose path for is_out + non-empty marshaler_type. + bool returnIsSystemTypeForCleanup = rt is not null && IsSystemType(rt); + bool needsTryFinally = returnIsString || returnIsRefType || returnIsReceiveArray || hasOutNeedsCleanup || hasReceiveArray || returnIsComplexStruct || hasNonBlittablePassArray || hasComplexStructInput || returnIsSystemTypeForCleanup; + if (needsTryFinally) { w.Write(" try\n {\n"); } + + string indent = needsTryFinally ? " " : " "; + + // Inside try (if applicable): assign complex-struct input locals via marshaller. + // Mirrors truth pattern: '__value = ProfileUsageMarshaller.ConvertToUnmanaged(value);' + // Includes both 'in' (ParamCategory.In) and 'in T' (ParamCategory.Ref) forms. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.In && cat != ParamCategory.Ref) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature pType = StripByRefAndCustomModifiers(p.Type); + if (!IsComplexStruct(pType)) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + string callName = GetParamName(p, paramNameOverride); + w.Write(indent); + w.Write("__"); + w.Write(localName); + w.Write(" = "); + w.Write(GetMarshallerFullName(w, pType)); + w.Write(".ConvertToUnmanaged("); + w.Write(callName); + w.Write(");\n"); + } + // Type input params: set up TypeReference locals before the fixed block. Mirrors truth: + // global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe(forType, out TypeReference __forType); + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + if (ParamHelpers.GetParamCategory(p) != ParamCategory.In) { continue; } + if (!IsSystemType(p.Type)) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + string callName = GetParamName(p, paramNameOverride); + w.Write(indent); + w.Write("global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe("); + w.Write(callName); + w.Write(", out TypeReference __"); + w.Write(localName); + w.Write(");\n"); + } + // Open a SINGLE fixed-block for ALL pinnable inputs (mirrors C++ write_abi_invoke): + // 1. Ref params (typed ptr, separate "fixed(T* _x = &x)\n" lines, no braces) + // 2. Complex-struct PassArrays (typed ptr, separate fixed line) + // 3. All other "void*"-style pinnables (strings, Type[], blittable PassArrays, + // reference-type PassArrays via inline-pool span) merged into ONE + // "fixed(void* _a = ..., _b = ..., ...) {\n" block. + // + // C# allows multiple chained "fixed(...)" without braces to share the next braced + // body, which is what the C++ tool emits. This avoids the deep nesting mine had + // when emitting a separate fixed block per PassArray. + int fixedNesting = 0; + + // Step 1: Emit typed-pointer fixed lines for Ref params and complex-struct PassArrays + // (no braces - they share the body of the upcoming combined fixed-void* block, OR + // each other if no void* block is needed). + bool hasAnyVoidStarPinnable = false; + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (IsString(p.Type) || IsSystemType(p.Type)) { hasAnyVoidStarPinnable = true; continue; } + if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) + { + // All PassArrays (including complex structs) go in the void* combined block, + // matching truth's pattern. Complex structs use a (T*) cast at the call site. + hasAnyVoidStarPinnable = true; + } + } + // Emit typed fixed lines for Ref params. + // Skip Ref+ComplexStruct: those are marshalled via __local (no fixed needed) and + // passed as &__local at the call site, mirroring C++ tool's is_value_type_in path. + int typedFixedCount = 0; + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat == ParamCategory.Ref) + { + AsmResolver.DotNet.Signatures.TypeSignature uRefSkip = StripByRefAndCustomModifiers(p.Type); + if (IsComplexStruct(uRefSkip)) { continue; } + string callName = GetParamName(p, paramNameOverride); + string localName = GetParamLocalName(p, paramNameOverride); + AsmResolver.DotNet.Signatures.TypeSignature uRef = uRefSkip; + string abiType = IsAnyStruct(uRef) ? GetBlittableStructAbiType(w, uRef) : GetAbiPrimitiveType(uRef); + w.Write(indent); + w.Write(new string(' ', fixedNesting * 4)); + w.Write("fixed("); + w.Write(abiType); + w.Write("* _"); + w.Write(localName); + w.Write(" = &"); + w.Write(callName); + w.Write(")\n"); + typedFixedCount++; + } + } + + // Step 2: Emit ONE combined fixed-void* block for all pinnables that share the + // same scope. Each variable is "_localName = rhsExpr". Strings get an extra + // "_localName_inlineHeaderArray = __localName_headerSpan" entry. + bool stringPinnablesEmitted = false; + if (hasAnyVoidStarPinnable) + { + w.Write(indent); + w.Write(new string(' ', fixedNesting * 4)); + w.Write("fixed(void* "); + bool first = true; + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + bool isString = IsString(p.Type); + bool isType = IsSystemType(p.Type); + bool isPassArray = cat == ParamCategory.PassArray || cat == ParamCategory.FillArray; + if (!isString && !isType && !isPassArray) { continue; } + string callName = GetParamName(p, paramNameOverride); + string localName = GetParamLocalName(p, paramNameOverride); + if (!first) { w.Write(", "); } + first = false; + w.Write("_"); + w.Write(localName); + w.Write(" = "); + if (isType) + { + w.Write("__"); + w.Write(localName); + } + else if (isPassArray) + { + AsmResolver.DotNet.Signatures.TypeSignature elemT = ((AsmResolver.DotNet.Signatures.SzArrayTypeSignature)p.Type).BaseType; + bool isBlittableElem = IsBlittablePrimitive(elemT) || IsAnyStruct(elemT); + bool isStringElem = IsString(elemT); + if (isBlittableElem) + { + w.Write(callName); + } + else + { + w.Write("__"); + w.Write(localName); + w.Write("_span"); + } + // For string elements: only PassArray needs the additional inlineHeaderArray + // pinned alongside the data span. FillArray fills HSTRINGs into the nint + // storage directly (no header conversion needed). + if (isStringElem && cat == ParamCategory.PassArray) + { + w.Write(", _"); + w.Write(localName); + w.Write("_inlineHeaderArray = __"); + w.Write(localName); + w.Write("_headerSpan"); + } + } + else + { + // string param + w.Write(callName); + } + } + w.Write(")\n"); + w.Write(indent); + w.Write(new string(' ', fixedNesting * 4)); + w.Write("{\n"); + fixedNesting++; + // Inside the body: emit HStringMarshaller calls for input string params. + for (int i = 0; i < sig.Params.Count; i++) + { + if (!IsString(sig.Params[i].Type)) { continue; } + string callName = GetParamName(sig.Params[i], paramNameOverride); + string localName = GetParamLocalName(sig.Params[i], paramNameOverride); + w.Write(indent); + w.Write(new string(' ', fixedNesting * 4)); + w.Write("HStringMarshaller.ConvertToUnmanagedUnsafe((char*)_"); + w.Write(localName); + w.Write(", "); + w.Write(callName); + w.Write("?.Length, out HStringReference __"); + w.Write(localName); + w.Write(");\n"); + } + stringPinnablesEmitted = true; + } + else if (typedFixedCount > 0) + { + // Typed fixed lines exist but no void* combined block - we need a body block + // to host them. Open a brace block after the last typed fixed line. + w.Write(indent); + w.Write(new string(' ', fixedNesting * 4)); + w.Write("{\n"); + fixedNesting++; + } + // Suppress unused variable warning when block above doesn't fire. + _ = stringPinnablesEmitted; + + string callIndent = indent + new string(' ', fixedNesting * 4); + + // For non-blittable PassArray params, emit CopyToUnmanaged_ (UnsafeAccessor) and call + // it to populate the inline/pooled storage from the user-supplied span. For string arrays, + // use HStringArrayMarshaller.ConvertToUnmanagedUnsafe instead. + // FillArray of strings is the exception: the native side fills the HSTRING handles, so + // there's nothing to convert pre-call (the post-call CopyToManaged_ handles writeback). + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + string callName = GetParamName(p, paramNameOverride); + string localName = GetParamLocalName(p, paramNameOverride); + if (IsString(szArr.BaseType)) + { + // Skip pre-call ConvertToUnmanagedUnsafe for FillArray of strings — there's + // nothing to convert (native fills the handles). Mirrors C++ truth pattern. + if (cat == ParamCategory.FillArray) { continue; } + w.Write(callIndent); + w.Write("HStringArrayMarshaller.ConvertToUnmanagedUnsafe(\n"); + w.Write(callIndent); + w.Write(" source: "); + w.Write(callName); + w.Write(",\n"); + w.Write(callIndent); + w.Write(" hstringHeaders: (HStringHeader*) _"); + w.Write(localName); + w.Write("_inlineHeaderArray,\n"); + w.Write(callIndent); + w.Write(" hstrings: __"); + w.Write(localName); + w.Write("_span,\n"); + w.Write(callIndent); + w.Write(" pinnedGCHandles: __"); + w.Write(localName); + w.Write("_pinnedHandleSpan);\n"); + } + else + { + // FillArray (Span) of non-blittable element types: skip pre-call + // CopyToUnmanaged. The buffer the native side gets (_) is uninitialized + // ABI-format storage; the native callee fills it. The post-call writeback loop + // emits CopyToManaged_ to propagate the native fills into the user's + // managed Span. (Mirrors C++ marshaler.write_marshal_to_abi which only emits + // CopyToUnmanaged for PassArray, not FillArray.) + if (cat == ParamCategory.FillArray) { continue; } + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szArr.BaseType)))); + string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + // For mapped value types (DateTime/TimeSpan) and complex structs, the storage + // element is the ABI struct type; the data pointer parameter type uses that + // ABI struct. The fixed() opens with void* (per truth's pattern), so a cast + // is required at the call site. For runtime classes/objects, use void**. + string dataParamType; + string dataCastType; + if (IsMappedAbiValueType(szArr.BaseType)) + { + dataParamType = GetMappedAbiTypeName(szArr.BaseType) + "*"; + dataCastType = "(" + GetMappedAbiTypeName(szArr.BaseType) + "*)"; + } + else if (IsHResultException(szArr.BaseType)) + { + dataParamType = "global::ABI.System.Exception*"; + dataCastType = "(global::ABI.System.Exception*)"; + } + else if (IsComplexStruct(szArr.BaseType)) + { + string abiStructName = GetAbiStructTypeName(w, szArr.BaseType); + dataParamType = abiStructName + "*"; + dataCastType = "(" + abiStructName + "*)"; + } + else + { + dataParamType = "void**"; + dataCastType = "(void**)"; + } + w.Write(callIndent); + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToUnmanaged\")]\n"); + w.Write(callIndent); + w.Write("static extern void CopyToUnmanaged_"); + w.Write(localName); + w.Write("([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); + w.Write("\")] object _, ReadOnlySpan<"); + w.Write(elementProjected); + w.Write("> span, uint length, "); + w.Write(dataParamType); + w.Write(" data);\n"); + w.Write(callIndent); + w.Write("CopyToUnmanaged_"); + w.Write(localName); + w.Write("(null, "); + w.Write(callName); + w.Write(", (uint)"); + w.Write(callName); + w.Write(".Length, "); + w.Write(dataCastType); + w.Write("_"); + w.Write(localName); + w.Write(");\n"); + } + } + + w.Write(callIndent); + // Mirrors C++ code_writers.h:6725 - omit the ThrowExceptionForHR wrap when the + // method/property is [NoException] (its HRESULT is contractually S_OK). + if (!isNoExcept) + { + w.Write("RestrictedErrorInfo.ThrowExceptionForHR((*(delegate* unmanaged[MemberFunction]<"); + } + else + { + w.Write("(*(delegate* unmanaged[MemberFunction]<"); + } + w.Write(fp.ToString()); + w.Write(">**)ThisPtr)["); + w.Write(slot); + w.Write("](ThisPtr"); + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) + { + string callName = GetParamName(p, paramNameOverride); + string localName = GetParamLocalName(p, paramNameOverride); + w.Write(",\n (uint)"); + w.Write(callName); + w.Write(".Length, _"); + w.Write(localName); + continue; + } + if (cat == ParamCategory.Out) + { + string localName = GetParamLocalName(p, paramNameOverride); + w.Write(",\n &__"); + w.Write(localName); + continue; + } + if (cat == ParamCategory.ReceiveArray) + { + string localName = GetParamLocalName(p, paramNameOverride); + w.Write(",\n &__"); + w.Write(localName); + w.Write("_length, &__"); + w.Write(localName); + w.Write("_data"); + continue; + } + if (cat == ParamCategory.Ref) + { + string localName = GetParamLocalName(p, paramNameOverride); + AsmResolver.DotNet.Signatures.TypeSignature uRefArg = StripByRefAndCustomModifiers(p.Type); + if (IsComplexStruct(uRefArg)) + { + // Complex struct 'in' (Ref) param: pass &__local (the marshaled ABI struct). + w.Write(",\n &__"); + w.Write(localName); + } + else + { + // 'in T' projected param: pass the pinned pointer. + w.Write(",\n _"); + w.Write(localName); + } + continue; + } + w.Write(",\n "); + if (IsHResultException(p.Type)) + { + w.Write("__"); + w.Write(GetParamLocalName(p, paramNameOverride)); + } + else if (IsString(p.Type)) + { + w.Write("__"); + w.Write(GetParamLocalName(p, paramNameOverride)); + w.Write(".HString"); + } + else if (IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type) || IsGenericInstance(p.Type)) + { + w.Write("__"); + w.Write(GetParamLocalName(p, paramNameOverride)); + w.Write(".GetThisPtrUnsafe()"); + } + else if (IsSystemType(p.Type)) + { + // System.Type input: pass the pre-converted ABI Type struct (via the local set up before the call). + w.Write("__"); + w.Write(GetParamLocalName(p, paramNameOverride)); + w.Write(".ConvertToUnmanagedUnsafe()"); + } + else if (IsMappedAbiValueType(p.Type)) + { + // Mapped value-type input: pass the pre-converted ABI local. + w.Write("__"); + w.Write(GetParamLocalName(p, paramNameOverride)); + } + else if (IsComplexStruct(p.Type)) + { + // Complex struct input: pass the pre-converted ABI struct local. + w.Write("__"); + w.Write(GetParamLocalName(p, paramNameOverride)); + } + else if (IsAnyStruct(p.Type)) + { + w.Write(GetParamName(p, paramNameOverride)); + } + else + { + EmitParamArgConversion(w, p, paramNameOverride); + } + } + if (returnIsReceiveArray) + { + w.Write(",\n &__retval_length, &__retval_data"); + } + else if (rt is not null) + { + w.Write(",\n &__retval"); + } + // Close the vtable call. One less ')' when noexcept (no ThrowExceptionForHR wrap). + w.Write(isNoExcept ? ");\n" : "));\n"); + + // After call: copy native-filled values back into the user's managed Span for + // FillArray of non-blittable element types. The native callee wrote into our + // ABI-format buffer (_) which is separate from the user's Span; we need to + // CopyToManaged_ to convert each ABI element back to the projected form and + // store it in the user's Span. Mirrors C++ marshaler.write_marshal_from_abi + // (code_writers.h:6268). + // Blittable element types (primitives and almost-blittable structs) don't need this + // because the user's Span wraps the same memory the native side wrote to. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szFA) { continue; } + if (IsBlittablePrimitive(szFA.BaseType) || IsAnyStruct(szFA.BaseType)) { continue; } + string callName = GetParamName(p, paramNameOverride); + string localName = GetParamLocalName(p, paramNameOverride); + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szFA.BaseType)))); + string elementInteropArg = EncodeInteropTypeName(szFA.BaseType, TypedefNameType.Projected); + // Determine the ABI element type for the data pointer parameter. + // - Strings / runtime classes / objects: void** + // - HResult exception: global::ABI.System.Exception* + // - Mapped value types: global::ABI.System.{DateTimeOffset|TimeSpan}* + // - Complex structs: * + string dataParamType; + string dataCastType; + if (IsString(szFA.BaseType) || IsRuntimeClassOrInterface(szFA.BaseType) || IsObject(szFA.BaseType)) + { + dataParamType = "void** data"; + dataCastType = "(void**)"; + } + else if (IsHResultException(szFA.BaseType)) + { + dataParamType = "global::ABI.System.Exception* data"; + dataCastType = "(global::ABI.System.Exception*)"; + } + else if (IsMappedAbiValueType(szFA.BaseType)) + { + string abiName = GetMappedAbiTypeName(szFA.BaseType); + dataParamType = abiName + "* data"; + dataCastType = "(" + abiName + "*)"; + } + else + { + string abiStructName = GetAbiStructTypeName(w, szFA.BaseType); + dataParamType = abiStructName + "* data"; + dataCastType = "(" + abiStructName + "*)"; + } + w.Write(callIndent); + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToManaged\")]\n"); + w.Write(callIndent); + w.Write("static extern void CopyToManaged_"); + w.Write(localName); + w.Write("([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, szFA.BaseType, elementInteropArg)); + w.Write("\")] object _, uint length, "); + w.Write(dataParamType); + w.Write(", Span<"); + w.Write(elementProjected); + w.Write("> span);\n"); + w.Write(callIndent); + w.Write("CopyToManaged_"); + w.Write(localName); + w.Write("(null, (uint)__"); + w.Write(localName); + w.Write("_span.Length, "); + w.Write(dataCastType); + w.Write("_"); + w.Write(localName); + w.Write(", "); + w.Write(callName); + w.Write(");\n"); + } + + // After call: write back Out params to caller's 'out' var. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.Out) { continue; } + string callName = GetParamName(p, paramNameOverride); + string localName = GetParamLocalName(p, paramNameOverride); + AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); + + // For Out generic instance: emit inline UnsafeAccessor to ConvertToManaged_ + // before the writeback. Mirrors the truth pattern (e.g. Collection1HandlerInvoke + // emits the accessor inside try, right before the assignment). + if (IsGenericInstance(uOut)) + { + string interopTypeName = EncodeInteropTypeName(uOut, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, uOut, false))); + w.Write(callIndent); + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); + w.Write(callIndent); + w.Write("static extern "); + w.Write(projectedTypeName); + w.Write(" ConvertToManaged_"); + w.Write(localName); + w.Write("([UnsafeAccessorType(\""); + w.Write(interopTypeName); + w.Write("\")] object _, void* value);\n"); + w.Write(callIndent); + w.Write(callName); + w.Write(" = ConvertToManaged_"); + w.Write(localName); + w.Write("(null, __"); + w.Write(localName); + w.Write(");\n"); + continue; + } + + w.Write(callIndent); + w.Write(callName); + w.Write(" = "); + if (IsString(uOut)) + { + w.Write("HStringMarshaller.ConvertToManaged(__"); + w.Write(localName); + w.Write(")"); + } + else if (IsObject(uOut)) + { + w.Write("WindowsRuntimeObjectMarshaller.ConvertToManaged(__"); + w.Write(localName); + w.Write(")"); + } + else if (IsRuntimeClassOrInterface(uOut)) + { + w.Write(GetMarshallerFullName(w, uOut)); + w.Write(".ConvertToManaged(__"); + w.Write(localName); + w.Write(")"); + } + else if (IsSystemType(uOut)) + { + w.Write("global::ABI.System.TypeMarshaller.ConvertToManaged(__"); + w.Write(localName); + w.Write(")"); + } + else if (IsComplexStruct(uOut)) + { + w.Write(GetMarshallerFullName(w, uOut)); + w.Write(".ConvertToManaged(__"); + w.Write(localName); + w.Write(")"); + } + else if (IsAnyStruct(uOut)) + { + w.Write("__"); + w.Write(localName); + } + else if (uOut is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibBool && corlibBool.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) + { + w.Write("__"); + w.Write(localName); + } + else if (uOut is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibChar && corlibChar.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) + { + w.Write("__"); + w.Write(localName); + } + else if (IsEnumType(uOut)) + { + // Enum out param: __ local is already the projected enum type (since the + // function pointer signature uses the projected type). No cast needed. + w.Write("__"); + w.Write(localName); + } + else + { + w.Write("__"); + w.Write(localName); + } + w.Write(";\n"); + } + + // Writeback for ReceiveArray params: emit a UnsafeAccessor + assign to the out param. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.ReceiveArray) { continue; } + string callName = GetParamName(p, paramNameOverride); + string localName = GetParamLocalName(p, paramNameOverride); + AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(sza.BaseType)))); + // Element ABI type: void* for ref types (string/runtime class/object); ABI struct + // type for complex structs (e.g. authored BasicStruct); blittable struct ABI for + // blittable structs; primitive ABI otherwise. + string elementAbi = IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType) + ? "void*" + : IsComplexStruct(sza.BaseType) + ? GetAbiStructTypeName(w, sza.BaseType) + : IsAnyStruct(sza.BaseType) + ? GetBlittableStructAbiType(w, sza.BaseType) + : GetAbiPrimitiveType(sza.BaseType); + string elementInteropArg = EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); + string marshallerPath = GetArrayMarshallerInteropPath(w, sza.BaseType, elementInteropArg); + w.Write(callIndent); + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); + w.Write(callIndent); + w.Write("static extern "); + w.Write(elementProjected); + w.Write("[] ConvertToManaged_"); + w.Write(localName); + w.Write("([UnsafeAccessorType(\""); + w.Write(marshallerPath); + w.Write("\")] object _, uint length, "); + w.Write(elementAbi); + w.Write("* data);\n"); + w.Write(callIndent); + w.Write(callName); + w.Write(" = ConvertToManaged_"); + w.Write(localName); + w.Write("(null, __"); + w.Write(localName); + w.Write("_length, __"); + w.Write(localName); + w.Write("_data);\n"); + } + if (rt is not null) + { + if (returnIsReceiveArray) + { + AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)rt; + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(retSz.BaseType)))); + string elementAbi = IsString(retSz.BaseType) || IsRuntimeClassOrInterface(retSz.BaseType) || IsObject(retSz.BaseType) + ? "void*" + : IsComplexStruct(retSz.BaseType) + ? GetAbiStructTypeName(w, retSz.BaseType) + : IsHResultException(retSz.BaseType) + ? "global::ABI.System.Exception" + : IsMappedAbiValueType(retSz.BaseType) + ? GetMappedAbiTypeName(retSz.BaseType) + : IsAnyStruct(retSz.BaseType) + ? GetBlittableStructAbiType(w, retSz.BaseType) + : GetAbiPrimitiveType(retSz.BaseType); + string elementInteropArg = EncodeInteropTypeName(retSz.BaseType, TypedefNameType.Projected); + w.Write(callIndent); + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); + w.Write(callIndent); + w.Write("static extern "); + w.Write(elementProjected); + w.Write("[] ConvertToManaged_retval([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, retSz.BaseType, elementInteropArg)); + w.Write("\")] object _, uint length, "); + w.Write(elementAbi); + w.Write("* data);\n"); + w.Write(callIndent); + w.Write("return ConvertToManaged_retval(null, __retval_length, __retval_data);\n"); + } + else if (returnIsHResultException) + { + w.Write(callIndent); + w.Write("return global::ABI.System.ExceptionMarshaller.ConvertToManaged(__retval);\n"); + } + else if (returnIsString) + { + w.Write(callIndent); + w.Write("return HStringMarshaller.ConvertToManaged(__retval);\n"); + } + else if (returnIsRefType) + { + if (IsNullableT(rt)) + { + // Nullable return: use Marshaller.UnboxToManaged. Mirrors truth pattern; + // there is no NullableMarshaller, the inner-T marshaller has UnboxToManaged. + AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(rt)!; + string innerMarshaller = GetNullableInnerMarshallerName(w, inner); + w.Write(callIndent); + w.Write("return "); + w.Write(innerMarshaller); + w.Write(".UnboxToManaged(__retval);\n"); + } + else if (IsGenericInstance(rt)) + { + string interopTypeName = EncodeInteropTypeName(rt, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt, false))); + w.Write(callIndent); + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); + w.Write(callIndent); + w.Write("static extern "); + w.Write(projectedTypeName); + w.Write(" ConvertToManaged_retval([UnsafeAccessorType(\""); + w.Write(interopTypeName); + w.Write("\")] object _, void* value);\n"); + w.Write(callIndent); + w.Write("return ConvertToManaged_retval(null, __retval);\n"); + } + else + { + w.Write(callIndent); + w.Write("return "); + EmitMarshallerConvertToManaged(w, rt, "__retval"); + w.Write(";\n"); + } + } + else if (rt is not null && IsMappedAbiValueType(rt)) + { + // Mapped value type return (e.g. DateTime/TimeSpan): convert ABI struct back via marshaller. + w.Write(callIndent); + w.Write("return "); + w.Write(GetMappedMarshallerName(rt)); + w.Write(".ConvertToManaged(__retval);\n"); + } + else if (rt is not null && IsSystemType(rt)) + { + // System.Type return: convert ABI Type struct back to System.Type via TypeMarshaller. + w.Write(callIndent); + w.Write("return global::ABI.System.TypeMarshaller.ConvertToManaged(__retval);\n"); + } + else if (returnIsAnyStruct) + { + w.Write(callIndent); + if (rt is not null && IsMappedAbiValueType(rt)) + { + // Mapped value type return: convert ABI struct back to projected via marshaller. + w.Write("return "); + w.Write(GetMappedMarshallerName(rt)); + w.Write(".ConvertToManaged(__retval);\n"); + } + else + { + w.Write("return __retval;\n"); + } + } + else if (returnIsComplexStruct) + { + w.Write(callIndent); + w.Write("return "); + w.Write(GetMarshallerFullName(w, rt!)); + w.Write(".ConvertToManaged(__retval);\n"); + } + else + { + w.Write(callIndent); + w.Write("return "); + string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt!, false))); + string abiType = GetAbiPrimitiveType(rt!); + if (projected == abiType) { w.Write("__retval;\n"); } + else + { + w.Write("("); + w.Write(projected); + w.Write(")__retval;\n"); + } + } + } + + // Close fixed blocks (innermost first). + for (int i = fixedNesting - 1; i >= 0; i--) + { + w.Write(indent); + w.Write(new string(' ', i * 4)); + w.Write("}\n"); + } + + if (needsTryFinally) + { + w.Write(" }\n finally\n {\n"); + + // Order matches truth (mirrors C++ disposer iteration order): + // 0. Complex-struct input param Dispose (e.g. ProfileUsageMarshaller.Dispose(__value)) + // 1. Non-blittable PassArray/FillArray cleanup (Dispose + ArrayPools) + // 2. Out param frees (HString / object / runtime class) + // 3. ReceiveArray param frees (Free_ via UnsafeAccessor) + // 4. Return free (__retval) — last + + // 0. Dispose complex-struct input params via marshaller (both 'in' and 'in T' forms). + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.In && cat != ParamCategory.Ref) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature pType = StripByRefAndCustomModifiers(p.Type); + if (!IsComplexStruct(pType)) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + w.Write(" "); + w.Write(GetMarshallerFullName(w, pType)); + w.Write(".Dispose(__"); + w.Write(localName); + w.Write(");\n"); + } + // 1. Cleanup non-blittable PassArray/FillArray params: + // For strings: HStringArrayMarshaller.Dispose + return ArrayPools (3 of them). + // For runtime classes/objects: Dispose_ (UnsafeAccessor) + return ArrayPool. + // For mapped value types (DateTime/TimeSpan): no per-element disposal needed and truth + // doesn't return the ArrayPool either, so skip entirely. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + if (IsMappedAbiValueType(szArr.BaseType)) { continue; } + if (IsHResultException(szArr.BaseType)) + { + // HResultException ABI is just an int; per-element Dispose is a no-op (mirror + // the truth: no Dispose_ emitted). Just return the inline-array's pool + // using the correct element type (ABI.System.Exception, not nint). + string localNameH = GetParamLocalName(p, paramNameOverride); + w.Write("\n if (__"); + w.Write(localNameH); + w.Write("_arrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); + w.Write(localNameH); + w.Write("_arrayFromPool);\n }\n"); + continue; + } + string localName = GetParamLocalName(p, paramNameOverride); + if (IsString(szArr.BaseType)) + { + // The HStringArrayMarshaller.Dispose + ArrayPool returns for strings only + // apply to PassArray (where we set up the pinned handles + headers in the + // first place). FillArray writes back HSTRING handles into the nint storage + // array directly, with no per-element pinned handle / header to release. + if (cat == ParamCategory.PassArray) + { + w.Write(" HStringArrayMarshaller.Dispose(__"); + w.Write(localName); + w.Write("_pinnedHandleSpan);\n\n"); + w.Write(" if (__"); + w.Write(localName); + w.Write("_pinnedHandleArrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); + w.Write(localName); + w.Write("_pinnedHandleArrayFromPool);\n }\n\n"); + w.Write(" if (__"); + w.Write(localName); + w.Write("_headerArrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); + w.Write(localName); + w.Write("_headerArrayFromPool);\n }\n"); + } + // Both PassArray and FillArray need the inline-array's nint pool returned. + w.Write("\n if (__"); + w.Write(localName); + w.Write("_arrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); + w.Write(localName); + w.Write("_arrayFromPool);\n }\n"); + } + else + { + // For complex structs, both the Dispose_ data param and the fixed() + // pointer must be typed as *; the cast can be omitted. For + // runtime classes / objects / strings the data is void** and the fixed() + // remains void* with a (void**) cast. + string disposeDataParamType; + string fixedPtrType; + string disposeCastType; + if (IsComplexStruct(szArr.BaseType)) + { + string abiStructName = GetAbiStructTypeName(w, szArr.BaseType); + disposeDataParamType = abiStructName + "*"; + fixedPtrType = abiStructName + "*"; + disposeCastType = string.Empty; + } + else + { + disposeDataParamType = "void** data"; + fixedPtrType = "void*"; + disposeCastType = "(void**)"; + } + string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"Dispose\")]\n"); + w.Write(" static extern void Dispose_"); + w.Write(localName); + w.Write("([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); + w.Write("\")] object _, uint length, "); + w.Write(disposeDataParamType); + if (!disposeDataParamType.EndsWith("data", System.StringComparison.Ordinal)) { w.Write(" data"); } + w.Write(");\n\n"); + w.Write(" fixed("); + w.Write(fixedPtrType); + w.Write(" _"); + w.Write(localName); + w.Write(" = __"); + w.Write(localName); + w.Write("_span)\n {\n"); + w.Write(" Dispose_"); + w.Write(localName); + w.Write("(null, (uint) __"); + w.Write(localName); + w.Write("_span.Length, "); + w.Write(disposeCastType); + w.Write("_"); + w.Write(localName); + w.Write(");\n }\n"); + } + // ArrayPool storage type matches the InlineArray storage (mapped ABI value type + // for DateTime/TimeSpan; ABI struct for complex structs; nint otherwise). + string poolStorageT = IsMappedAbiValueType(szArr.BaseType) + ? GetMappedAbiTypeName(szArr.BaseType) + : IsComplexStruct(szArr.BaseType) + ? GetAbiStructTypeName(w, szArr.BaseType) + : "nint"; + w.Write("\n if (__"); + w.Write(localName); + w.Write("_arrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool<"); + w.Write(poolStorageT); + w.Write(">.Shared.Return(__"); + w.Write(localName); + w.Write("_arrayFromPool);\n }\n"); + } + + // 2. Free Out string/object/runtime-class params. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.Out) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); + string localName = GetParamLocalName(p, paramNameOverride); + if (IsString(uOut)) + { + w.Write(" HStringMarshaller.Free(__"); + w.Write(localName); + w.Write(");\n"); + } + else if (IsObject(uOut) || IsRuntimeClassOrInterface(uOut) || IsGenericInstance(uOut)) + { + w.Write(" WindowsRuntimeUnknownMarshaller.Free(__"); + w.Write(localName); + w.Write(");\n"); + } + else if (IsSystemType(uOut)) + { + w.Write(" global::ABI.System.TypeMarshaller.Dispose(__"); + w.Write(localName); + w.Write(");\n"); + } + else if (IsComplexStruct(uOut)) + { + w.Write(" "); + w.Write(GetMarshallerFullName(w, uOut)); + w.Write(".Dispose(__"); + w.Write(localName); + w.Write(");\n"); + } + } + + // 3. Free ReceiveArray params via UnsafeAccessor. + for (int i = 0; i < sig.Params.Count; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.ReceiveArray) { continue; } + string localName = GetParamLocalName(p, paramNameOverride); + AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); + // Element ABI type: void* for ref types; ABI struct for complex/blittable structs; + // primitive ABI otherwise. (Same categorization as the ConvertToManaged_ path.) + string elementAbi = IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType) + ? "void*" + : IsComplexStruct(sza.BaseType) + ? GetAbiStructTypeName(w, sza.BaseType) + : IsAnyStruct(sza.BaseType) + ? GetBlittableStructAbiType(w, sza.BaseType) + : GetAbiPrimitiveType(sza.BaseType); + string elementInteropArg = EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); + string marshallerPath = GetArrayMarshallerInteropPath(w, sza.BaseType, elementInteropArg); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"Free\")]\n"); + w.Write(" static extern void Free_"); + w.Write(localName); + w.Write("([UnsafeAccessorType(\""); + w.Write(marshallerPath); + w.Write("\")] object _, uint length, "); + w.Write(elementAbi); + w.Write("* data);\n\n"); + w.Write(" Free_"); + w.Write(localName); + w.Write("(null, __"); + w.Write(localName); + w.Write("_length, __"); + w.Write(localName); + w.Write("_data);\n"); + } + + // 4. Free return value (__retval) — emitted last to match truth ordering. + if (returnIsString) + { + w.Write(" HStringMarshaller.Free(__retval);\n"); + } + else if (returnIsRefType) + { + w.Write(" WindowsRuntimeUnknownMarshaller.Free(__retval);\n"); + } + else if (returnIsComplexStruct) + { + w.Write(" "); + w.Write(GetMarshallerFullName(w, rt!)); + w.Write(".Dispose(__retval);\n"); + } + else if (returnIsSystemTypeForCleanup) + { + // System.Type return: dispose the ABI.System.Type's HSTRING fields. + w.Write(" global::ABI.System.TypeMarshaller.Dispose(__retval);\n"); + } + else if (returnIsReceiveArray) + { + AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)rt!; + string elementAbi = IsString(retSz.BaseType) || IsRuntimeClassOrInterface(retSz.BaseType) || IsObject(retSz.BaseType) + ? "void*" + : IsComplexStruct(retSz.BaseType) + ? GetAbiStructTypeName(w, retSz.BaseType) + : IsHResultException(retSz.BaseType) + ? "global::ABI.System.Exception" + : IsMappedAbiValueType(retSz.BaseType) + ? GetMappedAbiTypeName(retSz.BaseType) + : IsAnyStruct(retSz.BaseType) + ? GetBlittableStructAbiType(w, retSz.BaseType) + : GetAbiPrimitiveType(retSz.BaseType); + string elementInteropArg = EncodeInteropTypeName(retSz.BaseType, TypedefNameType.Projected); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"Free\")]\n"); + w.Write(" static extern void Free_retval([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, retSz.BaseType, elementInteropArg)); + w.Write("\")] object _, uint length, "); + w.Write(elementAbi); + w.Write("* data);\n"); + w.Write(" Free_retval(null, __retval_length, __retval_data);\n"); + } + + w.Write(" }\n"); + } + + w.Write(" }\n"); + } + + /// True if the type signature is a Nullable<T> where T is a primitive + /// supported by an ABI.System.<T>Marshaller (e.g. UInt64Marshaller, Int32Marshaller, etc.). + /// Returns the fully-qualified marshaller name in . + private static bool TryGetNullablePrimitiveMarshallerName(AsmResolver.DotNet.Signatures.TypeSignature sig, out string? marshallerName) + { + marshallerName = null; + if (sig is not AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) { return false; } + var gt = gi.GenericType; + string ns = gt?.Namespace?.Value ?? string.Empty; + string name = gt?.Name?.Value ?? string.Empty; + // In WinMD metadata, Nullable is encoded as Windows.Foundation.IReference. + // It only later gets projected to System.Nullable by the projection layer. + bool isNullable = (ns == "System" && name == "Nullable`1") + || (ns == "Windows.Foundation" && name == "IReference`1"); + if (!isNullable) { return false; } + if (gi.TypeArguments.Count != 1) { return false; } + AsmResolver.DotNet.Signatures.TypeSignature arg = gi.TypeArguments[0]; + // Map primitive corlib element type to its ABI marshaller name. + if (arg is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) + { + string? mn = corlib.ElementType switch + { + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean => "Boolean", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char => "Char", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 => "SByte", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 => "Byte", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 => "Int16", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 => "UInt16", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 => "Int32", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 => "UInt32", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 => "Int64", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 => "UInt64", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4 => "Single", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8 => "Double", + _ => null + }; + if (mn is null) { return false; } + marshallerName = "ABI.System." + mn + "Marshaller"; + return true; + } + return false; + } + + /// True if the type signature represents the System.Object root type. + private static bool IsObject(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + return sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && + corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object; + } + + /// True if the type signature represents Windows.Foundation.HResult / System.Exception + /// (special-cased: ABI is global::ABI.System.Exception (an HResult struct), projected is Exception, + /// requires custom marshalling via ABI.System.ExceptionMarshaller). + private static bool IsHResultException(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is not AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) { return false; } + string ns = td.Type?.Namespace?.Value ?? string.Empty; + string name = td.Type?.Name?.Value ?? string.Empty; + return (ns == "System" && name == "Exception") + || (ns == "Windows.Foundation" && name == "HResult"); + } + + /// + /// True if the type is a mapped value type that requires marshalling between projected and ABI + /// representations (e.g. Windows.Foundation.DateTime <-> System.DateTimeOffset, + /// Windows.Foundation.TimeSpan <-> System.TimeSpan, Windows.Foundation.HResult <-> System.Exception). + /// These types use 'global::ABI.<MappedNamespace>.<MappedName>' as their ABI representation + /// and need an explicit marshaller call ('global::ABI.<MappedNamespace>.<MappedName>Marshaller.ConvertToUnmanaged'/ + /// 'ConvertToManaged') to convert values across the boundary. + /// + private static bool IsMappedMarshalingValueType(AsmResolver.DotNet.Signatures.TypeSignature sig, out string mappedNs, out string mappedName) + { + mappedNs = string.Empty; + mappedName = string.Empty; + AsmResolver.DotNet.ITypeDefOrRef? td = null; + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature tds) { td = tds.Type; } + if (td is null) { return false; } + string ns = td.Namespace?.Value ?? string.Empty; + string name = td.Name?.Value ?? string.Empty; + // The set of mapped types that use the 'value-type marshaller' pattern (DateTime, TimeSpan, HResult). + // Uri is also a mapped marshalling type but it's a reference type (handled via UriMarshaller separately). + if (ns == "Windows.Foundation") + { + if (name == "DateTime") { mappedNs = "System"; mappedName = "DateTimeOffset"; return true; } + if (name == "TimeSpan") { mappedNs = "System"; mappedName = "TimeSpan"; return true; } + if (name == "HResult") { mappedNs = "System"; mappedName = "Exception"; return true; } + } + return false; + } + + /// True if the type is a mapped value type that needs ABI marshalling (excluding HResult, handled separately). + private static bool IsMappedAbiValueType(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (!IsMappedMarshalingValueType(sig, out _, out string mappedName)) { return false; } + // HResult/Exception is treated specially in many places; this helper is for DateTime/TimeSpan only. + return mappedName != "Exception"; + } + + /// Returns the ABI type name for a mapped value type (e.g. 'global::ABI.System.TimeSpan'). + private static string GetMappedAbiTypeName(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (!IsMappedMarshalingValueType(sig, out string ns, out string name)) { return string.Empty; } + return "global::ABI." + ns + "." + name; + } + + /// Returns the marshaller class name for a mapped value type (e.g. 'global::ABI.System.TimeSpanMarshaller'). + private static string GetMappedMarshallerName(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (!IsMappedMarshalingValueType(sig, out string ns, out string name)) { return string.Empty; } + return "global::ABI." + ns + "." + name + "Marshaller"; + } + + /// True if the type signature represents an enum (resolves cross-module typerefs). + private static bool IsEnumType(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is not AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) { return false; } + if (td.Type is TypeDefinition def) + { + return TypeCategorization.GetCategory(def) == TypeCategory.Enum; + } + if (td.Type is TypeReference tr && _cacheRef is not null) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + TypeDefinition? resolved = _cacheRef.Find(ns + "." + name); + return resolved is not null && TypeCategorization.GetCategory(resolved) == TypeCategory.Enum; + } + return false; + } + + /// True if the type signature represents a generic instantiation that needs WinRT.Interop UnsafeAccessor marshalling. + private static bool IsGenericInstance(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + return sig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature; + } + + /// True if the signature is a WinRT IReference<T> (which projects to Nullable<T>). + private static bool IsNullableT(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is not AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) { return false; } + string ns = gi.GenericType?.Namespace?.Value ?? string.Empty; + string name = gi.GenericType?.Name?.Value ?? string.Empty; + return (ns == "Windows.Foundation" && name == "IReference`1") + || (ns == "System" && name == "Nullable`1"); + } + + /// Returns the inner type argument of a Nullable<T> signature (or the IReference variant). + private static AsmResolver.DotNet.Signatures.TypeSignature? GetNullableInnerType(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi && gi.TypeArguments.Count == 1) + { + return gi.TypeArguments[0]; + } + return null; + } + + /// Returns the marshaller name for the inner type T of Nullable<T>. + /// Mirrors the truth pattern: e.g. for Nullable<DateTimeOffset> returns + /// global::ABI.System.DateTimeOffsetMarshaller; for primitives like Nullable<int> + /// returns global::ABI.System.Int32Marshaller. + private static string GetNullableInnerMarshallerName(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature innerType) + { + // Primitives (Int32, Int64, Boolean, etc.) live in ABI.System with the canonical .NET name. + if (innerType is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) + { + string typeName = corlib.ElementType switch + { + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean => "Boolean", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char => "Char", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 => "SByte", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 => "Byte", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 => "Int16", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 => "UInt16", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 => "Int32", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 => "UInt32", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 => "Int64", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 => "UInt64", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4 => "Single", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8 => "Double", + _ => "", + }; + if (!string.IsNullOrEmpty(typeName)) + { + return "global::ABI.System." + typeName + "Marshaller"; + } + } + // For non-primitive types (DateTimeOffset, TimeSpan, struct/enum types), use GetMarshallerFullName. + return GetMarshallerFullName(w, innerType); + } + + /// Strips ByReferenceTypeSignature and CustomModifierTypeSignature wrappers + /// to get the underlying type signature. + private static AsmResolver.DotNet.Signatures.TypeSignature StripByRefAndCustomModifiers(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + AsmResolver.DotNet.Signatures.TypeSignature current = sig; + while (true) + { + if (current is AsmResolver.DotNet.Signatures.ByReferenceTypeSignature br) { current = br.BaseType; continue; } + if (current is AsmResolver.DotNet.Signatures.CustomModifierTypeSignature cm) { current = cm.BaseType; continue; } + return current; + } + } + + /// True if the type signature represents a WinRT runtime class, interface, or delegate (reference type marshallable via *Marshaller). + private static bool IsRuntimeClassOrInterface(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) + { + // Same-module: use the resolved category directly. + if (td.Type is TypeDefinition def) + { + TypeCategory cat = TypeCategorization.GetCategory(def); + return cat is TypeCategory.Class or TypeCategory.Interface or TypeCategory.Delegate; + } + // Cross-module typeref: try to resolve via the metadata cache to check category. + string ns = td.Type?.Namespace?.Value ?? string.Empty; + string name = td.Type?.Name?.Value ?? string.Empty; + if (ns == "System") + { + return name switch + { + "Uri" or "Type" or "IDisposable" or "Exception" => true, + _ => false, + }; + } + if (_cacheRef is not null) + { + TypeDefinition? resolved = _cacheRef.Find(ns + "." + name); + if (resolved is not null) + { + TypeCategory cat = TypeCategorization.GetCategory(resolved); + return cat is TypeCategory.Class or TypeCategory.Interface or TypeCategory.Delegate; + } + } + // Unresolved cross-assembly TypeRef (e.g. a referenced winmd we don't have loaded). + // Fall back to the signature's encoding: WinRT metadata distinguishes value types + // (encoded as ValueType) from reference types (encoded as Class). If the signature + // has IsValueType == false, then it MUST be one of class/interface/delegate (since + // primitives/enums/strings/object are encoded with their own element type). This + // mirrors how the C++ tool's abi_marshaler abstraction handles unknown types — it + // dispatches based on the metadata semantics, not on resolution. + return !td.IsValueType; + } + return false; + } + + /// Emits the call to the appropriate marshaller's ConvertToUnmanaged for a runtime class / object input parameter. + private static void EmitMarshallerConvertToUnmanaged(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig, string argName) + { + if (IsObject(sig)) + { + w.Write("WindowsRuntimeObjectMarshaller.ConvertToUnmanaged("); + w.Write(argName); + w.Write(")"); + return; + } + // Runtime class / interface: use ABI..Marshaller + w.Write(GetMarshallerFullName(w, sig)); + w.Write(".ConvertToUnmanaged("); + w.Write(argName); + w.Write(")"); + } + + /// Emits the call to the appropriate marshaller's ConvertToManaged for a runtime class / object return value. + private static void EmitMarshallerConvertToManaged(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig, string argName) + { + if (IsObject(sig)) + { + w.Write("WindowsRuntimeObjectMarshaller.ConvertToManaged("); + w.Write(argName); + w.Write(")"); + return; + } + w.Write(GetMarshallerFullName(w, sig)); + w.Write(".ConvertToManaged("); + w.Write(argName); + w.Write(")"); + } + + /// Returns the full marshaller name (e.g. global::ABI.Windows.Foundation.UriMarshaller). + /// When the marshaller would land in the writer's current ABI namespace, returns just the + /// short marshaller class name (e.g. BasicStructMarshaller) — mirrors C++ which + /// elides the qualifier in same-namespace contexts. + private static string GetMarshallerFullName(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) + { + string ns = td.Type?.Namespace?.Value ?? string.Empty; + string name = td.Type?.Name?.Value ?? string.Empty; + // Apply mapped type remapping (e.g. System.Uri -> Windows.Foundation.Uri) + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + string nameStripped = Helpers.StripBackticks(name); + // If the writer is currently in the matching ABI namespace, drop the qualifier. + if (w.InAbiNamespace && string.Equals(w.CurrentNamespace, ns, System.StringComparison.Ordinal)) + { + return nameStripped + "Marshaller"; + } + return "global::ABI." + ns + "." + nameStripped + "Marshaller"; + } + return "global::ABI.Object.Marshaller"; + } + + private static string GetParamName(ParamInfo p, string? paramNameOverride) + { + string name = paramNameOverride ?? p.Parameter.Name ?? "param"; + return Helpers.IsKeyword(name) ? "@" + name : name; + } + + private static string GetParamLocalName(ParamInfo p, string? paramNameOverride) + { + // For local helper variables (e.g. __), strip the @ escape since `__event` is valid. + return paramNameOverride ?? p.Parameter.Name ?? "param"; + } + + private static bool IsString(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + return sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && + corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String; + } + + /// True if the type signature is System.Type (or a TypeRef/TypeSpec resolving to it, + /// or the WinRT Windows.UI.Xaml.Interop.TypeName struct that's mapped to it). + private static bool IsSystemType(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) + { + string ns = td.Type?.Namespace?.Value ?? string.Empty; + string name = td.Type?.Name?.Value ?? string.Empty; + if (ns == "System" && name == "Type") { return true; } + // The WinMD source type for System.Type is Windows.UI.Xaml.Interop.TypeName. + if (ns == "Windows.UI.Xaml.Interop" && name == "TypeName") { return true; } + } + return false; + } + + /// Emits the conversion of a parameter from its projected (managed) form to the ABI argument form. + private static void EmitParamArgConversion(TypeWriter w, ParamInfo p, string? paramNameOverride = null) + { + string pname = paramNameOverride ?? p.Parameter.Name ?? "param"; + // bool: ABI is 'bool' directly; pass as-is. + if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && + corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) + { + w.Write(pname); + } + // char: ABI is 'char' directly; pass as-is. + else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib2 && + corlib2.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) + { + w.Write(pname); + } + // Enums: function pointer signature uses the projected enum type, so pass directly. + else if (IsEnumType(p.Type)) + { + w.Write(pname); + } + else + { + w.Write(pname); + } + } + + /// True if the type is a blittable primitive (or enum) directly representable + /// at the ABI: bool/byte/sbyte/short/ushort/int/uint/long/ulong/float/double/char and enums. + private static bool IsBlittablePrimitive(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) + { + return corlib.ElementType is + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8 or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char; + } + // Enum (TypeDefOrRef-based value type with non-Object base) - same module or cross-module + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) + { + if (td.Type is TypeDefinition def && TypeCategorization.GetCategory(def) == TypeCategory.Enum) + { + return true; + } + // Cross-module enum: try to resolve via the metadata cache. + if (td.Type is TypeReference tr && _cacheRef is not null) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + TypeDefinition? resolved = _cacheRef.Find(ns + "." + name); + if (resolved is not null && TypeCategorization.GetCategory(resolved) == TypeCategory.Enum) + { + return true; + } + } + } + return false; + } + + /// True for any struct type that can be passed directly across the WinRT ABI + /// (no per-field marshalling required). This includes blittable structs and "almost-blittable" + /// structs that have only primitive fields like bool/char (whose C# layout matches the WinRT ABI). + /// Excludes structs with reference type fields (string/object/runtime classes/etc.). + /// True for structs that have at least one reference type field (string, generic + /// instance Nullable<T>, etc.). These need per-field marshalling via the *Marshaller class + /// (ConvertToUnmanaged/ConvertToManaged/Dispose). + private static bool IsComplexStruct(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is not AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) { return false; } + TypeDefinition? def = td.Type as TypeDefinition; + if (def is null && _cacheRef is not null && td.Type is TypeReference tr) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + if (ns == "System" && name == "Guid") { return false; } + def = _cacheRef.Find(ns + "." + name); + } + if (def is null) { return false; } + TypeCategory cat = TypeCategorization.GetCategory(def); + if (cat != TypeCategory.Struct) { return false; } + // Mirror C++ is_type_blittable: mapped struct types short-circuit based on + // RequiresMarshaling, regardless of inner field layout. So for mapped types like + // Duration, KeyTime, RepeatBehavior (RequiresMarshaling=false), they're never "complex". + { + string sNs = td.Type?.Namespace?.Value ?? string.Empty; + string sName = td.Type?.Name?.Value ?? string.Empty; + MappedType? sMapped = MappedTypes.Get(sNs, sName); + if (sMapped is not null) { return false; } + } + // A struct is "complex" if it has any field that is not a blittable primitive nor an + // almost-blittable struct (i.e. has a string/object/Nullable/etc. field). + foreach (FieldDefinition field in def.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; + if (IsBlittablePrimitive(ft)) { continue; } + if (IsAnyStruct(ft)) { continue; } + return true; + } + return false; + } + + private static bool IsAnyStruct(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is not AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) { return false; } + TypeDefinition? def = td.Type as TypeDefinition; + if (def is null && _cacheRef is not null && td.Type is TypeReference trEarly) + { + string ns = trEarly.Namespace?.Value ?? string.Empty; + string name = trEarly.Name?.Value ?? string.Empty; + if (ns == "System" && name == "Guid") { return true; } + def = _cacheRef.Find(ns + "." + name); + } + if (def is null) { return false; } + // Special case: mapped struct types short-circuit based on RequiresMarshaling, mirroring + // C++ is_type_blittable: 'auto mapping = get_mapped_type(...); return !mapping->requires_marshaling'. + // Only applies to actual structs (not mapped interfaces like IAsyncAction). + if (TypeCategorization.GetCategory(def) == TypeCategory.Struct) + { + string sNs = td.Type?.Namespace?.Value ?? string.Empty; + string sName = td.Type?.Name?.Value ?? string.Empty; + MappedType? sMapped = MappedTypes.Get(sNs, sName); + if (sMapped is not null) { return !sMapped.RequiresMarshaling; } + } + TypeCategory cat = TypeCategorization.GetCategory(def); + if (cat != TypeCategory.Struct) { return false; } + // Reject if any instance field is a reference type (string/object/runtime class/etc.). + foreach (FieldDefinition field in def.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; + if (ft is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibField) + { + if (corlibField.ElementType is + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String or + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object) + { return false; } + continue; + } + // Recurse: nested struct must also pass IsAnyStruct, otherwise reject. + if (IsBlittablePrimitive(ft)) { continue; } + if (IsAnyStruct(ft)) { continue; } + return false; + } + return true; + } + + /// Returns the ABI type name for a blittable struct (the projected type name). + private static string GetBlittableStructAbiType(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig) + { + // Mapped value types (DateTime/TimeSpan) use the ABI type, not the projected type. + if (IsMappedAbiValueType(sig)) { return GetMappedAbiTypeName(sig); } + return w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, sig, false))); + } + + /// Returns the ABI struct type name for a complex struct (e.g. global::ABI.Windows.Web.Http.HttpProgress). + /// When the writer is currently in the matching ABI namespace, returns just the + /// short type name (e.g. HttpProgress) to mirror the C++ tool which uses the + /// unqualified name in same-namespace contexts. + private static string GetAbiStructTypeName(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) + { + string ns = td.Type?.Namespace?.Value ?? string.Empty; + string name = td.Type?.Name?.Value ?? string.Empty; + // If this struct is mapped, use the mapped namespace+name (e.g. + // 'Windows.UI.Xaml.Interop.TypeName' is mapped to 'System.Type', so the ABI struct + // is 'global::ABI.System.Type', not 'global::ABI.Windows.UI.Xaml.Interop.TypeName'). + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + string nameStripped = Helpers.StripBackticks(name); + // If the writer is currently in the matching ABI namespace, drop the qualifier. + if (w.InAbiNamespace && string.Equals(w.CurrentNamespace, ns, System.StringComparison.Ordinal)) + { + return nameStripped; + } + return "global::ABI." + ns + "." + nameStripped; + } + return "global::ABI.Object"; + } + + private static string GetAbiPrimitiveType(AsmResolver.DotNet.Signatures.TypeSignature sig) + { + if (sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) + { + return corlib.ElementType switch + { + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean => "bool", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char => "char", + _ => GetAbiFundamentalTypeFromCorLib(corlib.ElementType), + }; + } + // Enum: use the projected enum type as the ABI signature (truth pattern). + if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) + { + TypeDefinition? def = td.Type as TypeDefinition; + if (def is null && _cacheRef is not null && td.Type is TypeReference tr) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + def = _cacheRef.Find(ns + "." + name); + } + if (def is not null && TypeCategorization.GetCategory(def) == TypeCategory.Enum) + { + return _cacheRef is null ? "int" : GetProjectedEnumName(def); + } + } + return "int"; + } + + private static string GetProjectedEnumName(TypeDefinition def) + { + string ns = def.Namespace?.Value ?? string.Empty; + string name = def.Name?.Value ?? string.Empty; + // Apply mapped-type translation so consumers see the projected (.NET) enum name + // (e.g. Windows.UI.Xaml.Interop.NotifyCollectionChangedAction → + // System.Collections.Specialized.NotifyCollectionChangedAction). Mirrors the same + // remapping that WriteTypedefName performs. + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + return string.IsNullOrEmpty(ns) ? "global::" + name : "global::" + ns + "." + name; + } + + private static string GetAbiFundamentalTypeFromCorLib(AsmResolver.PE.DotNet.Metadata.Tables.ElementType et) + { + return et switch + { + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 => "sbyte", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 => "byte", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 => "short", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 => "ushort", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 => "int", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 => "uint", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 => "long", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 => "ulong", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4 => "float", + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8 => "double", + _ => "int", + }; + } + + /// + /// Writes the IReference<T> implementation for a struct/enum/delegate + /// (mirrors C++ write_reference_impl). + /// + private static void WriteReferenceImpl(TypeWriter w, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = Helpers.StripBackticks(name); + string visibility = w.Settings.Component ? "public" : "file"; + bool blittable = IsTypeBlittable(type); + + w.Write("\n"); + w.Write(visibility); + w.Write(" static unsafe class "); + w.Write(nameStripped); + w.Write("ReferenceImpl\n{\n"); + w.Write(" [FixedAddressValueType]\n"); + w.Write(" private static readonly ReferenceVftbl Vftbl;\n\n"); + w.Write(" static "); + w.Write(nameStripped); + w.Write("ReferenceImpl()\n {\n"); + w.Write(" *(IInspectableVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IInspectableVftbl*)IInspectableImpl.Vtable;\n"); + w.Write(" Vftbl.get_Value = &get_Value;\n"); + w.Write(" }\n\n"); + w.Write(" public static nint Vtable\n {\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => (nint)Unsafe.AsPointer(in Vftbl);\n }\n\n"); + w.Write(" [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]\n"); + bool isBlittableStructType = blittable && TypeCategorization.GetCategory(type) == TypeCategory.Struct; + bool isNonBlittableStructType = !blittable && TypeCategorization.GetCategory(type) == TypeCategory.Struct; + if ((blittable && TypeCategorization.GetCategory(type) != TypeCategory.Struct) + || isBlittableStructType) + { + // For blittable types and blittable structs: direct memcpy via C# struct assignment. + // Even bool/char fields work because their managed layout (1 byte / 2 bytes) matches + // the WinRT ABI. + w.Write(" public static int get_Value(void* thisPtr, void* result)\n {\n"); + w.Write(" if (result is null)\n {\n"); + w.Write(" return unchecked((int)0x80004003);\n }\n\n"); + w.Write(" try\n {\n"); + w.Write(" var value = ("); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(")(ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr));\n"); + w.Write(" *("); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write("*)result = value;\n"); + w.Write(" return 0;\n }\n"); + w.Write(" catch (Exception e)\n {\n"); + w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e);\n }\n"); + w.Write(" }\n"); + } + else if (isNonBlittableStructType) + { + // Non-blittable struct: marshal via Marshaller.ConvertToUnmanaged then write the + // (ABI) struct value into the result pointer. Mirrors C++ write_reference_impl which + // emits 'unboxedValue = (T)...; value = TMarshaller.ConvertToUnmanaged(unboxedValue); + // *(ABIT*)result = value;'. + w.Write(" public static int get_Value(void* thisPtr, void* result)\n {\n"); + w.Write(" if (result is null)\n {\n"); + w.Write(" return unchecked((int)0x80004003);\n }\n\n"); + w.Write(" try\n {\n"); + w.Write(" "); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(" unboxedValue = ("); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(")ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr);\n"); + w.Write(" "); + WriteTypedefName(w, type, TypedefNameType.ABI, false); + w.Write(" value = "); + w.Write(nameStripped); + w.Write("Marshaller.ConvertToUnmanaged(unboxedValue);\n"); + w.Write(" *("); + WriteTypedefName(w, type, TypedefNameType.ABI, false); + w.Write("*)result = value;\n"); + w.Write(" return 0;\n }\n"); + w.Write(" catch (Exception e)\n {\n"); + w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e);\n }\n"); + w.Write(" }\n"); + } + else if (TypeCategorization.GetCategory(type) is TypeCategory.Class or TypeCategory.Delegate) + { + // Non-blittable runtime class / delegate: marshal via Marshaller and detach. + w.Write(" public static int get_Value(void* thisPtr, void* result)\n {\n"); + w.Write(" if (result is null)\n {\n"); + w.Write(" return unchecked((int)0x80004003);\n }\n\n"); + w.Write(" try\n {\n"); + w.Write(" "); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(" unboxedValue = ("); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + w.Write(")ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr);\n"); + w.Write(" void* value = "); + // Use the same-namespace short marshaller name (we're in the ABI namespace). + w.Write(nameStripped); + w.Write("Marshaller.ConvertToUnmanaged(unboxedValue).DetachThisPtrUnsafe();\n"); + w.Write(" *(void**)result = value;\n"); + w.Write(" return 0;\n }\n"); + w.Write(" catch (Exception e)\n {\n"); + w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e);\n }\n"); + w.Write(" }\n"); + } + else + { + // Unreachable: WriteReferenceImpl is only called for enum/struct/delegate types + // (WriteAbiEnum / WriteAbiStruct / WriteAbiDelegate dispatchers). Enums are blittable + // (handled by the first branch), structs by the first/second branches, delegates by + // the third. Defensive: emit a runtime assertion in case a future caller dispatches + // for an unsupported category. + throw new System.InvalidOperationException( + $"WriteReferenceImpl: unsupported type category {TypeCategorization.GetCategory(type)} " + + $"for type '{type.FullName}'. Expected enum/struct/delegate."); + } + // IID property: matches C++ write_reference_impl, which appends a 'public static ref readonly Guid IID' + // property pointing at the reference type's IID (e.g. IID_Windows_AI_Actions_ActionEntityKindReference). + w.Write("\n public static ref readonly Guid IID\n {\n"); + w.Write(" [MethodImpl(MethodImplOptions.AggressiveInlining)]\n"); + w.Write(" get => ref global::ABI.InterfaceIIDs."); + WriteIidReferenceGuidPropertyName(w, type); + w.Write(";\n }\n"); + w.Write("}\n\n"); + } + + /// Mirrors C++ write_abi_type: writes the ABI type for a type semantics. + public static void WriteAbiType(TypeWriter w, TypeSemantics semantics) + { + switch (semantics) + { + case TypeSemantics.Fundamental f: + w.Write(GetAbiFundamentalType(f.Type)); + break; + case TypeSemantics.Object_: + w.Write("void*"); + break; + case TypeSemantics.Guid_: + w.Write("Guid"); + break; + case TypeSemantics.Type_: + w.Write("global::WindowsRuntime.InteropServices.WindowsRuntimeTypeName"); + break; + case TypeSemantics.Definition d: + if (TypeCategorization.GetCategory(d.Type) is TypeCategory.Enum) + { + // Enums in WinRT ABI use the projected enum type directly (since their C# + // layout matches their underlying integer ABI representation 1:1). + WriteTypedefName(w, d.Type, TypedefNameType.Projected, true); + } + else if (TypeCategorization.GetCategory(d.Type) is TypeCategory.Struct) + { + string dNs = d.Type.Namespace?.Value ?? string.Empty; + string dName = d.Type.Name?.Value ?? string.Empty; + // Special case: mapped value types that require ABI marshalling + // (DateTime/TimeSpan -> ABI.System.DateTimeOffset/TimeSpan). + if (dNs == "Windows.Foundation" && dName == "DateTime") + { + w.Write("global::ABI.System.DateTimeOffset"); + break; + } + if (dNs == "Windows.Foundation" && dName == "TimeSpan") + { + w.Write("global::ABI.System.TimeSpan"); + break; + } + if (dNs == "Windows.Foundation" && dName == "HResult") + { + w.Write("global::ABI.System.Exception"); + break; + } + if (dNs == "Windows.UI.Xaml.Interop" && dName == "TypeName") + { + // System.Type ABI struct: maps to global::ABI.System.Type, not the + // ABI.Windows.UI.Xaml.Interop.TypeName form. + w.Write("global::ABI.System.Type"); + break; + } + AsmResolver.DotNet.Signatures.TypeSignature dts = d.Type.ToTypeSignature(); + // "Almost-blittable" structs (with bool/char fields but no reference-type + // fields) can pass through using the projected type since the C# layout + // matches the WinRT ABI directly. Truly complex structs (with string/object/ + // Nullable fields) need the ABI struct. + if (IsAnyStruct(dts)) + { + WriteTypedefName(w, d.Type, TypedefNameType.Projected, true); + } + else + { + WriteTypedefName(w, d.Type, TypedefNameType.ABI, true); + } + } + else + { + w.Write("void*"); + } + break; + case TypeSemantics.Reference r: + // Cross-module typeref: try resolving the type, applying mapped-type translation + // for the field/parameter type after resolution. + if (_cacheRef is not null) + { + string rns = r.Reference_.Namespace?.Value ?? string.Empty; + string rname = r.Reference_.Name?.Value ?? string.Empty; + // Special case: mapped value types that require ABI marshalling. + if (rns == "Windows.Foundation" && rname == "DateTime") + { + w.Write("global::ABI.System.DateTimeOffset"); + break; + } + if (rns == "Windows.Foundation" && rname == "TimeSpan") + { + w.Write("global::ABI.System.TimeSpan"); + break; + } + if (rns == "Windows.Foundation" && rname == "HResult") + { + w.Write("global::ABI.System.Exception"); + break; + } + // Look up the type by its ORIGINAL (unmapped) name in the cache. + TypeDefinition? rd = _cacheRef.Find(rns + "." + rname); + // If not found, try the mapped name (for cases where the mapping target is in the cache). + if (rd is null) + { + MappedType? rmapped = MappedTypes.Get(rns, rname); + if (rmapped is not null) + { + rd = _cacheRef.Find(rmapped.MappedNamespace + "." + rmapped.MappedName); + } + } + if (rd is not null) + { + TypeCategory cat = TypeCategorization.GetCategory(rd); + if (cat == TypeCategory.Enum) + { + // Enums use the projected enum type directly (C# layout == ABI layout). + WriteTypedefName(w, rd, TypedefNameType.Projected, true); + break; + } + if (cat == TypeCategory.Struct) + { + // Special case: HResult is mapped to System.Exception (a reference type) + // but its ABI representation is the global::ABI.System.Exception struct + // (which wraps the underlying HRESULT int). + string rdNs = rd.Namespace?.Value ?? string.Empty; + string rdName = rd.Name?.Value ?? string.Empty; + if (rdNs == "Windows.Foundation" && rdName == "HResult") + { + w.Write("global::ABI.System.Exception"); + break; + } + if (IsAnyStruct(rd.ToTypeSignature())) + { + WriteTypedefName(w, rd, TypedefNameType.Projected, true); + } + else + { + WriteTypedefName(w, rd, TypedefNameType.ABI, true); + } + break; + } + } + } + // Unresolved cross-assembly TypeRef. If the signature was encoded as a value type + // (e.g. WindowId from Microsoft.UI.winmd when that winmd isn't loaded), assume it's + // a blittable struct and emit the projected type name — the consumer's compiler + // will resolve it via their own references. Otherwise (encoded as Class) emit + // void* (it's a runtime class/interface/delegate). + if (r.IsValueType) + { + string rns = r.Reference_.Namespace?.Value ?? string.Empty; + string rname = r.Reference_.Name?.Value ?? string.Empty; + w.Write("global::"); + if (!string.IsNullOrEmpty(rns)) { w.Write(rns); w.Write("."); } + w.Write(Helpers.StripBackticks(rname)); + break; + } + w.Write("void*"); + break; + case TypeSemantics.GenericInstance: + w.Write("void*"); + break; + default: + w.Write("void*"); + break; + } + } + + private static string GetAbiFundamentalType(FundamentalType t) => t switch + { + FundamentalType.Boolean => "bool", + FundamentalType.Char => "char", + FundamentalType.String => "void*", + _ => FundamentalTypes.ToCSharpType(t) + }; +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Class.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Class.cs new file mode 100644 index 0000000000..9885fb6823 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Class.cs @@ -0,0 +1,708 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Class emission helpers, mirroring functions in code_writers.h. +/// +internal static partial class CodeWriters +{ + /// Mirrors C++ is_fast_abi_class. + public static bool IsFastAbiClass(TypeDefinition type, Settings settings) + { + // Fast ABI is enabled when the type is marked [FastAbi] and netstandard_compat is off + // (CsWinRT 3.0 always has netstandard_compat = false, but we keep the gate for fidelity). + return !settings.NetstandardCompat && + TypeCategorization.HasAttribute(type, "Windows.Foundation.Metadata", "FastAbiAttribute"); + } + + /// Mirrors C++ write_class_modifiers. + public static void WriteClassModifiers(TypeWriter w, TypeDefinition type) + { + if (TypeCategorization.IsStatic(type)) + { + w.Write("static "); + return; + } + if (type.IsSealed) + { + w.Write("sealed "); + } + } + + /// + /// Returns the fast-abi class type for if the interface is + /// exclusive_to a class marked [FastAbi]; otherwise null. Mirrors C++ + /// find_fast_abi_class_type in helpers.h. + /// + public static TypeDefinition? FindFastAbiClassType(TypeDefinition iface) + { + if (_cacheRef is null) { return null; } + TypeDefinition? exclusiveToClass = GetExclusiveToType(iface); + if (exclusiveToClass is null) { return null; } + if (!IsFastAbiClass(exclusiveToClass, GetSettings(iface))) { return null; } + return exclusiveToClass; + } + + /// + /// Returns the fast-abi class info (class type + default interface + sorted other exclusive + /// interfaces) for , if the interface is exclusive_to a fast-abi + /// class; otherwise null. Mirrors C++ get_fast_abi_class_for_interface. + /// + public static (TypeDefinition Class, TypeDefinition? Default, System.Collections.Generic.List Others)? GetFastAbiClassForInterface(TypeDefinition iface) + { + TypeDefinition? cls = FindFastAbiClassType(iface); + if (cls is null) { return null; } + (TypeDefinition? def, System.Collections.Generic.List others) = GetFastAbiInterfaces(cls); + return (cls, def, others); + } + + /// + /// Whether is a non-default exclusive interface of a fast-abi class + /// (i.e. its members are merged into the default interface's vtable and dispatched through + /// the default interface's ABI Methods class). Mirrors C++ fast_abi_class::contains_other_interface. + /// + public static bool IsFastAbiOtherInterface(TypeDefinition iface) + { + var fastAbi = GetFastAbiClassForInterface(iface); + if (fastAbi is null) { return false; } + if (fastAbi.Value.Default is not null && InterfacesEqual(fastAbi.Value.Default, iface)) { return false; } + foreach (TypeDefinition other in fastAbi.Value.Others) + { + if (InterfacesEqual(other, iface)) { return true; } + } + return false; + } + + /// + /// Returns true if is the default interface of a fast-abi class. + /// + public static bool IsFastAbiDefaultInterface(TypeDefinition iface) + { + var fastAbi = GetFastAbiClassForInterface(iface); + if (fastAbi is null) { return false; } + return fastAbi.Value.Default is not null && InterfacesEqual(fastAbi.Value.Default, iface); + } + + private static bool InterfacesEqual(TypeDefinition a, TypeDefinition b) + { + if (a == b) { return true; } + return (a.Namespace?.Value ?? string.Empty) == (b.Namespace?.Value ?? string.Empty) + && (a.Name?.Value ?? string.Empty) == (b.Name?.Value ?? string.Empty); + } + + // We don't have direct access to the active Settings from a static helper that only takes + // a TypeDefinition. The fast-abi flag is purely determined by the [FastAbiAttribute] (the + // netstandard_compat gate is always false in CsWinRT 3.0). Pass an empty Settings stand-in + // so the IsFastAbiClass check evaluates only the attribute presence. + private static Settings GetSettings(TypeDefinition _) => s_emptySettingsForFastAbi; + private static readonly Settings s_emptySettingsForFastAbi = new() { NetstandardCompat = false }; + + /// + /// Returns the [Default] interface and the [ExclusiveTo] interfaces (sorted) for fast ABI. + /// Mirrors C++ get_default_and_exclusive_interfaces + sort_fast_abi_ifaces. + /// + public static (TypeDefinition? DefaultInterface, System.Collections.Generic.List OtherInterfaces) GetFastAbiInterfaces(TypeDefinition classType) + { + TypeDefinition? defaultIface = null; + System.Collections.Generic.List exclusiveIfaces = new(); + foreach (InterfaceImplementation impl in classType.Interfaces) + { + if (impl.Interface is null) { continue; } + TypeDefinition? ifaceTd = impl.Interface as TypeDefinition; + if (ifaceTd is null && _cacheRef is not null) + { + try { ifaceTd = impl.Interface.Resolve(_cacheRef.RuntimeContext); } + catch { ifaceTd = null; } + } + if (ifaceTd is null) { continue; } + + if (Helpers.IsDefaultInterface(impl)) + { + defaultIface = ifaceTd; + } + else if (TypeCategorization.IsExclusiveTo(ifaceTd)) + { + exclusiveIfaces.Add(ifaceTd); + } + } + // Sort exclusive interfaces by: + // 1. Number of [PreviousContractVersion] attrs (ascending; newer interfaces have more) + // 2. Contract version (ascending) + // 3. Type version (ascending) + // 4. Type namespace and name (ascending) + exclusiveIfaces.Sort((a, b) => + { + int aPrev = -CountAttributes(a, "Windows.Foundation.Metadata", "PreviousContractVersionAttribute"); + int bPrev = -CountAttributes(b, "Windows.Foundation.Metadata", "PreviousContractVersionAttribute"); + if (aPrev != bPrev) { return aPrev.CompareTo(bPrev); } + + int? aCV = Helpers.GetContractVersion(a); + int? bCV = Helpers.GetContractVersion(b); + if (aCV.HasValue && bCV.HasValue && aCV.Value != bCV.Value) { return aCV.Value.CompareTo(bCV.Value); } + + int? aV = Helpers.GetVersion(a); + int? bV = Helpers.GetVersion(b); + if (aV.HasValue && bV.HasValue && aV.Value != bV.Value) { return aV.Value.CompareTo(bV.Value); } + + string aNs = a.Namespace?.Value ?? string.Empty; + string bNs = b.Namespace?.Value ?? string.Empty; + if (aNs != bNs) { return System.StringComparer.Ordinal.Compare(aNs, bNs); } + return System.StringComparer.Ordinal.Compare(a.Name?.Value ?? string.Empty, b.Name?.Value ?? string.Empty); + }); + return (defaultIface, exclusiveIfaces); + } + + private static int CountAttributes(IHasCustomAttribute member, string ns, string name) + { + int count = 0; + for (int i = 0; i < member.CustomAttributes.Count; i++) + { + CustomAttribute attr = member.CustomAttributes[i]; + ITypeDefOrRef? type = attr.Constructor?.DeclaringType; + if (type is not null && type.Namespace == ns && type.Name == name) { count++; } + } + return count; + } + + /// Mirrors C++ get_gc_pressure_amount. + public static int GetGcPressureAmount(TypeDefinition type) + { + if (!type.IsSealed) { return 0; } + CustomAttribute? attr = TypeCategorization.GetAttribute(type, "Windows.Foundation.Metadata", "GCPressureAttribute"); + if (attr is null || attr.Signature is null) { return 0; } + // The attribute has a single named arg "Amount" of an enum type. Defaults: 0=Low, 1=Medium, 2=High. + // We try both fixed args and named args. + int amount = -1; + if (attr.Signature.NamedArguments.Count > 0) + { + object? v = attr.Signature.NamedArguments[0].Argument.Element; + if (v is int i) { amount = i; } + } + return amount switch + { + 0 => 12000, + 1 => 120000, + 2 => 1200000, + _ => 0 + }; + } + + /// + /// Mirrors C++ write_static_class. + /// + public static void WriteStaticClass(TypeWriter w, TypeDefinition type) + { + bool prevCheckPlatform = w.CheckPlatform; + string prevPlatform = w.Platform; + w.CheckPlatform = true; + w.Platform = string.Empty; + try + { + WriteWinRTMetadataAttribute(w, type, _cacheRef!); + WriteTypeCustomAttributes(w, type, true); + w.Write(Helpers.InternalAccessibility(w.Settings)); + w.Write(" static class "); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + w.Write("\n{\n"); + WriteStaticClassMembers(w, type); + w.Write("}\n"); + } + finally + { + w.CheckPlatform = prevCheckPlatform; + w.Platform = prevPlatform; + } + } + + /// + /// Emits static members from [Static] factory interfaces. Mirrors C++ write_static_members. + /// + public static void WriteStaticClassMembers(TypeWriter w, TypeDefinition type) + { + if (_cacheRef is null) { return; } + // Per-property accessor state (origin tracking for getter/setter) + Dictionary properties = new(System.StringComparer.Ordinal); + // Track the static factory ifaces we've emitted objref fields for (to dedupe) + HashSet emittedObjRefs = new(System.StringComparer.Ordinal); + + string runtimeClassFullName = (type.Namespace?.Value ?? string.Empty) + "." + (type.Name?.Value ?? string.Empty); + + foreach (KeyValuePair kv in AttributedTypes.Get(type, _cacheRef)) + { + AttributedType factory = kv.Value; + if (!(factory.Statics && factory.Type is not null)) { continue; } + TypeDefinition staticIface = factory.Type; + + // Compute the objref name for this static factory interface. + string objRef = GetObjRefName(w, staticIface); + // Compute the ABI Methods static class name (e.g. "global::ABI.Windows.System.ILauncherStaticsMethods") + string abiClass = w.WriteTemp("%", new System.Action(_ => + { + WriteTypedefName(w, staticIface, TypedefNameType.StaticAbiClass, true); + })); + if (!abiClass.StartsWith("global::", System.StringComparison.Ordinal)) + { + abiClass = "global::" + abiClass; + } + + // Emit the lazy static objref field (mirrors truth's pattern) once per static iface. + if (emittedObjRefs.Add(objRef)) + { + WriteStaticFactoryObjRef(w, staticIface, runtimeClassFullName, objRef); + } + + // Compute the platform attribute string from the static factory interface's + // [ContractVersion] attribute. Mirrors C++ code_writers.h:3315 + // 'auto platform_attribute = write_platform_attribute_temp(w, factory.type);' + // and the per-static-method/event/property emission at lines 3316-3349. + string platformAttribute = w.WriteTemp("%", new System.Action(_ => WritePlatformAttribute(w, staticIface))); + + // Methods + foreach (MethodDefinition method in staticIface.Methods) + { + if (Helpers.IsSpecial(method)) { continue; } + MethodSig sig = new(method); + string mname = method.Name?.Value ?? string.Empty; + w.Write("\n"); + if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } + w.Write("public static "); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(mname); + w.Write("("); + WriteParameterList(w, sig); + if (w.Settings.ReferenceProjection) + { + // Mirrors C++ write_abi_static_method_call (code_writers.h:1637): static + // method bodies become 'throw null' in reference projection mode. + w.Write(") => throw null;\n"); + } + else + { + w.Write(") => "); + w.Write(abiClass); + w.Write("."); + w.Write(mname); + w.Write("("); + w.Write(objRef); + for (int i = 0; i < sig.Params.Count; i++) + { + w.Write(", "); + WriteParameterNameWithModifier(w, sig.Params[i]); + } + w.Write(");\n"); + } + } + // Events: dispatch via static ABI class which returns an event source. + foreach (EventDefinition evt in staticIface.Events) + { + string evtName = evt.Name?.Value ?? string.Empty; + w.Write("\n"); + if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } + w.Write("public static event "); + WriteEventType(w, evt); + w.Write(" "); + w.Write(evtName); + w.Write("\n{\n"); + if (w.Settings.ReferenceProjection) + { + // Mirrors C++ write_abi_event_source_static_method_call (code_writers.h:1711): + // event accessor bodies become 'throw null' in reference projection mode. + w.Write(" add => throw null;\n"); + w.Write(" remove => throw null;\n"); + } + else + { + w.Write(" add => "); + w.Write(abiClass); + w.Write("."); + w.Write(evtName); + w.Write("("); + w.Write(objRef); + w.Write(", "); + w.Write(objRef); + w.Write(").Subscribe(value);\n"); + w.Write(" remove => "); + w.Write(abiClass); + w.Write("."); + w.Write(evtName); + w.Write("("); + w.Write(objRef); + w.Write(", "); + w.Write(objRef); + w.Write(").Unsubscribe(value);\n"); + } + w.Write("}\n"); + } + // Properties (merge getter/setter across interfaces, tracking origin per accessor) + foreach (PropertyDefinition prop in staticIface.Properties) + { + string propName = prop.Name?.Value ?? string.Empty; + (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); + string propType = WritePropType(w, prop); + if (!properties.TryGetValue(propName, out StaticPropertyAccessorState? state)) + { + state = new StaticPropertyAccessorState + { + PropTypeText = propType, + }; + properties[propName] = state; + } + if (getter is not null && !state.HasGetter) + { + state.HasGetter = true; + state.GetterAbiClass = abiClass; + state.GetterObjRef = objRef; + // Mirror C++ getter_platform tracking (code_writers.h:3328, 3342). + state.GetterPlatformAttribute = platformAttribute; + } + if (setter is not null && !state.HasSetter) + { + state.HasSetter = true; + state.SetterAbiClass = abiClass; + state.SetterObjRef = objRef; + // Mirror C++ setter_platform tracking (code_writers.h:3330, 3349). + state.SetterPlatformAttribute = platformAttribute; + } + } + } + + // Emit properties with merged accessors + foreach (KeyValuePair kv in properties) + { + StaticPropertyAccessorState s = kv.Value; + w.Write("\n"); + // Mirrors C++ code_writers.h:2041-2046: collapse to property-level platform attribute + // when getter and setter platforms match; otherwise emit per-accessor. + string getterPlat = s.GetterPlatformAttribute; + string setterPlat = s.SetterPlatformAttribute; + string propertyPlat = string.Empty; + bool bothSidesPresent = s.HasGetter && s.HasSetter; + if (!bothSidesPresent || getterPlat == setterPlat) + { + propertyPlat = !string.IsNullOrEmpty(getterPlat) ? getterPlat : setterPlat; + getterPlat = string.Empty; + setterPlat = string.Empty; + } + if (!string.IsNullOrEmpty(propertyPlat)) { w.Write(propertyPlat); } + w.Write("public static "); + w.Write(s.PropTypeText); + w.Write(" "); + w.Write(kv.Key); + // Getter-only -> expression body; otherwise -> accessor block (matches truth). + // In ref mode, all accessor bodies emit '=> throw null;' (mirrors C++ + // write_abi_get/set_property_static_method_call, code_writers.h:1669, 1683). + bool getterOnly = s.HasGetter && !s.HasSetter; + if (getterOnly) + { + if (w.Settings.ReferenceProjection) + { + w.Write(" => throw null;\n"); + } + else + { + w.Write(" => "); + w.Write(s.GetterAbiClass); + w.Write("."); + w.Write(kv.Key); + w.Write("("); + w.Write(s.GetterObjRef); + w.Write(");\n"); + } + } + else + { + w.Write("\n{\n"); + if (s.HasGetter) + { + if (!string.IsNullOrEmpty(getterPlat)) { w.Write(getterPlat); } + if (w.Settings.ReferenceProjection) + { + w.Write("get => throw null;\n"); + } + else + { + w.Write("get => "); + w.Write(s.GetterAbiClass); + w.Write("."); + w.Write(kv.Key); + w.Write("("); + w.Write(s.GetterObjRef); + w.Write(");\n"); + } + } + if (s.HasSetter) + { + if (!string.IsNullOrEmpty(setterPlat)) { w.Write(setterPlat); } + if (w.Settings.ReferenceProjection) + { + w.Write("set => throw null;\n"); + } + else + { + w.Write("set => "); + w.Write(s.SetterAbiClass); + w.Write("."); + w.Write(kv.Key); + w.Write("("); + w.Write(s.SetterObjRef); + w.Write(", value);\n"); + } + } + w.Write("}\n"); + } + } + } + + private sealed class StaticPropertyAccessorState + { + public bool HasGetter; + public bool HasSetter; + public string PropTypeText = string.Empty; + public string GetterAbiClass = string.Empty; + public string GetterObjRef = string.Empty; + public string SetterAbiClass = string.Empty; + public string SetterObjRef = string.Empty; + // Per-accessor platform attribute strings. Mirrors C++ getter_platform/setter_platform + // tracking in code_writers.h:3328-3349. + public string GetterPlatformAttribute = string.Empty; + public string SetterPlatformAttribute = string.Empty; + } + + /// + /// Emits the static lazy objref property for a static factory interface (mirrors truth's + /// pattern: lazy WindowsRuntimeObjectReference.GetActivationFactory(...)). + /// + private static void WriteStaticFactoryObjRef(TypeWriter w, TypeDefinition staticIface, string runtimeClassFullName, string objRefName) + { + w.Write("\nprivate static WindowsRuntimeObjectReference "); + w.Write(objRefName); + w.Write("\n{\n"); + if (w.Settings.ReferenceProjection) + { + // Mirrors C++ write_static_objref_definition (code_writers.h:2789): in ref mode + // the static factory objref getter body is just 'throw null;'. + w.Write(" get\n {\n throw null;\n }\n}\n"); + return; + } + w.Write(" get\n {\n"); + w.Write(" var __"); + w.Write(objRefName); + w.Write(" = field;\n"); + w.Write(" if (__"); + w.Write(objRefName); + w.Write(" != null && __"); + w.Write(objRefName); + w.Write(".IsInCurrentContext)\n {\n"); + w.Write(" return __"); + w.Write(objRefName); + w.Write(";\n }\n"); + w.Write(" return field = WindowsRuntimeObjectReference.GetActivationFactory(\""); + w.Write(runtimeClassFullName); + w.Write("\", "); + WriteIidExpression(w, staticIface); + w.Write(");\n }\n}\n"); + } + + /// + /// Mirrors C++ write_class. Emits a runtime class projection. + /// + public static void WriteClass(TypeWriter w, TypeDefinition type) + { + if (w.Settings.Component) { return; } + + if (TypeCategorization.IsStatic(type)) + { + WriteStaticClass(w, type); + return; + } + + // Mirror C++ writer::write_platform_guard set at the start of write_class. + // Tracks the highest platform seen within this class to suppress redundant + // [SupportedOSPlatform(...)] emissions across interface boundaries. + bool prevCheckPlatform = w.CheckPlatform; + string prevPlatform = w.Platform; + w.CheckPlatform = true; + w.Platform = string.Empty; + try + { + WriteClassCore(w, type); + } + finally + { + w.CheckPlatform = prevCheckPlatform; + w.Platform = prevPlatform; + } + } + + private static void WriteClassCore(TypeWriter w, TypeDefinition type) + { + string typeName = type.Name?.Value ?? string.Empty; + int gcPressure = GetGcPressureAmount(type); + + // Header attributes + w.Write("\n"); + WriteWinRTMetadataAttribute(w, type, _cacheRef!); + WriteTypeCustomAttributes(w, type, true); + WriteComWrapperMarshallerAttribute(w, type); + w.Write(w.Settings.Internal ? "internal" : "public"); + w.Write(" "); + WriteClassModifiers(w, type); + // Mirrors C++ write_class which uses '%class' (no partial) — runtime classes + // are emitted as plain (non-partial) classes. + w.Write("class "); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + WriteTypeInheritance(w, type, false, true); + w.Write("\n{\n"); + + // ObjRef field definitions for each implemented interface (mirrors C++ write_class_objrefs_definition). + // These back the per-interface dispatch in instance methods/properties and the + // IWindowsRuntimeInterface.GetInterface() implementations. + WriteClassObjRefDefinitions(w, type); + + // Constructor: WindowsRuntimeObjectReference-based constructor (RCW-like) + if (!w.Settings.ReferenceProjection) + { + string ctorAccess = type.IsSealed ? "internal" : "protected internal"; + w.Write("\n"); + w.Write(ctorAccess); + w.Write(" "); + w.Write(typeName); + w.Write("(WindowsRuntimeObjectReference nativeObjectReference)\n: base(nativeObjectReference)\n{\n"); + if (!type.IsSealed) + { + // For unsealed classes, the default interface objref needs to be initialized only + // when GetType() matches the projected class exactly (derived classes have their own + // default interface). The init; accessor on _objRef_ allows this set. + ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); + if (defaultIface is not null) + { + string defaultObjRefName = GetObjRefName(w, defaultIface); + w.Write("if (GetType() == typeof("); + w.Write(typeName); + w.Write("))\n{\n"); + w.Write(defaultObjRefName); + w.Write(" = NativeObjectReference;\n"); + w.Write("}\n"); + } + } + if (gcPressure > 0) + { + w.Write("GC.AddMemoryPressure("); + w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write(");\n"); + } + w.Write("}\n"); + } + else if (_cacheRef is not null) + { + // In ref mode, if WriteAttributedTypes will not emit any public constructors, + // we need a 'private TypeName() { throw null; }' to suppress the C# compiler's + // implicit public default constructor (which would expose an unintended API). + // Mirrors C++ code_writers.h:9519-9538 exactly: a type has constructors when + // either: + // - factory.activatable is true (parameterless or parameterized — Activatable + // always emits at least one ctor), OR + // - factory.composable && factory.type && factory.type.MethodList().size() > 0 + // (composable factories with NO methods don't emit any ctors). + bool hasRefModeCtors = false; + foreach (KeyValuePair kv in AttributedTypes.Get(type, _cacheRef)) + { + AttributedType factory = kv.Value; + if (factory.Activatable) + { + hasRefModeCtors = true; + break; + } + if (factory.Composable && factory.Type is not null && factory.Type.Methods.Count > 0) + { + hasRefModeCtors = true; + break; + } + } + if (!hasRefModeCtors) + { + EmitSyntheticPrivateCtor(w, typeName); + } + } + + // Activator/composer constructors from [Activatable]/[Composable] factory interfaces. + // Mirror C++ write_attributed_types: emits factory ctors AND static members (via + // write_static_members) BEFORE the override hooks and instance members. + WriteAttributedTypes(w, type); + + // Static members from [Static] factory interfaces (e.g. GetForCurrentView). + // C++ emits these inside write_attributed_types -> write_static_members; emit them + // here right after to preserve the same overall ordering. + WriteStaticClassMembers(w, type); + + // Conditional finalizer + if (gcPressure > 0) + { + w.Write("~"); + w.Write(typeName); + w.Write("()\n{\nGC.RemoveMemoryPressure("); + w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write(");\n}\n"); + } + + // Class members from interfaces (instance methods, properties, events) + // Override hooks must be emitted BEFORE the public members to match the C++ + // ordering (write_class line 9591/9600/9601: hooks first, then write_class_members). + // HasUnwrappableNativeObjectReference and IsOverridableInterface overrides. + if (!w.Settings.ReferenceProjection) + { + w.Write("\nprotected override bool HasUnwrappableNativeObjectReference => "); + if (!type.IsSealed) + { + w.Write("GetType() == typeof("); + w.Write(typeName); + w.Write(");"); + } + else + { + w.Write("true;"); + } + w.Write("\n"); + + // IsOverridableInterface override (mirrors C++ write_custom_query_interface_impl). + // Emit '|| == iid' for each [Overridable] interface impl, then '|| base.IsOverridableInterface(in iid)' + // if the type has a base class, finally fall back to 'false' if no entries. + w.Write("\nprotected override bool IsOverridableInterface(in Guid iid) => "); + bool firstClause = true; + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (!Helpers.IsOverridable(impl)) { continue; } + ITypeDefOrRef? implRef = impl.Interface; + if (implRef is null) { continue; } + if (!firstClause) { w.Write(" || "); } + firstClause = false; + WriteIidExpression(w, implRef); + w.Write(" == iid"); + } + // base call when type has a non-object base class + bool hasBaseClass = type.BaseType is not null + && !(type.BaseType.Namespace?.Value == "System" && type.BaseType.Name?.Value == "Object") + && !(type.BaseType.Namespace?.Value == "WindowsRuntime" && type.BaseType.Name?.Value == "WindowsRuntimeObject"); + if (hasBaseClass) + { + if (!firstClause) { w.Write(" || "); } + w.Write("base.IsOverridableInterface(in iid)"); + firstClause = false; + } + if (firstClause) { w.Write("false"); } + w.Write(";\n"); + } + + WriteClassMembers(w, type); + + w.Write("}\n"); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ClassMembers.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ClassMembers.cs new file mode 100644 index 0000000000..f7b9af76fb --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ClassMembers.cs @@ -0,0 +1,995 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Class member emission: walks implemented interfaces and emits the public/protected +/// instance methods, properties, and events (mirrors C++ write_class_members). +/// +internal static partial class CodeWriters +{ + /// + /// Emits all instance members (methods, properties, events) inherited from implemented interfaces. + /// Mirrors C++ write_class_members. In ref-projection mode, this is still called: type + /// declarations and per-interface objref getters are emitted, but non-mapped instance + /// method/property/event bodies are emitted as => throw null; stubs. + /// + public static void WriteClassMembers(TypeWriter w, TypeDefinition type) + { + HashSet writtenMethods = new(System.StringComparer.Ordinal); + // For properties: track per-name accessor presence so we can merge get/set across interfaces. + // Use insertion-order Dictionary so the per-class property emission order matches the + // .winmd metadata definition order (mirrors C++ which uses type.PropertyList() order). + Dictionary propertyState = new(System.StringComparer.Ordinal); + HashSet writtenEvents = new(System.StringComparer.Ordinal); + HashSet writtenInterfaces = new(); + + // Mirror C++ class member ordering: emit GetInterface()/GetDefaultInterface() per + // interface inside WriteInterfaceMembersRecursive (right before that interface's + // members), instead of one upfront block. This interleaves the GetInterface() impls + // with their corresponding interface body, matching truth's per-interface layout. + WriteInterfaceMembersRecursive(w, type, type, null, writtenMethods, propertyState, writtenEvents, writtenInterfaces); + + // After collecting all properties (with merged accessors), emit them. + foreach (KeyValuePair kvp in propertyState) + { + PropertyAccessorState s = kvp.Value; + // For generic-interface properties, emit the UnsafeAccessor static externs above the + // property declaration. Note: getter and setter use the same accessor name (because + // C# allows method overloading on parameter list for the static externs). + if (s.HasGetter && s.GetterIsGeneric && !string.IsNullOrEmpty(s.GetterGenericInteropType)) + { + w.Write("\n[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \""); + w.Write(kvp.Key); + w.Write("\")]\n"); + w.Write("static extern "); + w.Write(s.GetterPropTypeText); + w.Write(" "); + w.Write(s.GetterGenericAccessorName); + w.Write("([UnsafeAccessorType(\""); + w.Write(s.GetterGenericInteropType); + w.Write("\")] object _, WindowsRuntimeObjectReference thisReference);\n"); + } + if (s.HasSetter && s.SetterIsGeneric && !string.IsNullOrEmpty(s.SetterGenericInteropType)) + { + w.Write("\n[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \""); + w.Write(kvp.Key); + w.Write("\")]\n"); + w.Write("static extern void "); + w.Write(s.SetterGenericAccessorName); + w.Write("([UnsafeAccessorType(\""); + w.Write(s.SetterGenericInteropType); + w.Write("\")] object _, WindowsRuntimeObjectReference thisReference, "); + w.Write(s.SetterPropTypeText); + w.Write(" value);\n"); + } + + w.Write("\n"); + // Mirrors C++ code_writers.h:2041-2046: collapse to property-level platform attribute + // when getter and setter platforms match; otherwise emit per-accessor. + string getterPlat = s.GetterPlatformAttribute; + string setterPlat = s.SetterPlatformAttribute; + string propertyPlat = string.Empty; + // C++: if (getter_platform == setter_platform) { property_platform = getter_platform; getter_platform = ""; setter_platform = ""; } + // For getter-only or setter-only properties, only one side is set; compare the relevant side. + bool bothSidesPresent = s.HasGetter && s.HasSetter; + if (!bothSidesPresent || getterPlat == setterPlat) + { + // Collapse: prefer the populated side (matches C++ which compares string_view equality + // including both being empty). + propertyPlat = !string.IsNullOrEmpty(getterPlat) ? getterPlat : setterPlat; + getterPlat = string.Empty; + setterPlat = string.Empty; + } + if (!string.IsNullOrEmpty(propertyPlat)) { w.Write(propertyPlat); } + w.Write(s.Access); + w.Write(s.MethodSpec); + w.Write(s.PropTypeText); + w.Write(" "); + w.Write(kvp.Key); + // For getter-only properties, emit expression body: 'public T Prop => Expr;' + // For getter+setter or setter-only, use accessor block: 'public T Prop { get => ...; set => ...; }' + // (mirrors C++ which uses '%' template substitution where get-only collapses to '=> %'). + // + // In ref mode, all property bodies emit '=> throw null;' (mirrors C++ + // write_abi_get/set_property_static_method_call + write_unsafe_accessor_property_static_method_call, + // code_writers.h:1669, 1683, 1697). + bool getterOnly = s.HasGetter && !s.HasSetter; + if (getterOnly) + { + w.Write(" => "); + if (w.Settings.ReferenceProjection) + { + w.Write("throw null;"); + } + else if (s.GetterIsGeneric) + { + if (!string.IsNullOrEmpty(s.GetterGenericInteropType)) + { + w.Write(s.GetterGenericAccessorName); + w.Write("(null, "); + w.Write(s.GetterObjRef); + w.Write(");"); + } + else + { + w.Write("throw null!;"); + } + } + else + { + w.Write(s.GetterAbiClass); + w.Write("."); + w.Write(kvp.Key); + w.Write("("); + w.Write(s.GetterObjRef); + w.Write(");"); + } + w.Write("\n"); + } + else + { + w.Write("\n{\n"); + if (s.HasGetter) + { + if (!string.IsNullOrEmpty(getterPlat)) + { + w.Write(" "); + w.Write(getterPlat); + } + if (w.Settings.ReferenceProjection) + { + w.Write(" get => throw null;\n"); + } + else if (s.GetterIsGeneric) + { + if (!string.IsNullOrEmpty(s.GetterGenericInteropType)) + { + w.Write(" get => "); + w.Write(s.GetterGenericAccessorName); + w.Write("(null, "); + w.Write(s.GetterObjRef); + w.Write(");\n"); + } + else + { + w.Write(" get => throw null!;\n"); + } + } + else + { + w.Write(" get => "); + w.Write(s.GetterAbiClass); + w.Write("."); + w.Write(kvp.Key); + w.Write("("); + w.Write(s.GetterObjRef); + w.Write(");\n"); + } + } + if (s.HasSetter) + { + if (!string.IsNullOrEmpty(setterPlat)) + { + w.Write(" "); + w.Write(setterPlat); + } + if (w.Settings.ReferenceProjection) + { + w.Write(" set => throw null;\n"); + } + else if (s.SetterIsGeneric) + { + if (!string.IsNullOrEmpty(s.SetterGenericInteropType)) + { + w.Write(" set => "); + w.Write(s.SetterGenericAccessorName); + w.Write("(null, "); + w.Write(s.SetterObjRef); + w.Write(", value);\n"); + } + else + { + w.Write(" set => throw null!;\n"); + } + } + else + { + w.Write(" set => "); + w.Write(s.SetterAbiClass); + w.Write("."); + w.Write(kvp.Key); + w.Write("("); + w.Write(s.SetterObjRef); + w.Write(", value);\n"); + } + } + w.Write("}\n"); + } + + // For overridable properties, emit an explicit interface implementation that + // delegates to the protected property. Mirrors truth pattern: + // T InterfaceName.PropName { get => PropName; } + // T InterfaceName.PropName { set => PropName = value; } + if (s.IsOverridable && s.OverridableInterface is not null) + { + w.Write(s.PropTypeText); + w.Write(" "); + WriteInterfaceTypeNameForCcw(w, s.OverridableInterface); + w.Write("."); + w.Write(kvp.Key); + w.Write(" {"); + if (s.HasGetter) + { + w.Write("get => "); + w.Write(kvp.Key); + w.Write("; "); + } + if (s.HasSetter) + { + w.Write("set => "); + w.Write(kvp.Key); + w.Write(" = value; "); + } + w.Write("}\n"); + } + } + + // GetInterface() / GetDefaultInterface() impls are emitted per-interface inside + // WriteInterfaceMembersRecursive (matches the C++ tool's per-interface ordering). + } + + private static string BuildMethodSignatureKey(string name, MethodSig sig) + { + System.Text.StringBuilder sb = new(); + sb.Append(name); + sb.Append('('); + for (int i = 0; i < sig.Params.Count; i++) + { + if (i > 0) { sb.Append(','); } + sb.Append(sig.Params[i].Type?.FullName ?? "?"); + } + sb.Append(')'); + return sb.ToString(); + } + + private sealed class PropertyAccessorState + { + public bool HasGetter; + public bool HasSetter; + public string PropTypeText = string.Empty; + public string Access = "public "; + public string MethodSpec = string.Empty; + public string GetterAbiClass = string.Empty; + public string GetterObjRef = string.Empty; + public string SetterAbiClass = string.Empty; + public string SetterObjRef = string.Empty; + public string Name = string.Empty; + public bool GetterIsGeneric; + public bool SetterIsGeneric; + public string GetterGenericInteropType = string.Empty; + public string GetterGenericAccessorName = string.Empty; + public string GetterPropTypeText = string.Empty; + public string SetterGenericInteropType = string.Empty; + public string SetterGenericAccessorName = string.Empty; + public string SetterPropTypeText = string.Empty; + // True if this property comes from an Overridable interface (needs explicit interface impl). + public bool IsOverridable; + // The originating interface (used to qualify the explicit interface impl). + public ITypeDefOrRef? OverridableInterface; + // Per-accessor platform attribute strings from the originating interface's [ContractVersion], + // emitted before the property in ref mode. Mirrors C++ getter_platform/setter_platform + // tracking in code_writers.h:4306-4308 / 4323/4330. When both match, emit at the property + // level only; when they differ (getter and setter come from different interfaces with + // different platforms), emit per-accessor. + public string GetterPlatformAttribute = string.Empty; + public string SetterPlatformAttribute = string.Empty; + } + + /// + /// Returns true if the given interface implementation should appear in the class's inheritance list + /// (i.e., it has [Overridable], or is not [ExclusiveTo], or includeExclusiveInterface is set). + /// + private static bool IsInterfaceInInheritanceList(InterfaceImplementation impl, bool includeExclusiveInterface) + { + if (impl.Interface is null) { return false; } + if (Helpers.IsOverridable(impl)) { return true; } + if (includeExclusiveInterface) { return true; } + TypeDefinition? td = ResolveInterface(impl.Interface); + if (td is null) { return true; } + return !TypeCategorization.IsExclusiveTo(td); + } + + private static void WriteInterfaceMembersRecursive(TypeWriter w, TypeDefinition classType, TypeDefinition declaringType, + AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? currentInstance, + HashSet writtenMethods, IDictionary propertyState, HashSet writtenEvents, HashSet writtenInterfaces) + { + AsmResolver.DotNet.Signatures.GenericContext genCtx = new(currentInstance, null); + + foreach (InterfaceImplementation impl in declaringType.Interfaces) + { + if (impl.Interface is null) { continue; } + + // Resolve TypeRef to TypeDef using our cache + TypeDefinition? ifaceType = ResolveInterface(impl.Interface); + if (ifaceType is null) { continue; } + + if (writtenInterfaces.Contains(ifaceType)) { continue; } + _ = writtenInterfaces.Add(ifaceType); + + bool isOverridable = Helpers.IsOverridable(impl); + bool isProtected = TypeCategorization.HasAttribute(impl, "Windows.Foundation.Metadata", "ProtectedAttribute"); + + // Substitute generic type arguments using the current generic context BEFORE emitting + // any references to this interface. This is critical for nested recursion: e.g. when + // emitting members for IObservableMap's base IMap, we need to + // substitute !0/!1 with string/object so the generated code references + // IDictionary instead of IDictionary. Mirrors the C++ tool's + // writer.push_generic_args() stack inside for_typedef(). + ITypeDefOrRef substitutedInterface = impl.Interface; + AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? nextInstance = null; + if (impl.Interface is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) + { + if (currentInstance is not null) + { + AsmResolver.DotNet.Signatures.TypeSignature subSig = gi.InstantiateGenericTypes(genCtx); + if (subSig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature subGi) + { + nextInstance = subGi; + AsmResolver.DotNet.ITypeDefOrRef? newRef = subGi.ToTypeDefOrRef(); + if (newRef is not null) { substitutedInterface = newRef; } + } + else + { + nextInstance = gi; + } + } + else + { + nextInstance = gi; + } + } + + // Emit GetInterface() / GetDefaultInterface() impl for this interface BEFORE its + // members (mirrors C++ write_class_interface at code_writers.h:4257-4280). For + // overridable interfaces or non-exclusive direct interfaces, emit + // IWindowsRuntimeInterface.GetInterface(). For the default interface on an + // unsealed class with an exclusive default, emit "internal new GetDefaultInterface()". + // + // The IWindowsRuntimeInterface markers are NOT emitted in ref mode (gated by + // !w.Settings.ReferenceProjection here, mirrors C++ code_writers.h:4257 + // '&& !settings.reference_projection' in the corresponding condition). The + // 'internal new GetDefaultInterface()' helper IS emitted in both modes since + // it's referenced by overrides on derived classes. + if (IsInterfaceInInheritanceList(impl, includeExclusiveInterface: false) && !w.Settings.ReferenceProjection) + { + string giObjRefName = GetObjRefName(w, substitutedInterface); + w.Write("\nWindowsRuntimeObjectReferenceValue IWindowsRuntimeInterface<"); + WriteInterfaceTypeNameForCcw(w, substitutedInterface); + w.Write(">.GetInterface()\n{\nreturn "); + w.Write(giObjRefName); + w.Write(".AsValue();\n}\n"); + } + else if (Helpers.IsDefaultInterface(impl) && !classType.IsSealed) + { + // Mirrors C++ code_writers.h:4263-4280. The C++ source emits the + // 'internal new GetDefaultInterface()' helper whenever the interface is the + // default interface and the class is unsealed -- regardless of exclusive-to + // status. In ref-projection mode this is the only branch that emits the helper + // (the prior 'IWindowsRuntimeInterface.GetInterface' branch is gated off). + // In non-ref mode this branch is only reached when the prior branch's + // IsInterfaceInInheritanceList check fails (i.e., ExclusiveTo default interfaces), + // because non-exclusive default interfaces are routed to the prior branch. + string giObjRefName = GetObjRefName(w, substitutedInterface); + bool hasBaseType = false; + if (classType.BaseType is not null) + { + string? baseNs = classType.BaseType.Namespace?.Value; + string? baseName = classType.BaseType.Name?.Value; + hasBaseType = !(baseNs == "System" && baseName == "Object"); + } + w.Write("\ninternal "); + if (hasBaseType) { w.Write("new "); } + w.Write("WindowsRuntimeObjectReferenceValue GetDefaultInterface()\n{\nreturn "); + w.Write(giObjRefName); + w.Write(".AsValue();\n}\n"); + } + + // For mapped interfaces with custom members output (e.g. IClosable -> IDisposable, IMap`2 + // -> IDictionary), emit stubs for the C# interface's required members so the class + // satisfies its inheritance contract. The runtime's adapter actually services them. + string ifaceNs = ifaceType.Namespace?.Value ?? string.Empty; + string ifaceName = ifaceType.Name?.Value ?? string.Empty; + if (MappedTypes.Get(ifaceNs, ifaceName) is { HasCustomMembersOutput: true }) + { + if (IsMappedInterfaceRequiringStubs(ifaceNs, ifaceName)) + { + // For generic interfaces, use the substituted nextInstance to compute the + // objref name so type arguments are concrete (matches the field name emitted + // by WriteClassObjRefDefinitions). For non-generic, fall back to impl.Interface. + string objRefName = GetObjRefName(w, substitutedInterface); + WriteMappedInterfaceStubs(w, nextInstance, ifaceName, objRefName); + } + continue; + } + + WriteInterfaceMembers(w, classType, ifaceType, impl.Interface, isOverridable, isProtected, nextInstance, + writtenMethods, propertyState, writtenEvents); + + // Recurse into derived interfaces + WriteInterfaceMembersRecursive(w, classType, ifaceType, nextInstance, writtenMethods, propertyState, writtenEvents, writtenInterfaces); + } + } + + private static TypeDefinition? ResolveInterface(ITypeDefOrRef typeRef) + { + if (typeRef is TypeDefinition td) { return td; } + if (_cacheRef is null) { return null; } + // Try the runtime context resolver first (handles cross-module references via the resolver) + try + { + TypeDefinition? resolved = typeRef.Resolve(_cacheRef.RuntimeContext); + if (resolved is not null) { return resolved; } + } + catch + { + // Fall through to local lookup + } + // Fall back to local lookup by full name + if (typeRef is TypeReference tr) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + string fullName = string.IsNullOrEmpty(ns) ? name : ns + "." + name; + return _cacheRef.Find(fullName); + } + if (typeRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + return ResolveInterface(gi.GenericType); + } + return null; + } + + private static void WriteInterfaceMembers(TypeWriter w, TypeDefinition classType, TypeDefinition ifaceType, + ITypeDefOrRef originalInterface, + bool isOverridable, bool isProtected, AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? currentInstance, + HashSet writtenMethods, IDictionary propertyState, HashSet writtenEvents) + { + bool sealed_ = classType.IsSealed; + // Determine accessibility and method modifier. + // Overridable interfaces are emitted with 'protected' visibility, plus 'virtual' on + // non-sealed classes. Sealed classes still get 'protected' (without virtual). + string access = (isOverridable || isProtected) ? "protected " : "public "; + string methodSpec = string.Empty; + if (isOverridable && !sealed_) + { + methodSpec = "virtual "; + } + + AsmResolver.DotNet.Signatures.GenericContext? genCtx = currentInstance is not null + ? new AsmResolver.DotNet.Signatures.GenericContext(currentInstance, null) + : null; + + // Generic interfaces require UnsafeAccessor-based dispatch (real ABI lives in the + // post-build interop assembly). + bool isGenericInterface = ifaceType.GenericParameters.Count > 0; + + // Fast ABI: when this interface is exclusive_to a fast-abi class (and we're emitting + // class members, classType is that fast-abi class), dispatch routes through the + // default interface's ABI Methods class and objref instead of through this interface's + // own ABI Methods class. The native vtable bundles all exclusive interfaces' methods + // into the default interface's vtable in a fixed order. Mirrors C++ + // code_writers.h:4250-4251 (semantics_for_abi_call assignment) which redirects both + // static_iface_target and the objref to the default interface for fast-abi cases. + TypeDefinition abiInterface = ifaceType; + ITypeDefOrRef abiInterfaceRef = originalInterface; + bool isFastAbiExclusive = IsFastAbiClass(classType, w.Settings) && TypeCategorization.IsExclusiveTo(ifaceType); + bool isDefaultInterface = false; + if (isFastAbiExclusive) + { + (TypeDefinition? defaultIface, _) = GetFastAbiInterfaces(classType); + if (defaultIface is not null) + { + abiInterface = defaultIface; + abiInterfaceRef = defaultIface; + isDefaultInterface = ReferenceEquals(defaultIface, ifaceType); + } + } + + // Mirrors C++ code_writers.h:4293 — the 'inline_event_source_field' arg is + // '!is_fast_abi_iface || is_default_interface'. For events on a fast-abi non-default + // exclusive interface (e.g. ISimple5.Event0 on the Simple class), the inline + // _eventSource_X field pattern is WRONG: the slot computed from the interface's own + // method index is invalid (the runtime exposes only the merged ISimple vtable, not + // a separate ISimple5 vtable). Instead, dispatch through the default interface's + // ABI Methods class helper (e.g. ISimpleMethods.Event0(this, _objRef_..ISimple)) + // which uses the correct merged-vtable slot and a ConditionalWeakTable for caching. + bool inlineEventSourceField = !isFastAbiExclusive || isDefaultInterface; + + // Compute the ABI Methods static class name (e.g. "global::ABI.Windows.Foundation.IDeferralMethods") + // — note this is the ungenerified Methods class for generic interfaces (matches truth output). + // The _objRef_ field name uses the full instantiated interface name so generic instantiations + // (e.g. IAsyncOperation) get a per-instantiation field. + string abiClass = w.WriteTemp("%", new System.Action(_ => + { + WriteTypedefName(w, abiInterface, TypedefNameType.StaticAbiClass, true); + })); + if (!abiClass.StartsWith("global::", System.StringComparison.Ordinal)) + { + abiClass = "global::" + abiClass; + } + string objRef = GetObjRefName(w, abiInterfaceRef); + + // For generic interfaces, also compute the encoded parent type name (used in UnsafeAccessor + // function names) and the WinRT.Interop accessor type string (passed to UnsafeAccessorType). + string genericParentEncoded = string.Empty; + string genericInteropType = string.Empty; + if (isGenericInterface && currentInstance is not null) + { + string projectedParent = w.WriteTemp("%", new System.Action(_ => + WriteTypeName(w, TypeSemanticsFactory.Get(currentInstance), TypedefNameType.Projected, true))); + genericParentEncoded = EscapeTypeNameForIdentifier(projectedParent, stripGlobal: true); + genericInteropType = EncodeInteropTypeName(currentInstance, TypedefNameType.StaticAbiClass) + ", WinRT.Interop"; + } + + // Compute the platform attribute string from the interface type's [ContractVersion] + // attribute. In ref mode, this is prepended to each member emission so the projected + // class members carry [SupportedOSPlatform("WindowsX.Y.Z.0")] mirroring the interface's + // contract version. Only emitted in ref mode (WritePlatformAttribute internally returns + // immediately if not ref). Mirrors C++ code_writers.h:4290 + // 'auto platform_attribute = write_platform_attribute_temp(w, interface_type);'. + string platformAttribute = w.WriteTemp("%", new System.Action(_ => WritePlatformAttribute(w, ifaceType))); + + // Methods + foreach (MethodDefinition method in ifaceType.Methods) + { + if (Helpers.IsSpecial(method)) { continue; } + string name = method.Name?.Value ?? string.Empty; + // Track by full signature (name + each param's element-type code) to avoid trivial overload duplicates. + // This prevents collapsing distinct overloads like Format(double) and Format(ulong). + MethodSig sig = new(method, genCtx); + string key = BuildMethodSignatureKey(name, sig); + if (!writtenMethods.Add(key)) { continue; } + + // Detect a 'string ToString()' that overrides Object.ToString(). C++ uses 'override' + // here (and even forces 'string' as the return type). See code_writers.h:1942-1959. + string methodSpecForThis = methodSpec; + if (name == "ToString" && sig.Params.Count == 0 + && sig.ReturnType is AsmResolver.DotNet.Signatures.CorLibTypeSignature crt + && crt.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String) + { + methodSpecForThis = "override "; + } + + // Detect 'bool Equals(object obj)' and 'int GetHashCode()' that override their + // System.Object counterparts. Mirrors C++ helpers.h:566 (is_object_equals_method) and + // helpers.h:625 (is_object_hashcode_method) + code_writers.h:1962-1974: matching + // signature and return type -> 'override'; matching name only -> 'new'. + if (name == "Equals" && sig.Params.Count == 1) + { + AsmResolver.DotNet.Signatures.TypeSignature p0 = sig.Params[0].Type; + bool paramIsObject = p0 is AsmResolver.DotNet.Signatures.CorLibTypeSignature po + && po.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object; + bool returnsBool = sig.ReturnType is AsmResolver.DotNet.Signatures.CorLibTypeSignature ro + && ro.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean; + if (paramIsObject) + { + methodSpecForThis = returnsBool ? "override " : (methodSpecForThis + "new "); + } + } + else if (name == "GetHashCode" && sig.Params.Count == 0) + { + bool returnsInt = sig.ReturnType is AsmResolver.DotNet.Signatures.CorLibTypeSignature ri + && ri.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4; + methodSpecForThis = returnsInt ? "override " : (methodSpecForThis + "new "); + } + + if (isGenericInterface && !string.IsNullOrEmpty(genericInteropType)) + { + // Emit UnsafeAccessor static extern + body that dispatches through it. + string accessorName = genericParentEncoded + "_" + name; + w.Write("\n[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \""); + w.Write(name); + w.Write("\")]\n"); + w.Write("static extern "); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(accessorName); + w.Write("([UnsafeAccessorType(\""); + w.Write(genericInteropType); + w.Write("\")] object _, WindowsRuntimeObjectReference thisReference"); + for (int i = 0; i < sig.Params.Count; i++) + { + w.Write(", "); + WriteProjectionParameter(w, sig.Params[i]); + } + w.Write(");\n"); + + // Mirrors C++ code_writers.h:4292 — prepend the per-interface platform attribute + // string to each public method emission. In ref mode this produces e.g. + // [global::System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.16299.0")]. + if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } + w.Write(access); + w.Write(methodSpecForThis); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(name); + w.Write("("); + WriteParameterList(w, sig); + if (w.Settings.ReferenceProjection) + { + // Mirrors C++ write_unsafe_accessor_static_method_call (code_writers.h:1653) + // which emits 'throw null' in reference projection mode. + w.Write(") => throw null;\n"); + } + else + { + w.Write(") => "); + w.Write(accessorName); + w.Write("(null, "); + w.Write(objRef); + for (int i = 0; i < sig.Params.Count; i++) + { + w.Write(", "); + WriteParameterNameWithModifier(w, sig.Params[i]); + } + w.Write(");\n"); + } + } + else + { + w.Write("\n"); + if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } + w.Write(access); + w.Write(methodSpecForThis); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(name); + w.Write("("); + WriteParameterList(w, sig); + if (w.Settings.ReferenceProjection) + { + // Mirrors C++ write_abi_static_method_call (code_writers.h:1637) + // which emits 'throw null' in reference projection mode. + w.Write(") => throw null;\n"); + } + else + { + w.Write(") => "); + w.Write(abiClass); + w.Write("."); + w.Write(name); + w.Write("("); + w.Write(objRef); + for (int i = 0; i < sig.Params.Count; i++) + { + w.Write(", "); + WriteParameterNameWithModifier(w, sig.Params[i]); + } + w.Write(");\n"); + } + } + + // For overridable interface methods, emit an explicit interface implementation + // that delegates to the protected (and virtual on non-sealed) method. Mirrors C++ + // overridable interface pattern: + // T InterfaceName.MethodName(args) => MethodName(args); + if (isOverridable) + { + // Mirror C++ which carries the platform attribute on the explicit interface + // impl as well (since it shares the same originating interface). + if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } + WriteProjectionReturnType(w, sig); + w.Write(" "); + WriteInterfaceTypeNameForCcw(w, originalInterface); + w.Write("."); + w.Write(name); + w.Write("("); + WriteParameterList(w, sig); + w.Write(") => "); + w.Write(name); + w.Write("("); + for (int i = 0; i < sig.Params.Count; i++) + { + if (i > 0) { w.Write(", "); } + WriteParameterNameWithModifier(w, sig.Params[i]); + } + w.Write(");\n"); + } + } + + // Properties: collect into propertyState (merging accessors from multiple interfaces). + // Track per-accessor origin so that the getter/setter dispatch to the right ABI Methods + // class on the right _objRef_ field. + foreach (PropertyDefinition prop in ifaceType.Properties) + { + string name = prop.Name?.Value ?? string.Empty; + (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); + if (!propertyState.TryGetValue(name, out PropertyAccessorState? state)) + { + state = new PropertyAccessorState + { + PropTypeText = WritePropType(w, prop, genCtx), + Access = access, + MethodSpec = methodSpec, + IsOverridable = isOverridable, + OverridableInterface = isOverridable ? originalInterface : null, + }; + propertyState[name] = state; + } + if (getter is not null && !state.HasGetter) + { + state.HasGetter = true; + state.GetterAbiClass = abiClass; + state.GetterObjRef = objRef; + state.GetterIsGeneric = isGenericInterface; + state.GetterGenericInteropType = genericInteropType; + state.GetterGenericAccessorName = isGenericInterface ? (genericParentEncoded + "_" + name) : string.Empty; + state.GetterPropTypeText = WritePropType(w, prop, genCtx); + // Mirror C++ getter_platform tracking (code_writers.h:4306, 4323). + state.GetterPlatformAttribute = platformAttribute; + } + if (setter is not null && !state.HasSetter) + { + state.HasSetter = true; + state.SetterAbiClass = abiClass; + state.SetterObjRef = objRef; + state.SetterIsGeneric = isGenericInterface; + state.SetterGenericInteropType = genericInteropType; + state.SetterGenericAccessorName = isGenericInterface ? (genericParentEncoded + "_" + name) : string.Empty; + state.SetterPropTypeText = WritePropType(w, prop, genCtx); + // Mirror C++ setter_platform tracking (code_writers.h:4308, 4330). + state.SetterPlatformAttribute = platformAttribute; + } + } + + // Events: emit the event with Subscribe/Unsubscribe through a per-event _eventSource_ + // backing property field that lazily constructs an EventHandlerEventSource for the event + // handler type. Mirrors C++ write_class_events_using_static_abi_methods + write_event. + foreach (EventDefinition evt in ifaceType.Events) + { + string name = evt.Name?.Value ?? string.Empty; + if (!writtenEvents.Add(name)) { continue; } + + // Compute event handler type and event source type strings. + AsmResolver.DotNet.Signatures.TypeSignature evtSig = evt.EventType!.ToTypeSignature(false); + if (currentInstance is not null) + { + evtSig = evtSig.InstantiateGenericTypes(new AsmResolver.DotNet.Signatures.GenericContext(currentInstance, null)); + } + bool isGenericEvent = evtSig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature; + + // Special case for ICommand.CanExecuteChanged: the WinRT event handler is + // EventHandler but C# expects non-generic EventHandler. Use the non-generic + // EventHandlerEventSource backing field. Mirrors C++ write_event hard-coded fix. + bool isICommandCanExecuteChanged = name == "CanExecuteChanged" + && (ifaceType.FullName == "Microsoft.UI.Xaml.Input.ICommand" + || ifaceType.FullName == "Windows.UI.Xaml.Input.ICommand"); + + string eventSourceType; + if (isICommandCanExecuteChanged) + { + eventSourceType = "global::WindowsRuntime.InteropServices.EventHandlerEventSource"; + isGenericEvent = false; + } + else + { + eventSourceType = w.WriteTemp("%", new System.Action(_ => + WriteTypeName(w, TypeSemanticsFactory.Get(evtSig), TypedefNameType.EventSource, false))); + } + string eventSourceTypeFull = eventSourceType; + if (!eventSourceTypeFull.StartsWith("global::", System.StringComparison.Ordinal)) + { + eventSourceTypeFull = "global::" + eventSourceTypeFull; + } + // The "interop" type name string for the EventSource UnsafeAccessor (only needed for generic events). + string eventSourceInteropType = isGenericEvent + ? EncodeInteropTypeName(evtSig, TypedefNameType.EventSource) + ", WinRT.Interop" + : string.Empty; + + // Compute vtable index = method index in the interface vtable + 6 (for IInspectable methods). + // The add method is the first method of the event in the interface. + int methodIndex = 0; + foreach (MethodDefinition m in ifaceType.Methods) + { + if (m == evt.AddMethod) { break; } + methodIndex++; + } + int vtableIndex = 6 + methodIndex; + + // Emit the _eventSource_ property field — skipped in ref mode (the event + // accessors below become 'add => throw null;' / 'remove => throw null;' which + // don't reference the field, mirrors C++ where the inline_event_source_field + // path emits 'throw null' at code_writers.h:2215, 2238). Also skipped when the + // event must dispatch through the ABI Methods class instead (see + // 'inlineEventSourceField' computation above for fast-abi non-default exclusive). + if (!w.Settings.ReferenceProjection && inlineEventSourceField) + { + w.Write("\nprivate "); + w.Write(eventSourceTypeFull); + w.Write(" _eventSource_"); + w.Write(name); + w.Write("\n{\n get\n {\n"); + if (isGenericEvent && !string.IsNullOrEmpty(eventSourceInteropType)) + { + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.Constructor)]\n"); + w.Write(" [return: UnsafeAccessorType(\""); + w.Write(eventSourceInteropType); + w.Write("\")]\n"); + w.Write(" static extern object ctor(WindowsRuntimeObjectReference nativeObjectReference, int index);\n\n"); + } + w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); + w.Write(" "); + w.Write(eventSourceTypeFull); + w.Write(" MakeEventSource()\n {\n"); + w.Write(" _ = global::System.Threading.Interlocked.CompareExchange(\n"); + w.Write(" location1: ref field,\n"); + w.Write(" value: "); + if (isGenericEvent) + { + w.Write("Unsafe.As<"); + w.Write(eventSourceTypeFull); + w.Write(">(ctor("); + w.Write(objRef); + w.Write(", "); + w.Write(vtableIndex.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write("))"); + } + else + { + w.Write("new "); + w.Write(eventSourceTypeFull); + w.Write("("); + w.Write(objRef); + w.Write(", "); + w.Write(vtableIndex.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write(")"); + } + w.Write(",\n"); + w.Write(" comparand: null);\n\n"); + w.Write(" return field;\n }\n\n"); + w.Write(" return field ?? MakeEventSource();\n }\n}\n"); + } + + // Emit the public/protected event with Subscribe/Unsubscribe. + w.Write("\n"); + // Mirrors C++ code_writers.h:4293 — prepend the per-interface platform attribute + // string to each event emission. In ref mode this produces e.g. + // [global::System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.16299.0")]. + if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } + w.Write(access); + w.Write(methodSpec); + w.Write("event "); + WriteEventType(w, evt, currentInstance); + w.Write(" "); + w.Write(name); + w.Write("\n{\n"); + if (w.Settings.ReferenceProjection) + { + w.Write(" add => throw null;\n"); + w.Write(" remove => throw null;\n"); + } + else if (inlineEventSourceField) + { + w.Write(" add => _eventSource_"); + w.Write(name); + w.Write(".Subscribe(value);\n"); + w.Write(" remove => _eventSource_"); + w.Write(name); + w.Write(".Unsubscribe(value);\n"); + } + else + { + // Fast-abi non-default exclusive: dispatch through the default interface's + // ABI Methods class helper. Mirrors C++ code_writers.h write_event when + // inline_event_source_field is false (the default helper-based path). + // Example: Simple.Event0 (on ISimple5) becomes + // add => global::ABI.test_component_fast.ISimpleMethods.Event0((WindowsRuntimeObject)this, _objRef_test_component_fast_ISimple).Subscribe(value); + w.Write(" add => "); + w.Write(abiClass); + w.Write("."); + w.Write(name); + w.Write("((WindowsRuntimeObject)this, "); + w.Write(objRef); + w.Write(").Subscribe(value);\n"); + w.Write(" remove => "); + w.Write(abiClass); + w.Write("."); + w.Write(name); + w.Write("((WindowsRuntimeObject)this, "); + w.Write(objRef); + w.Write(").Unsubscribe(value);\n"); + } + w.Write("}\n"); + } + } + + /// + /// Writes a parameter name prefixed with its modifier (in/out/ref) for use as a call argument. + /// + private static void WriteParameterNameWithModifier(TypeWriter w, ParamInfo p) + { + ParamCategory cat = ParamHelpers.GetParamCategory(p); + switch (cat) + { + case ParamCategory.Out: + w.Write("out "); + break; + case ParamCategory.Ref: + w.Write("in "); + break; + case ParamCategory.ReceiveArray: + w.Write("out "); + break; + } + WriteParameterName(w, p); + } + + /// + /// Writes the projected name for an interface reference (TypeDefinition, TypeReference, or + /// generic instance), applying mapped-type remapping. Used inside IWindowsRuntimeInterface<T>. + /// + private static void WriteInterfaceTypeNameForCcw(TypeWriter w, ITypeDefOrRef ifaceType) + { + // If the reference is to a type in the same module, resolve to TypeDefinition so + // WriteTypedefName can drop the 'global::.' prefix when the namespace matches. + // Mirrors the C++ tool's behavior of emitting the bare interface name when in scope. + if (ifaceType is not TypeDefinition && ifaceType is not TypeSpecification && _cacheRef is not null) + { + try + { + TypeDefinition? resolved = ifaceType.Resolve(_cacheRef.RuntimeContext); + if (resolved is not null) { ifaceType = resolved; } + } + catch { /* leave as TypeReference */ } + } + if (ifaceType is TypeDefinition td) + { + WriteTypedefName(w, td, TypedefNameType.CCW, false); + WriteTypeParams(w, td); + } + else if (ifaceType is TypeReference tr) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + w.Write("global::"); + w.Write(ns); + w.Write("."); + w.WriteCode(name); + } + else if (ifaceType is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) + { + ITypeDefOrRef gt = gi.GenericType; + string ns = gt.Namespace?.Value ?? string.Empty; + string name = gt.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + w.Write("global::"); + w.Write(ns); + w.Write("."); + w.WriteCode(name); + w.Write("<"); + for (int i = 0; i < gi.TypeArguments.Count; i++) + { + if (i > 0) { w.Write(", "); } + WriteTypeName(w, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, true); + } + w.Write(">"); + } + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Component.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Component.cs new file mode 100644 index 0000000000..94d4fa0c9c --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Component.cs @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Component-mode helpers, mirroring functions in code_writers.h. +/// +internal static partial class CodeWriters +{ + /// Mirrors C++ add_metadata_type_entry. + public static void AddMetadataTypeEntry(TypeWriter w, TypeDefinition type, ConcurrentDictionary map) + { + if (!w.Settings.Component) { return; } + TypeCategory cat = TypeCategorization.GetCategory(type); + if ((cat == TypeCategory.Class && TypeCategorization.IsStatic(type)) || + (cat == TypeCategory.Interface && TypeCategorization.IsExclusiveTo(type))) + { + return; + } + string typeName = w.WriteTemp("%", new System.Action(_ => + { + WriteTypedefName(w, type, TypedefNameType.Projected, true); + WriteTypeParams(w, type); + })); + string metadataTypeName = w.WriteTemp("%", new System.Action(_ => + { + WriteTypedefName(w, type, TypedefNameType.CCW, true); + WriteTypeParams(w, type); + })); + _ = map.TryAdd(typeName, metadataTypeName); + } + + /// Mirrors C++ write_factory_class (simplified). + public static void WriteFactoryClass(TypeWriter w, TypeDefinition type) + { + string typeName = type.Name?.Value ?? string.Empty; + string typeNs = type.Namespace?.Value ?? string.Empty; + // Mirror C++ 'write_type_name(type, Projected)' which for an authored type produces 'global::.'. + string projectedTypeName = string.IsNullOrEmpty(typeNs) + ? $"global::{Helpers.StripBackticks(typeName)}" + : $"global::{typeNs}.{Helpers.StripBackticks(typeName)}"; + string factoryTypeName = $"{Helpers.StripBackticks(typeName)}ServerActivationFactory"; + bool isActivatable = !TypeCategorization.IsStatic(type) && Helpers.HasDefaultConstructor(type); + + // Build the inheritance list: factory interfaces ([Activatable]/[Static]) only. + // Mirrors C++ write_factory_class_inheritance. + MetadataCache? cache = GetMetadataCache(); + List factoryInterfaces = new(); + if (cache is not null) + { + foreach (KeyValuePair kv in AttributedTypes.Get(type, cache)) + { + AttributedType info = kv.Value; + if ((info.Activatable || info.Statics) && info.Type is not null) + { + factoryInterfaces.Add(info.Type); + } + } + } + + w.Write("\ninternal sealed class "); + w.Write(factoryTypeName); + w.Write(" : global::WindowsRuntime.InteropServices.IActivationFactory"); + foreach (TypeDefinition iface in factoryInterfaces) + { + w.Write(", "); + // Mirror C++ 'write_type_name(factory.type, CCW, false)'. For factory interfaces, + // CCW + non-forced namespace is the user-facing interface name (e.g. 'IButtonUtilsStatic'). + WriteTypedefName(w, iface, TypedefNameType.CCW, false); + WriteTypeParams(w, iface); + } + w.Write("\n{\n"); + + w.Write("static "); + w.Write(factoryTypeName); + w.Write("()\n{\n"); + w.Write("global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof("); + w.Write(projectedTypeName); + w.Write(").TypeHandle);\n}\n"); + + w.Write("\npublic static unsafe void* Make()\n{\nreturn global::WindowsRuntime.InteropServices.Marshalling.WindowsRuntimeInterfaceMarshaller\n .ConvertToUnmanaged(_factory, in global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IActivationFactory)\n .DetachThisPtrUnsafe();\n}\n"); + + w.Write("\nprivate static readonly "); + w.Write(factoryTypeName); + w.Write(" _factory = new();\n"); + + w.Write("\npublic object ActivateInstance()\n{\n"); + if (isActivatable) + { + w.Write("return new "); + w.Write(projectedTypeName); + w.Write("();"); + } + else + { + w.Write("throw new NotImplementedException();"); + } + w.Write("\n}\n"); + + // Emit factory-class members: forwarding methods/properties/events for static factory + // interfaces, and constructor wrappers for activatable factory interfaces. + // Mirrors C++ write_factory_class_members. + if (cache is not null) + { + foreach (KeyValuePair kv in AttributedTypes.Get(type, cache)) + { + AttributedType info = kv.Value; + if (info.Type is null) { continue; } + + if (info.Activatable) + { + foreach (MethodDefinition method in info.Type.Methods) + { + if (method.IsConstructor) { continue; } + WriteFactoryActivatableMethod(w, method, projectedTypeName); + } + } + else if (info.Statics) + { + foreach (MethodDefinition method in info.Type.Methods) + { + if (method.IsConstructor) { continue; } + WriteStaticFactoryMethod(w, method, projectedTypeName); + } + foreach (PropertyDefinition prop in info.Type.Properties) + { + WriteStaticFactoryProperty(w, prop, projectedTypeName); + } + foreach (EventDefinition evt in info.Type.Events) + { + WriteStaticFactoryEvent(w, evt, projectedTypeName); + } + } + } + } + + w.Write("}\n"); + } + + /// + /// Writes a factory-class activatable wrapper method: public T MethodName(args) => new T(args);. + /// Mirrors C++ write_factory_activatable_method. + /// + private static void WriteFactoryActivatableMethod(TypeWriter w, MethodDefinition method, string projectedTypeName) + { + if (method.IsSpecialName) { return; } + string methodName = method.Name?.Value ?? string.Empty; + w.Write("\npublic "); + w.Write(projectedTypeName); + w.Write(" "); + w.Write(methodName); + w.Write("("); + WriteFactoryMethodParameters(w, method, includeTypes: true); + w.Write(") => new "); + w.Write(projectedTypeName); + w.Write("("); + WriteFactoryMethodParameters(w, method, includeTypes: false); + w.Write(");\n"); + } + + /// + /// Writes a static-factory forwarding method: public Ret MethodName(args) => global::Ns.Type.MethodName(args);. + /// Mirrors C++ write_static_factory_method. + /// + private static void WriteStaticFactoryMethod(TypeWriter w, MethodDefinition method, string projectedTypeName) + { + if (method.IsSpecialName) { return; } + string methodName = method.Name?.Value ?? string.Empty; + w.Write("\npublic "); + WriteFactoryReturnType(w, method); + w.Write(" "); + w.Write(methodName); + w.Write("("); + WriteFactoryMethodParameters(w, method, includeTypes: true); + w.Write(") => "); + w.Write(projectedTypeName); + w.Write("."); + w.Write(methodName); + w.Write("("); + WriteFactoryMethodParameters(w, method, includeTypes: false); + w.Write(");\n"); + } + + /// + /// Writes a static-factory forwarding property: a multi-line block matching C++ + /// write_property + write_static_factory_property. + /// + private static void WriteStaticFactoryProperty(TypeWriter w, PropertyDefinition prop, string projectedTypeName) + { + string propName = prop.Name?.Value ?? string.Empty; + (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); + // Single-line form when no setter is present (mirrors C++ early-return path). + if (setter is null) + { + w.Write("\npublic "); + WriteFactoryPropertyType(w, prop); + w.Write(" "); + w.Write(propName); + w.Write(" => "); + w.Write(projectedTypeName); + w.Write("."); + w.Write(propName); + w.Write(";\n"); + return; + } + w.Write("\npublic "); + WriteFactoryPropertyType(w, prop); + w.Write(" "); + w.Write(propName); + w.Write("\n{\n"); + if (getter is not null) + { + w.Write("get => "); + w.Write(projectedTypeName); + w.Write("."); + w.Write(propName); + w.Write(";\n"); + } + w.Write("set => "); + w.Write(projectedTypeName); + w.Write("."); + w.Write(propName); + w.Write(" = value;\n"); + w.Write("}\n"); + } + + /// + /// Writes a static-factory forwarding event as a multi-line block matching C++ + /// write_event + write_static_factory_event. + /// + private static void WriteStaticFactoryEvent(TypeWriter w, EventDefinition evt, string projectedTypeName) + { + string evtName = evt.Name?.Value ?? string.Empty; + w.Write("\npublic event "); + if (evt.EventType is not null) + { + TypeSemantics evtSemantics = TypeSemanticsFactory.GetFromTypeDefOrRef(evt.EventType); + WriteTypeName(w, evtSemantics, TypedefNameType.Projected, false); + } + w.Write(" "); + w.Write(evtName); + w.Write("\n{\n"); + w.Write("add => "); + w.Write(projectedTypeName); + w.Write("."); + w.Write(evtName); + w.Write(" += value;\n"); + w.Write("remove => "); + w.Write(projectedTypeName); + w.Write("."); + w.Write(evtName); + w.Write(" -= value;\n"); + w.Write("}\n"); + } + + private static void WriteFactoryReturnType(TypeWriter w, MethodDefinition method) + { + AsmResolver.DotNet.Signatures.TypeSignature? returnType = method.Signature?.ReturnType; + if (returnType is null || returnType.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Void) + { + w.Write("void"); + return; + } + TypeSemantics semantics = TypeSemanticsFactory.Get(returnType); + WriteTypeName(w, semantics, TypedefNameType.Projected, true); + } + + private static void WriteFactoryPropertyType(TypeWriter w, PropertyDefinition prop) + { + AsmResolver.DotNet.Signatures.TypeSignature? sig = prop.Signature?.ReturnType; + if (sig is null) { w.Write("object"); return; } + TypeSemantics semantics = TypeSemanticsFactory.Get(sig); + WriteTypeName(w, semantics, TypedefNameType.Projected, true); + } + + private static void WriteFactoryMethodParameters(TypeWriter w, MethodDefinition method, bool includeTypes) + { + AsmResolver.DotNet.Signatures.MethodSignature? sig = method.Signature; + if (sig is null) { return; } + for (int i = 0; i < sig.ParameterTypes.Count; i++) + { + if (i > 0) { w.Write(", "); } + ParameterDefinition? p = method.Parameters.Count > i + (method.IsStatic ? 0 : 0) ? method.Parameters[i].Definition : null; + string paramName = p?.Name?.Value ?? $"arg{i}"; + if (includeTypes) + { + TypeSemantics semantics = TypeSemanticsFactory.Get(sig.ParameterTypes[i]); + WriteTypeName(w, semantics, TypedefNameType.Projected, true); + w.Write(" "); + w.Write(paramName); + } + else + { + w.Write(paramName); + } + } + } + + /// Mirrors C++ write_module_activation_factory (simplified). + public static void WriteModuleActivationFactory(TextWriter w, IReadOnlyDictionary> typesByModule) + { + w.Write("\nusing System;\n"); + foreach (KeyValuePair> kv in typesByModule) + { + w.Write("\nnamespace ABI."); + w.Write(kv.Key); + w.Write("\n{\npublic static class ManagedExports\n{\npublic static unsafe void* GetActivationFactory(ReadOnlySpan activatableClassId)\n{\nswitch (activatableClassId)\n{\n"); + // Sort by the type's metadata token / row index so cases appear in WinMD declaration + // order. Mirrors C++ which uses std::set (sorted by metadata RID). + List orderedTypes = new(kv.Value); + orderedTypes.Sort((a, b) => + { + uint ra = a.MetadataToken.Rid; + uint rb = b.MetadataToken.Rid; + return ra.CompareTo(rb); + }); + foreach (TypeDefinition type in orderedTypes) + { + string ns = type.Namespace?.Value ?? string.Empty; + string name = type.Name?.Value ?? string.Empty; + w.Write("case \""); + w.Write(ns); + w.Write("."); + w.Write(name); + w.Write("\":\n return "); + // Mirror C++ 'write_type_name(type, CCW, true)' which for an authored type + // emits 'global::ABI.Impl..'. + w.Write("global::ABI.Impl."); + w.Write(ns); + w.Write("."); + w.Write(Helpers.StripBackticks(name)); + w.Write("ServerActivationFactory.Make();\n"); + } + w.Write("default:\n return null;\n}\n}\n}\n}\n"); + } + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Constructors.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Constructors.cs new file mode 100644 index 0000000000..374de602dc --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Constructors.cs @@ -0,0 +1,1026 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Activator/composer constructor emission. Mirrors C++ write_factory_constructors +/// and write_composable_constructors. +/// +internal static partial class CodeWriters +{ + /// + /// Mirrors C++ write_attributed_types: emits constructors and static members + /// for the given runtime class. + /// + public static void WriteAttributedTypes(TypeWriter w, TypeDefinition classType) + { + if (_cacheRef is null) { return; } + + // Track whether we need to emit the static _objRef_ field (used by + // default constructors). Emit it once per class if any [Activatable] factory exists. + bool needsClassObjRef = false; + + foreach (KeyValuePair kv in AttributedTypes.Get(classType, _cacheRef)) + { + AttributedType factory = kv.Value; + if (factory.Activatable && factory.Type is null) + { + needsClassObjRef = true; + break; + } + } + + if (needsClassObjRef) + { + string fullName = (classType.Namespace?.Value ?? string.Empty) + "." + (classType.Name?.Value ?? string.Empty); + string objRefName = "_objRef_" + EscapeTypeNameForIdentifier("global::" + fullName, stripGlobal: true); + w.Write("\nprivate static WindowsRuntimeObjectReference "); + w.Write(objRefName); + if (w.Settings.ReferenceProjection) + { + // Mirrors C++ write_activation_factory_objref_definition (code_writers.h:2748): + // in ref mode the activation factory objref getter body is just 'throw null;'. + EmitRefModeObjRefGetterBody(w); + } + else + { + w.Write("\n{\n get\n {\n var __"); + w.Write(objRefName); + w.Write(" = field;\n if (__"); + w.Write(objRefName); + w.Write(" != null && __"); + w.Write(objRefName); + w.Write(".IsInCurrentContext)\n {\n return __"); + w.Write(objRefName); + w.Write(";\n }\n return field = WindowsRuntimeObjectReference.GetActivationFactory(\""); + w.Write(fullName); + w.Write("\");\n }\n}\n"); + } + } + + foreach (KeyValuePair kv in AttributedTypes.Get(classType, _cacheRef)) + { + AttributedType factory = kv.Value; + if (factory.Activatable) + { + WriteFactoryConstructors(w, factory.Type, classType); + } + else if (factory.Composable) + { + WriteComposableConstructors(w, factory.Type, classType, factory.Visible ? "public" : "protected"); + } + } + } + + /// + /// Mirrors C++ write_factory_constructors. + /// + public static void WriteFactoryConstructors(TypeWriter w, TypeDefinition? factoryType, TypeDefinition classType) + { + string typeName = classType.Name?.Value ?? string.Empty; + int gcPressure = GetGcPressureAmount(classType); + if (factoryType is not null) + { + // Emit the factory objref property (lazy-initialized). + string factoryRuntimeClassFullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; + string factoryObjRefName = GetObjRefName(w, factoryType); + WriteStaticFactoryObjRef(w, factoryType, factoryRuntimeClassFullName, factoryObjRefName); + + string defaultIfaceIid = GetDefaultInterfaceIid(w, classType); + string marshalingType = GetMarshalingTypeName(classType); + // Compute the platform attribute string from the activation factory interface's + // [ContractVersion] attribute. Mirrors C++ code_writers.h:2861 + // 'auto platform_attribute = write_platform_attribute_temp(w, factory_type);' + // emitted at line 2872 before the public ctor. + string platformAttribute = w.WriteTemp("%", new System.Action(_ => WritePlatformAttribute(w, factoryType))); + int methodIndex = 0; + foreach (MethodDefinition method in factoryType.Methods) + { + if (Helpers.IsSpecial(method)) { methodIndex++; continue; } + MethodSig sig = new(method); + string callbackName = (method.Name?.Value ?? "Create") + "_" + sig.Params.Count.ToString(System.Globalization.CultureInfo.InvariantCulture); + string argsName = callbackName + "Args"; + + // Emit the public constructor. + w.Write("\n"); + if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } + w.Write("public unsafe "); + w.Write(typeName); + w.Write("("); + WriteParameterList(w, sig); + w.Write(")\n :base("); + if (sig.Params.Count == 0) + { + w.Write("default"); + } + else + { + w.Write(callbackName); + w.Write(".Instance, "); + w.Write(defaultIfaceIid); + w.Write(", "); + w.Write(marshalingType); + w.Write(", WindowsRuntimeActivationArgsReference.CreateUnsafe(new "); + w.Write(argsName); + w.Write("("); + for (int i = 0; i < sig.Params.Count; i++) + { + if (i > 0) { w.Write(", "); } + string raw = sig.Params[i].Parameter.Name ?? "param"; + w.Write(Helpers.IsKeyword(raw) ? "@" + raw : raw); + } + w.Write("))"); + } + w.Write(")\n{\n"); + if (gcPressure > 0) + { + w.Write("GC.AddMemoryPressure("); + w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write(");\n"); + } + w.Write("}\n"); + + if (sig.Params.Count > 0) + { + EmitFactoryArgsStruct(w, sig, argsName); + EmitFactoryCallbackClass(w, sig, callbackName, argsName, factoryObjRefName, methodIndex); + } + + methodIndex++; + } + } + else + { + // No factory type means [Activatable(uint version)] - emit a default ctor that calls + // the WindowsRuntimeObject base constructor with the activation factory objref. + // The default interface IID is needed too. + string fullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; + string objRefName = "_objRef_" + EscapeTypeNameForIdentifier("global::" + fullName, stripGlobal: true); + + // Find the default interface IID to use. + string defaultIfaceIid = GetDefaultInterfaceIid(w, classType); + + w.Write("\npublic "); + w.Write(typeName); + w.Write("()\n :base(default(WindowsRuntimeActivationTypes.DerivedSealed), "); + w.Write(objRefName); + w.Write(", "); + w.Write(defaultIfaceIid); + w.Write(", "); + w.Write(GetMarshalingTypeName(classType)); + w.Write(")\n{\n"); + if (gcPressure > 0) + { + w.Write("GC.AddMemoryPressure("); + w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write(");\n"); + } + w.Write("}\n"); + } + } + + /// + /// Reads the [MarshalingBehaviorAttribute] on the class and returns the corresponding + /// CreateObjectReferenceMarshalingType.* expression. Mirrors C++ + /// get_marshaling_type_name. + /// + private static string GetMarshalingTypeName(TypeDefinition classType) + { + for (int i = 0; i < classType.CustomAttributes.Count; i++) + { + CustomAttribute attr = classType.CustomAttributes[i]; + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + if (attrType is null) { continue; } + if (attrType.Namespace?.Value != "Windows.Foundation.Metadata" || + attrType.Name?.Value != "MarshalingBehaviorAttribute") { continue; } + if (attr.Signature is null) { continue; } + for (int j = 0; j < attr.Signature.FixedArguments.Count; j++) + { + AsmResolver.DotNet.Signatures.CustomAttributeArgument arg = attr.Signature.FixedArguments[j]; + if (arg.Element is int v) + { + return v switch + { + 2 => "CreateObjectReferenceMarshalingType.Agile", + 3 => "CreateObjectReferenceMarshalingType.Standard", + _ => "CreateObjectReferenceMarshalingType.Unknown", + }; + } + } + } + return "CreateObjectReferenceMarshalingType.Unknown"; + } + + /// Emits the private readonly ref struct <Name>Args(args...) {...}. + /// If >= 0, only emit the first + /// params (used for composable factories where the trailing baseInterface/innerInterface params + /// are consumed by the callback Invoke signature directly, not stored in args). + private static void EmitFactoryArgsStruct(TypeWriter w, MethodSig sig, string argsName, int userParamCount = -1) + { + int count = userParamCount >= 0 ? userParamCount : sig.Params.Count; + w.Write("\nprivate readonly ref struct "); + w.Write(argsName); + w.Write("("); + for (int i = 0; i < count; i++) + { + if (i > 0) { w.Write(", "); } + WriteProjectionParameter(w, sig.Params[i]); + } + w.Write(")\n{\n"); + for (int i = 0; i < count; i++) + { + ParamInfo p = sig.Params[i]; + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write(" public readonly "); + // Use the parameter's projected type (matches the constructor parameter type, including + // ReadOnlySpan/Span for array params). + WriteProjectionParameterType(w, p); + w.Write(" "); + w.Write(pname); + w.Write(" = "); + w.Write(pname); + w.Write(";\n"); + } + w.Write("}\n"); + } + + /// Emits the private sealed class <Name> : WindowsRuntimeActivationFactoryCallback.DerivedSealed. + /// When true, emit the DerivedComposed callback variant whose + /// Invoke signature includes the additional WindowsRuntimeObject baseInterface + + /// out void* innerInterface params. Iteration over user params is bounded by + /// (defaults to all params). + private static void EmitFactoryCallbackClass(TypeWriter w, MethodSig sig, string callbackName, string argsName, string factoryObjRefName, int factoryMethodIndex, bool isComposable = false, int userParamCount = -1) + { + int paramCount = userParamCount >= 0 ? userParamCount : sig.Params.Count; + w.Write("\nprivate sealed class "); + w.Write(callbackName); + w.Write(isComposable + ? " : WindowsRuntimeActivationFactoryCallback.DerivedComposed\n{\n" + : " : WindowsRuntimeActivationFactoryCallback.DerivedSealed\n{\n"); + w.Write(" public static readonly "); + w.Write(callbackName); + w.Write(" Instance = new();\n\n"); + w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); + if (isComposable) + { + // Composable Invoke signature is multi-line and includes baseInterface (in) + + // innerInterface (out). Mirrors truth output exactly. + w.Write(" public override unsafe void Invoke(\n"); + w.Write(" WindowsRuntimeActivationArgsReference additionalParameters,\n"); + w.Write(" WindowsRuntimeObject baseInterface,\n"); + w.Write(" out void* innerInterface,\n"); + w.Write(" out void* retval)\n {\n"); + } + else + { + // Sealed Invoke signature is multi-line. Mirrors C++ at code_writers.h:6838. + w.Write(" public override unsafe void Invoke(\n"); + w.Write(" WindowsRuntimeActivationArgsReference additionalParameters,\n"); + w.Write(" out void* retval)\n {\n"); + } + + // Mirrors C++ at code_writers.h:6849: in reference projection mode, the entire + // Invoke body is just 'throw null;' (no factory dispatch, no marshalling). + if (w.Settings.ReferenceProjection) + { + EmitRefModeInvokeBody(w); + return; + } + + w.Write(" using WindowsRuntimeObjectReferenceValue activationFactoryValue = "); + w.Write(factoryObjRefName); + w.Write(".AsValue();\n"); + w.Write(" void* ThisPtr = activationFactoryValue.GetThisPtrUnsafe();\n"); + w.Write(" ref readonly "); + w.Write(argsName); + w.Write(" args = ref additionalParameters.GetValueRefUnsafe<"); + w.Write(argsName); + w.Write(">();\n"); + + // Bind each arg from the args struct to a local of its ABI-marshalable input type. + // Bind arg locals. + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + w.Write(" "); + // For array params, the bind type is ReadOnlySpan / Span (not the SzArray). + if (cat == ParamCategory.PassArray) + { + w.Write("ReadOnlySpan<"); + WriteProjectionType(w, TypeSemanticsFactory.Get(((AsmResolver.DotNet.Signatures.SzArrayTypeSignature)p.Type).BaseType)); + w.Write(">"); + } + else if (cat == ParamCategory.FillArray) + { + w.Write("Span<"); + WriteProjectionType(w, TypeSemanticsFactory.Get(((AsmResolver.DotNet.Signatures.SzArrayTypeSignature)p.Type).BaseType)); + w.Write(">"); + } + else + { + WriteProjectedSignature(w, p.Type, true); + } + w.Write(" "); + w.Write(pname); + w.Write(" = args."); + w.Write(pname); + w.Write(";\n"); + } + + // For generic instance params, emit local UnsafeAccessor delegates (or Nullable -> BoxToUnmanaged). + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + if (!IsGenericInstance(p.Type)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + if (IsNullableT(p.Type)) + { + AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(p.Type)!; + string innerMarshaller = GetNullableInnerMarshallerName(w, inner); + w.Write(" using WindowsRuntimeObjectReferenceValue __"); + w.Write(raw); + w.Write(" = "); + w.Write(innerMarshaller); + w.Write(".BoxToUnmanaged("); + w.Write(pname); + w.Write(");\n"); + continue; + } + string interopTypeName = EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, p.Type, false))); + w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); + w.Write(" static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_"); + w.Write(raw); + w.Write("([UnsafeAccessorType(\""); + w.Write(interopTypeName); + w.Write("\")] object _, "); + w.Write(projectedTypeName); + w.Write(" value);\n"); + w.Write(" using WindowsRuntimeObjectReferenceValue __"); + w.Write(raw); + w.Write(" = ConvertToUnmanaged_"); + w.Write(raw); + w.Write("(null, "); + w.Write(pname); + w.Write(");\n"); + } + + // For runtime class / object params, emit `using WindowsRuntimeObjectReferenceValue __ = ...ConvertToUnmanaged();` + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + if (IsGenericInstance(p.Type)) { continue; } // already handled above + if (!IsRuntimeClassOrInterface(p.Type) && !IsObject(p.Type)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write(" using WindowsRuntimeObjectReferenceValue __"); + w.Write(raw); + w.Write(" = "); + EmitMarshallerConvertToUnmanaged(w, p.Type, pname); + w.Write(";\n"); + } + + // For composable factories, marshal the additional `baseInterface` (which is a + // WindowsRuntimeObject parameter on Invoke, not an args field). Truth pattern: + // using WindowsRuntimeObjectReferenceValue __baseInterface = WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(baseInterface); + if (isComposable) + { + w.Write(" using WindowsRuntimeObjectReferenceValue __baseInterface = WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(baseInterface);\n"); + w.Write(" void* __innerInterface = default;\n"); + } + + // For mapped value-type params (DateTime, TimeSpan), emit ABI local + marshaller conversion. + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + if (!IsMappedAbiValueType(p.Type)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + string abiType = GetMappedAbiTypeName(p.Type); + string marshaller = GetMappedMarshallerName(p.Type); + w.Write(" "); + w.Write(abiType); + w.Write(" __"); + w.Write(raw); + w.Write(" = "); + w.Write(marshaller); + w.Write(".ConvertToUnmanaged("); + w.Write(pname); + w.Write(");\n"); + } + + // For HResultException params, emit ABI local + ExceptionMarshaller conversion. + // (HResult is excluded from IsMappedAbiValueType because it's "treated specially in many + // places", but for activator factory ctor params the marshalling pattern is the same.) + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + if (!IsHResultException(p.Type)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write(" global::ABI.System.Exception __"); + w.Write(raw); + w.Write(" = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged("); + w.Write(pname); + w.Write(");\n"); + } + + // Declare InlineArray16 + ArrayPool fallback for non-blittable PassArray params + // (runtime classes, objects, strings). + bool hasNonBlittableArray = false; + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + hasNonBlittableArray = true; + string raw = p.Parameter.Name ?? "param"; + string callName = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); + w.Write(raw); + w.Write("_inlineArray);\n"); + w.Write(" nint[] __"); + w.Write(raw); + w.Write("_arrayFromPool = null;\n"); + w.Write(" Span __"); + w.Write(raw); + w.Write("_span = "); + w.Write(callName); + w.Write(".Length <= 16\n ? __"); + w.Write(raw); + w.Write("_inlineArray[.."); + w.Write(callName); + w.Write(".Length]\n : (__"); + w.Write(raw); + w.Write("_arrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); + w.Write(callName); + w.Write(".Length));\n"); + + if (IsString(szArr.BaseType)) + { + w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); + w.Write(raw); + w.Write("_inlineHeaderArray);\n"); + w.Write(" HStringHeader[] __"); + w.Write(raw); + w.Write("_headerArrayFromPool = null;\n"); + w.Write(" Span __"); + w.Write(raw); + w.Write("_headerSpan = "); + w.Write(callName); + w.Write(".Length <= 16\n ? __"); + w.Write(raw); + w.Write("_inlineHeaderArray[.."); + w.Write(callName); + w.Write(".Length]\n : (__"); + w.Write(raw); + w.Write("_headerArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); + w.Write(callName); + w.Write(".Length));\n"); + + w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); + w.Write(raw); + w.Write("_inlinePinnedHandleArray);\n"); + w.Write(" nint[] __"); + w.Write(raw); + w.Write("_pinnedHandleArrayFromPool = null;\n"); + w.Write(" Span __"); + w.Write(raw); + w.Write("_pinnedHandleSpan = "); + w.Write(callName); + w.Write(".Length <= 16\n ? __"); + w.Write(raw); + w.Write("_inlinePinnedHandleArray[.."); + w.Write(callName); + w.Write(".Length]\n : (__"); + w.Write(raw); + w.Write("_pinnedHandleArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); + w.Write(callName); + w.Write(".Length));\n"); + } + } + + w.Write(" void* __retval = default;\n"); + if (hasNonBlittableArray) { w.Write(" try\n {\n"); } + string baseIndent = hasNonBlittableArray ? " " : " "; + + // For System.Type params, pre-marshal to TypeReference (must be declared OUTSIDE the + // fixed() block since the fixed block pins the resulting reference). + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + if (!IsSystemType(p.Type)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write(baseIndent); + w.Write("global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe("); + w.Write(pname); + w.Write(", out TypeReference __"); + w.Write(raw); + w.Write(");\n"); + } + + // Open ONE combined "fixed(void* _a = ..., _b = ..., ...)" block for ALL pinnable + // params (string, Type, PassArray). Mirrors C++ write_abi_method_call_marshalers + // which emits a single combined fixed-block for all is_pinnable marshalers. + int fixedNesting = 0; + int pinnableCount = 0; + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (IsString(p.Type) || IsSystemType(p.Type)) { pinnableCount++; } + else if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) { pinnableCount++; } + } + if (pinnableCount > 0) + { + string indent = baseIndent; + w.Write(indent); + w.Write("fixed(void* "); + bool firstPin = true; + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + bool isStr = IsString(p.Type); + bool isType = IsSystemType(p.Type); + bool isArr = cat == ParamCategory.PassArray || cat == ParamCategory.FillArray; + if (!isStr && !isType && !isArr) { continue; } + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + if (!firstPin) { w.Write(", "); } + firstPin = false; + w.Write("_"); + w.Write(raw); + w.Write(" = "); + if (isType) { w.Write("__"); w.Write(raw); } + else if (isArr) + { + AsmResolver.DotNet.Signatures.TypeSignature elemT = ((AsmResolver.DotNet.Signatures.SzArrayTypeSignature)p.Type).BaseType; + bool isBlittableElem = IsBlittablePrimitive(elemT) || IsAnyStruct(elemT); + bool isStringElem = IsString(elemT); + if (isBlittableElem) { w.Write(pname); } + else { w.Write("__"); w.Write(raw); w.Write("_span"); } + if (isStringElem) + { + w.Write(", _"); + w.Write(raw); + w.Write("_inlineHeaderArray = __"); + w.Write(raw); + w.Write("_headerSpan"); + } + } + else + { + // string param: pin the input string itself. + w.Write(pname); + } + } + w.Write(")\n"); + w.Write(indent); + w.Write("{\n"); + fixedNesting = 1; + // Inside the block: emit HStringMarshaller.ConvertToUnmanagedUnsafe for each + // string input. The HStringReference local lives stack-only. + string innerIndent = baseIndent + new string(' ', fixedNesting * 4); + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + if (!IsString(p.Type)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write(innerIndent); + w.Write("HStringMarshaller.ConvertToUnmanagedUnsafe((char*)_"); + w.Write(raw); + w.Write(", "); + w.Write(pname); + w.Write("?.Length, out HStringReference __"); + w.Write(raw); + w.Write(");\n"); + } + } + + string callIndent = baseIndent + new string(' ', fixedNesting * 4); + + // Emit CopyToUnmanaged for non-blittable PassArray params. + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + if (IsString(szArr.BaseType)) + { + w.Write(callIndent); + w.Write("HStringArrayMarshaller.ConvertToUnmanagedUnsafe(\n"); + w.Write(callIndent); + w.Write(" source: "); + w.Write(pname); + w.Write(",\n"); + w.Write(callIndent); + w.Write(" hstringHeaders: (HStringHeader*) _"); + w.Write(raw); + w.Write("_inlineHeaderArray,\n"); + w.Write(callIndent); + w.Write(" hstrings: __"); + w.Write(raw); + w.Write("_span,\n"); + w.Write(callIndent); + w.Write(" pinnedGCHandles: __"); + w.Write(raw); + w.Write("_pinnedHandleSpan);\n"); + } + else + { + string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szArr.BaseType)))); + string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + w.Write(callIndent); + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToUnmanaged\")]\n"); + w.Write(callIndent); + w.Write("static extern void CopyToUnmanaged_"); + w.Write(raw); + w.Write("([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); + w.Write("\")] object _, ReadOnlySpan<"); + w.Write(elementProjected); + w.Write("> span, uint length, void** data);\n"); + w.Write(callIndent); + w.Write("CopyToUnmanaged_"); + w.Write(raw); + w.Write("(null, "); + w.Write(pname); + w.Write(", (uint)"); + w.Write(pname); + w.Write(".Length, (void**)_"); + w.Write(raw); + w.Write(");\n"); + } + } + + w.Write(callIndent); + // delegate* signature: void*, then each ABI param type, then [void*, void**] (composable), + // then void**, then int. + w.Write("RestrictedErrorInfo.ThrowExceptionForHR((*(delegate* unmanaged[MemberFunction]**)ThisPtr)["); + w.Write((6 + factoryMethodIndex).ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write("](ThisPtr"); + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + string raw = p.Parameter.Name ?? "param"; + string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; + w.Write(",\n "); + if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) + { + w.Write("(uint)"); + w.Write(pname); + w.Write(".Length, _"); + w.Write(raw); + continue; + } + // For enums, cast to underlying type. For bool, cast to byte. For char, cast to ushort. + // For string params, use the marshalled HString from the fixed block. + // For runtime class / object / generic instance params, use __.GetThisPtrUnsafe(). + if (IsEnumType(p.Type)) + { + // No cast needed: function pointer signature uses the projected enum type. + w.Write(pname); + } + else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibBool && + corlibBool.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) + { + w.Write(pname); + } + else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibChar && + corlibChar.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) + { + w.Write(pname); + } + else if (IsString(p.Type)) + { + w.Write("__"); + w.Write(raw); + w.Write(".HString"); + } + else if (IsSystemType(p.Type)) + { + w.Write("__"); + w.Write(raw); + w.Write(".ConvertToUnmanagedUnsafe()"); + } + else if (IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type) || IsGenericInstance(p.Type)) + { + w.Write("__"); + w.Write(raw); + w.Write(".GetThisPtrUnsafe()"); + } + else if (IsMappedAbiValueType(p.Type)) + { + w.Write("__"); + w.Write(raw); + } + else if (IsHResultException(p.Type)) + { + w.Write("__"); + w.Write(raw); + } + else + { + w.Write(pname); + } + } + if (isComposable) + { + // Pass __baseInterface.GetThisPtrUnsafe() and &__innerInterface. + w.Write(",\n __baseInterface.GetThisPtrUnsafe(),\n &__innerInterface"); + } + w.Write(",\n &__retval));\n"); + if (isComposable) + { + w.Write(callIndent); + w.Write("innerInterface = __innerInterface;\n"); + } + w.Write(callIndent); + w.Write("retval = __retval;\n"); + + // Close fixed blocks (innermost first). + for (int i = fixedNesting - 1; i >= 0; i--) + { + string indent = baseIndent + new string(' ', i * 4); + w.Write(indent); + w.Write("}\n"); + } + + // Close try and emit finally with cleanup for non-blittable PassArray params. + if (hasNonBlittableArray) + { + w.Write(" }\n finally\n {\n"); + for (int i = 0; i < paramCount; i++) + { + ParamInfo p = sig.Params[i]; + ParamCategory cat = ParamHelpers.GetParamCategory(p); + if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } + if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } + if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } + string raw = p.Parameter.Name ?? "param"; + if (IsString(szArr.BaseType)) + { + w.Write("\n HStringArrayMarshaller.Dispose(__"); + w.Write(raw); + w.Write("_pinnedHandleSpan);\n\n"); + w.Write(" if (__"); + w.Write(raw); + w.Write("_pinnedHandleArrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); + w.Write(raw); + w.Write("_pinnedHandleArrayFromPool);\n }\n\n"); + w.Write(" if (__"); + w.Write(raw); + w.Write("_headerArrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); + w.Write(raw); + w.Write("_headerArrayFromPool);\n }\n"); + } + else + { + string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + w.Write("\n [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"Dispose\")]\n"); + w.Write(" static extern void Dispose_"); + w.Write(raw); + w.Write("([UnsafeAccessorType(\""); + w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); + w.Write("\")] object _, uint length, void** data);\n\n"); + w.Write(" fixed(void* _"); + w.Write(raw); + w.Write(" = __"); + w.Write(raw); + w.Write("_span)\n {\n"); + w.Write(" Dispose_"); + w.Write(raw); + w.Write("(null, (uint) __"); + w.Write(raw); + w.Write("_span.Length, (void**)_"); + w.Write(raw); + w.Write(");\n }\n"); + } + w.Write("\n if (__"); + w.Write(raw); + w.Write("_arrayFromPool is not null)\n {\n"); + w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); + w.Write(raw); + w.Write("_arrayFromPool);\n }\n"); + } + w.Write(" }\n"); + } + + w.Write(" }\n}\n"); + } + + /// Returns the IID expression for the class's default interface. + private static string GetDefaultInterfaceIid(TypeWriter w, TypeDefinition classType) + { + ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(classType); + if (defaultIface is null) { return "default(global::System.Guid)"; } + return w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, defaultIface))); + } + + /// + /// Mirrors C++ write_composable_constructors. + /// Emits: + /// 1. Public/protected constructors for each composable factory method (with proper body). + /// 2. Static factory callback class (per ctor) for parameterized composable activation. + /// 3. Four protected base-chaining constructors used by derived projected types. + /// + public static void WriteComposableConstructors(TypeWriter w, TypeDefinition? composableType, TypeDefinition classType, string visibility) + { + if (composableType is null) { return; } + string typeName = classType.Name?.Value ?? string.Empty; + + // Emit the factory objref + IIDs at the top so the parameterized ctors can reference it. + if (composableType.Methods.Count > 0) + { + string runtimeClassFullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; + string factoryObjRefName = GetObjRefName(w, composableType); + WriteStaticFactoryObjRef(w, composableType, runtimeClassFullName, factoryObjRefName); + } + + string defaultIfaceIid = GetDefaultInterfaceIid(w, classType); + string marshalingType = GetMarshalingTypeName(classType); + string defaultIfaceObjRef; + ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(classType); + defaultIfaceObjRef = defaultIface is not null ? GetObjRefName(w, defaultIface) : string.Empty; + int gcPressure = GetGcPressureAmount(classType); + // Compute the platform attribute string from the composable factory interface's + // [ContractVersion] attribute. Mirrors C++ code_writers.h:3167 + // 'auto platform_attribute = write_platform_attribute_temp(w, composable_type);' + // emitted at line 3179 before the public ctor. + string platformAttribute = w.WriteTemp("%", new System.Action(_ => WritePlatformAttribute(w, composableType))); + + int methodIndex = 0; + foreach (MethodDefinition method in composableType.Methods) + { + if (Helpers.IsSpecial(method)) { methodIndex++; continue; } + // Composable factory methods have signature like: + // T CreateInstance(args, object baseInterface, out object innerInterface) + // For the constructor on the projected class, we exclude the trailing two params. + MethodSig sig = new(method); + int userParamCount = sig.Params.Count >= 2 ? sig.Params.Count - 2 : sig.Params.Count; + // Mirror C++ write_constructor_callback_method_name (code_writers.h:2635-2643): + // the callback / args type name suffix is the TOTAL ABI param count + // (size(method.Signature().Params())), NOT the user-visible param count. Using the + // total count guarantees uniqueness against other composable factory overloads that + // might share the same user-param count but differ in trailing baseInterface shape. + string callbackName = (method.Name?.Value ?? "Create") + "_" + sig.Params.Count.ToString(System.Globalization.CultureInfo.InvariantCulture); + string argsName = callbackName + "Args"; + bool isParameterless = userParamCount == 0; + + w.Write("\n"); + if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } + w.Write(visibility); + if (!isParameterless) { w.Write(" unsafe "); } else { w.Write(" "); } + w.Write(typeName); + w.Write("("); + for (int i = 0; i < userParamCount; i++) + { + if (i > 0) { w.Write(", "); } + WriteProjectionParameter(w, sig.Params[i]); + } + w.Write(")\n :base("); + if (isParameterless) + { + // base(default(WindowsRuntimeActivationTypes.DerivedComposed), , , ) + string factoryObjRef = GetObjRefName(w, composableType); + w.Write("default(WindowsRuntimeActivationTypes.DerivedComposed), "); + w.Write(factoryObjRef); + w.Write(", "); + w.Write(defaultIfaceIid); + w.Write(", "); + w.Write(marshalingType); + } + else + { + w.Write(callbackName); + w.Write(".Instance, "); + w.Write(defaultIfaceIid); + w.Write(", "); + w.Write(marshalingType); + w.Write(", WindowsRuntimeActivationArgsReference.CreateUnsafe(new "); + w.Write(argsName); + w.Write("("); + for (int i = 0; i < userParamCount; i++) + { + if (i > 0) { w.Write(", "); } + string raw = sig.Params[i].Parameter.Name ?? "param"; + w.Write(Helpers.IsKeyword(raw) ? "@" + raw : raw); + } + w.Write("))"); + } + w.Write(")\n{\n"); + w.Write("if (GetType() == typeof("); + w.Write(typeName); + w.Write("))\n{\n"); + if (!string.IsNullOrEmpty(defaultIfaceObjRef)) + { + w.Write(defaultIfaceObjRef); + w.Write(" = NativeObjectReference;\n"); + } + w.Write("}\n"); + if (gcPressure > 0) + { + w.Write("GC.AddMemoryPressure("); + w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); + w.Write(");\n"); + } + w.Write("}\n"); + + // Emit args struct + callback class for parameterized composable factories. + // Mirrors C++ write_static_composing_factory_method (code_writers.h:6886) which + // skips both the args struct AND the callback class entirely in ref mode. The + // public ctor above still references these types, but reference assemblies don't + // need their bodies' references to resolve (only the public API surface matters). + if (!isParameterless && !w.Settings.ReferenceProjection) + { + EmitFactoryArgsStruct(w, sig, argsName, userParamCount); + string factoryObjRefName = GetObjRefName(w, composableType); + EmitFactoryCallbackClass(w, sig, callbackName, argsName, factoryObjRefName, methodIndex, isComposable: true, userParamCount: userParamCount); + } + + methodIndex++; + } + + if (w.Settings.ReferenceProjection) { return; } + + // Emit the four base-chaining constructors used by derived projected types. + string gcPressureBody = gcPressure > 0 + ? "GC.AddMemoryPressure(" + gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture) + ");\n" + : string.Empty; + + // 1. WindowsRuntimeActivationTypes.DerivedComposed + w.Write("\nprotected "); + w.Write(typeName); + w.Write("(WindowsRuntimeActivationTypes.DerivedComposed _, WindowsRuntimeObjectReference activationFactoryObjectReference, in Guid iid, CreateObjectReferenceMarshalingType marshalingType)\n"); + w.Write(" :base(_, activationFactoryObjectReference, in iid, marshalingType)\n"); + w.Write("{\n"); + if (!string.IsNullOrEmpty(gcPressureBody)) { w.Write(gcPressureBody); } + w.Write("}\n"); + + // 2. WindowsRuntimeActivationTypes.DerivedSealed + w.Write("\nprotected "); + w.Write(typeName); + w.Write("(WindowsRuntimeActivationTypes.DerivedSealed _, WindowsRuntimeObjectReference activationFactoryObjectReference, in Guid iid, CreateObjectReferenceMarshalingType marshalingType)\n"); + w.Write(" :base(_, activationFactoryObjectReference, in iid, marshalingType)\n"); + w.Write("{\n"); + if (!string.IsNullOrEmpty(gcPressureBody)) { w.Write(gcPressureBody); } + w.Write("}\n"); + + // 3. WindowsRuntimeActivationFactoryCallback.DerivedComposed + w.Write("\nprotected "); + w.Write(typeName); + w.Write("(WindowsRuntimeActivationFactoryCallback.DerivedComposed activationFactoryCallback, in Guid iid, CreateObjectReferenceMarshalingType marshalingType, WindowsRuntimeActivationArgsReference additionalParameters)\n"); + w.Write(" :base(activationFactoryCallback, in iid, marshalingType, additionalParameters)\n"); + w.Write("{\n"); + if (!string.IsNullOrEmpty(gcPressureBody)) { w.Write(gcPressureBody); } + w.Write("}\n"); + + // 4. WindowsRuntimeActivationFactoryCallback.DerivedSealed + w.Write("\nprotected "); + w.Write(typeName); + w.Write("(WindowsRuntimeActivationFactoryCallback.DerivedSealed activationFactoryCallback, in Guid iid, CreateObjectReferenceMarshalingType marshalingType, WindowsRuntimeActivationArgsReference additionalParameters)\n"); + w.Write(" :base(activationFactoryCallback, in iid, marshalingType, additionalParameters)\n"); + w.Write("{\n"); + if (!string.IsNullOrEmpty(gcPressureBody)) { w.Write(gcPressureBody); } + w.Write("}\n"); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.CustomAttributes.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.CustomAttributes.cs new file mode 100644 index 0000000000..b18f5bd857 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.CustomAttributes.cs @@ -0,0 +1,355 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Custom attribute carry-over and platform attribute helpers. Mirrors C++ functions +/// in code_writers.h for write_custom_attributes, write_custom_attribute_args, +/// write_platform_attribute, get_platform, etc. +/// +internal static partial class CodeWriters +{ + /// + /// Mirrors C++ write_custom_attribute_args. Returns the formatted argument strings. + /// + public static List WriteCustomAttributeArgs(TypeWriter w, CustomAttribute attribute) + { + List result = new(); + if (attribute.Signature is null) { return result; } + + // Detect AttributeUsage which takes an AttributeTargets enum + ITypeDefOrRef? attrType = attribute.Constructor?.DeclaringType; + bool isAttributeUsage = attrType?.Name == "AttributeUsageAttribute" || + attrType?.Name == "AttributeUsage"; + + for (int i = 0; i < attribute.Signature.FixedArguments.Count; i++) + { + CustomAttributeArgument arg = attribute.Signature.FixedArguments[i]; + uint? targetsValue = null; + if (isAttributeUsage && i == 0) + { + if (arg.Element is uint u) { targetsValue = u; } + else if (arg.Element is int s) { targetsValue = unchecked((uint)s); } + } + if (targetsValue is uint tv) + { + result.Add(FormatAttributeTargets(tv)); + } + else + { + result.Add(FormatCustomAttributeArg(w, arg)); + } + } + for (int i = 0; i < attribute.Signature.NamedArguments.Count; i++) + { + CustomAttributeNamedArgument named = attribute.Signature.NamedArguments[i]; + result.Add(named.MemberName?.Value + " = " + FormatCustomAttributeArg(w, named.Argument)); + } + return result; + } + + /// + /// Formats an AttributeTargets uint value as a bitwise OR of global::System.AttributeTargets.X. + /// Mirrors the C++ AttributeTargets handling in write_custom_attribute_args. + /// + private static string FormatAttributeTargets(uint value) + { + if (value == 0xFFFFFFFFu) + { + return "global::System.AttributeTargets.All"; + } + // Map each bit to its corresponding enum name. Includes WinMD-specific values + // that map to the same .NET enum (e.g., RuntimeClass=512 -> Class, ApiContract=8192 -> Struct). + (uint Bit, string Name)[] entries = + { + (1, "Delegate"), + (2, "Enum"), + (4, "Event"), + (8, "Field"), + (16, "Interface"), + (64, "Method"), + (128, "Parameter"), + (256, "Property"), + (512, "Class"), // RuntimeClass + (1024, "Struct"), + (2048, "All"), // InterfaceImpl - not directly representable, use All + (8192, "Struct"), // ApiContract -> Struct + }; + List values = new(); + foreach ((uint bit, string name) in entries) + { + if ((value & bit) != 0) + { + values.Add("global::System.AttributeTargets." + name); + } + } + if (values.Count == 0) + { + return "global::System.AttributeTargets.All"; + } + return string.Join(" | ", values); + } + + private static string FormatCustomAttributeArg(TypeWriter w, CustomAttributeArgument arg) + { + // The arg can hold scalar, type, enum or string values. + object? element = arg.Element; + return element switch + { + null => "null", + string s => "@\"" + EscapeVerbatimString(s) + "\"", + AsmResolver.Utf8String us => "@\"" + EscapeVerbatimString(us.Value) + "\"", + bool b => b ? "true" : "false", + byte by => by.ToString(CultureInfo.InvariantCulture), + sbyte sb => sb.ToString(CultureInfo.InvariantCulture), + short sh => sh.ToString(CultureInfo.InvariantCulture), + ushort us2 => us2.ToString(CultureInfo.InvariantCulture), + int i => i.ToString(CultureInfo.InvariantCulture), + uint ui => ui.ToString(CultureInfo.InvariantCulture) + "u", + long l => l.ToString(CultureInfo.InvariantCulture), + ulong ul => ul.ToString(CultureInfo.InvariantCulture) + "ul", + float f => f.ToString("R", CultureInfo.InvariantCulture) + "f", + double d => d.ToString("R", CultureInfo.InvariantCulture), + char c => "'" + c + "'", + // Always prepend 'global::' to typeof() arguments. The C++ cswinrt tool does this for the + // same reason: when the generated file's namespace context happens to contain a 'Windows' + // sub-namespace (e.g. 'TestComponentCSharp.Windows.*'), an unqualified 'Windows.Foundation.X' + // would resolve to 'TestComponentCSharp.Windows.Foundation.X' first under C# name lookup + // and fail with CS0234. The 'global::' prefix forces fully-qualified resolution. + TypeSignature ts when ts.FullName is { Length: > 0 } fn => "typeof(global::" + fn + ")", + TypeSignature => "typeof(object)", + _ => element.ToString() ?? "null" + }; + } + + /// + /// Escapes a string for use inside a C# verbatim string literal (@"..."). + /// Mirrors C++ write_custom_attribute_args string emission (code_writers.h:2401-2427): + /// the WinMD attribute string value carries source-level escape sequences (e.g. \" + /// for an embedded quote). The C++ tool un-escapes these before emitting a verbatim string, + /// so a WinMD value of \"quotes\" becomes the verbatim source text ""quotes"" + /// (which decodes to "quotes" at runtime). + /// Logic: + /// - \ followed by \ / ' / ": drop the backslash, keep the char. + /// - \ followed by anything else: keep both \ and the char. + /// - Each emitted " is doubled ("") per verbatim-string escape rules. + /// + private static string EscapeVerbatimString(string s) + { + StringBuilder sb = new(s.Length); + bool prevEscape = false; + foreach (char c in s) + { + if (c == '\\' && !prevEscape) + { + prevEscape = true; + continue; + } + if (prevEscape && c != '\\' && c != '\'' && c != '"') + { + sb.Append('\\'); + } + prevEscape = false; + sb.Append(c); + if (c == '"') { sb.Append('"'); } + } + if (prevEscape) { sb.Append('\\'); } + return sb.ToString(); + } + + /// + /// Mirrors C++ get_platform(writer&, CustomAttribute): returns the formatted + /// SupportedOSPlatform string ("WindowsX.Y.Z.0") for a [ContractVersion] attribute, + /// or empty if no platform mapping exists. Honors writer's + /// state to deduplicate platforms within a single class scope (mirrors C++ + /// _check_platform / _platform behavior in code_writers.h:2515-2525). + /// + private static string GetPlatform(TypeWriter w, CustomAttribute attribute) + { + if (attribute.Signature is null || attribute.Signature.FixedArguments.Count < 2) + { + return string.Empty; + } + CustomAttributeArgument arg0 = attribute.Signature.FixedArguments[0]; + string contractName; + if (arg0.Element is TypeSignature ts && ts.FullName is { } fn) + { + contractName = fn; + } + else if (arg0.Element is string s) + { + contractName = s; + } + else if (arg0.Element is not null) + { + // AsmResolver returns Utf8String for string custom-attribute args. + contractName = arg0.Element.ToString() ?? string.Empty; + if (contractName.Length == 0) { return string.Empty; } + } + else + { + return string.Empty; + } + + // The version is a uint where the top 16 bits are the major version + CustomAttributeArgument arg1 = attribute.Signature.FixedArguments[1]; + uint versionRaw = arg1.Element switch + { + uint u => u, + int i => (uint)i, + _ => 0u + }; + int contractVersion = (int)(versionRaw >> 16); + + string platform = ContractPlatforms.GetPlatform(contractName, contractVersion); + if (string.IsNullOrEmpty(platform)) { return string.Empty; } + if (w.CheckPlatform) + { + // Suppress when this platform is <= the previously seen platform for the class. + if (string.CompareOrdinal(platform, w.Platform) <= 0) + { + return string.Empty; + } + // Only seed _platform on first non-empty observation (matches C++ behavior: + // higher platforms emit but don't update _platform). + if (w.Platform.Length == 0) + { + w.Platform = platform; + } + } + return "\"Windows" + platform + "\""; + } + + /// + /// Mirrors C++ write_platform_attribute: emits [SupportedOSPlatform("WindowsX.Y.Z.0")] + /// for a [ContractVersion] attribute. Only writes for reference projection. + /// + public static void WritePlatformAttribute(TypeWriter w, IHasCustomAttribute member) + { + if (!w.Settings.ReferenceProjection) { return; } + for (int i = 0; i < member.CustomAttributes.Count; i++) + { + CustomAttribute attr = member.CustomAttributes[i]; + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + if (attrType is null) { continue; } + string name = attrType.Name?.Value ?? string.Empty; + // Strip 'Attribute' suffix + if (name.EndsWith("Attribute", System.StringComparison.Ordinal)) + { + name = name.Substring(0, name.Length - "Attribute".Length); + } + if (name == "ContractVersion" && attr.Signature?.FixedArguments.Count == 2) + { + string platform = GetPlatform(w, attr); + if (!string.IsNullOrEmpty(platform)) + { + w.Write("[global::System.Runtime.Versioning.SupportedOSPlatform("); + w.Write(platform); + w.Write(")]\n"); + return; + } + } + } + } + + /// + /// Mirrors C++ write_custom_attributes: carries selected custom attributes + /// to the projection (e.g., [Obsolete], [Deprecated], [SupportedOSPlatform]). + /// + public static void WriteCustomAttributes(TypeWriter w, IHasCustomAttribute member, bool enablePlatformAttrib) + { + Dictionary> attributes = new(System.StringComparer.Ordinal); + bool allowMultiple = false; + + for (int i = 0; i < member.CustomAttributes.Count; i++) + { + CustomAttribute attr = member.CustomAttributes[i]; + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + if (attrType is null) { continue; } + string ns = attrType.Namespace?.Value ?? string.Empty; + string name = attrType.Name?.Value ?? string.Empty; + // Strip 'Attribute' suffix + string strippedName = name.EndsWith("Attribute", System.StringComparison.Ordinal) + ? name.Substring(0, name.Length - "Attribute".Length) + : name; + + // Skip attributes handled separately + if (strippedName is "GCPressure" or "Guid" or "Flags" or "ProjectionInternal") { continue; } + + string fullAttrName = strippedName == "AttributeUsage" + ? "System.AttributeUsage" + : ns + "." + strippedName; + + List args = WriteCustomAttributeArgs(w, attr); + + if (w.Settings.ReferenceProjection && enablePlatformAttrib && strippedName == "ContractVersion" && attr.Signature?.FixedArguments.Count == 2) + { + string platform = GetPlatform(w, attr); + if (!string.IsNullOrEmpty(platform)) + { + if (!attributes.TryGetValue("System.Runtime.Versioning.SupportedOSPlatform", out List? list)) + { + list = new List(); + attributes["System.Runtime.Versioning.SupportedOSPlatform"] = list; + } + list.Add(platform); + } + } + + // Skip metadata attributes without a projection + if (ns == "Windows.Foundation.Metadata") + { + if (strippedName == "AllowMultiple") + { + allowMultiple = true; + } + if (strippedName == "ContractVersion") + { + if (!w.Settings.ReferenceProjection) { continue; } + } + else if (strippedName is not ("DefaultOverload" or "Overload" or "AttributeUsage" or "Experimental")) + { + continue; + } + } + + attributes[fullAttrName] = args; + } + + // Add AllowMultiple to AttributeUsage if needed + if (attributes.TryGetValue("System.AttributeUsage", out List? usage)) + { + usage.Add("AllowMultiple = " + (allowMultiple ? "true" : "false")); + } + + foreach (KeyValuePair> kv in attributes) + { + w.Write("[global::"); + w.Write(kv.Key); + if (kv.Value.Count > 0) + { + w.Write("("); + for (int i = 0; i < kv.Value.Count; i++) + { + if (i > 0) { w.Write(", "); } + w.Write(kv.Value[i]); + } + w.Write(")"); + } + w.Write("]\n"); + } + } + + /// Mirrors C++ write_type_custom_attributes. + public static void WriteTypeCustomAttributes(TypeWriter w, TypeDefinition type, bool enablePlatformAttrib) + { + WriteCustomAttributes(w, type, enablePlatformAttrib); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Guids.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Guids.cs new file mode 100644 index 0000000000..9ccba022c2 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Guids.cs @@ -0,0 +1,408 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using System.Text.RegularExpressions; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// GUID/IID-related code writers, mirroring functions in code_writers.h. +/// +internal static partial class CodeWriters +{ + /// Mirrors C++ get_fundamental_type_guid_signature. + public static string GetFundamentalTypeGuidSignature(FundamentalType t) => t switch + { + FundamentalType.Boolean => "b1", + FundamentalType.Char => "c2", + FundamentalType.Int8 => "i1", + FundamentalType.UInt8 => "u1", + FundamentalType.Int16 => "i2", + FundamentalType.UInt16 => "u2", + FundamentalType.Int32 => "i4", + FundamentalType.UInt32 => "u4", + FundamentalType.Int64 => "i8", + FundamentalType.UInt64 => "u8", + FundamentalType.Float => "f4", + FundamentalType.Double => "f8", + FundamentalType.String => "string", + _ => throw new InvalidOperationException("Unknown fundamental type") + }; + + private static readonly Regex s_typeNameEscapeRe = new(@"[ :<>`,.]", RegexOptions.Compiled); + + /// Mirrors C++ escape_type_name_for_identifier. + public static string EscapeTypeNameForIdentifier(string typeName, bool stripGlobal = false, bool stripGlobalABI = false) + { + // Match C++ behavior: escape special chars first, then strip ONLY the prefix (not all + // occurrences). C++ uses rfind(prefix, 0) + erase(0, len) which only removes the prefix. + string result = s_typeNameEscapeRe.Replace(typeName, "_"); + if (stripGlobalABI && typeName.StartsWith("global::ABI.", StringComparison.Ordinal)) + { + result = result.Substring(12); // Remove "global::ABI." (with ":" and "." already replaced) + } + else if (stripGlobal && typeName.StartsWith("global::", StringComparison.Ordinal)) + { + result = result.Substring(8); // Remove "global::" + } + return result; + } + + /// + /// Reads the GUID values from the [GuidAttribute] of the type and returns them as a tuple. + /// + public static (uint Data1, ushort Data2, ushort Data3, byte[] Data4)? GetGuidFields(TypeDefinition type) + { + CustomAttribute? attr = TypeCategorization.GetAttribute(type, "Windows.Foundation.Metadata", "GuidAttribute"); + if (attr is null || attr.Signature is null) { return null; } + var args = attr.Signature.FixedArguments; + if (args.Count < 11) { return null; } + + uint data1 = ToUInt32(args[0].Element); + ushort data2 = ToUInt16(args[1].Element); + ushort data3 = ToUInt16(args[2].Element); + byte[] data4 = new byte[8]; + for (int i = 0; i < 8; i++) + { + data4[i] = ToByte(args[3 + i].Element); + } + return (data1, data2, data3, data4); + + static uint ToUInt32(object? v) => v switch + { + uint u => u, + int i => (uint)i, + _ => 0u + }; + static ushort ToUInt16(object? v) => v switch + { + ushort u => u, + short s => (ushort)s, + int i => (ushort)i, + _ => (ushort)0 + }; + static byte ToByte(object? v) => v switch + { + byte b => b, + sbyte sb => (byte)sb, + int i => (byte)i, + _ => (byte)0 + }; + } + + /// Mirrors C++ write_guid. + public static void WriteGuid(TextWriter w, TypeDefinition type, bool lowerCase) + { + var fields = GetGuidFields(type) ?? throw new InvalidOperationException( + $"'Windows.Foundation.Metadata.GuidAttribute' attribute for type '{type.Namespace}.{type.Name}' not found"); + string fmt = lowerCase ? "x" : "X"; + // Format: %08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x + w.Write(fields.Data1.ToString(fmt + "8", CultureInfo.InvariantCulture)); + w.Write("-"); + w.Write(fields.Data2.ToString(fmt + "4", CultureInfo.InvariantCulture)); + w.Write("-"); + w.Write(fields.Data3.ToString(fmt + "4", CultureInfo.InvariantCulture)); + w.Write("-"); + for (int i = 0; i < 2; i++) { w.Write(fields.Data4[i].ToString(fmt + "2", CultureInfo.InvariantCulture)); } + w.Write("-"); + for (int i = 2; i < 8; i++) { w.Write(fields.Data4[i].ToString(fmt + "2", CultureInfo.InvariantCulture)); } + } + + /// Mirrors C++ write_guid_bytes. + public static void WriteGuidBytes(TextWriter w, TypeDefinition type) + { + var fields = GetGuidFields(type) ?? throw new InvalidOperationException( + $"'Windows.Foundation.Metadata.GuidAttribute' attribute for type '{type.Namespace}.{type.Name}' not found"); + WriteByte(w, (fields.Data1 >> 0) & 0xFF, true); + WriteByte(w, (fields.Data1 >> 8) & 0xFF, false); + WriteByte(w, (fields.Data1 >> 16) & 0xFF, false); + WriteByte(w, (fields.Data1 >> 24) & 0xFF, false); + WriteByte(w, (uint)((fields.Data2 >> 0) & 0xFF), false); + WriteByte(w, (uint)((fields.Data2 >> 8) & 0xFF), false); + WriteByte(w, (uint)((fields.Data3 >> 0) & 0xFF), false); + WriteByte(w, (uint)((fields.Data3 >> 8) & 0xFF), false); + for (int i = 0; i < 8; i++) { WriteByte(w, fields.Data4[i], false); } + } + + private static void WriteByte(TextWriter w, uint b, bool first) + { + if (!first) { w.Write(", "); } + w.Write("0x"); + w.Write((b & 0xFF).ToString("X", CultureInfo.InvariantCulture)); + } + + /// Mirrors C++ write_iid_guid_property_name. + public static void WriteIidGuidPropertyName(TypeWriter w, TypeDefinition type) + { + string name = w.WriteTemp("%", new Action(_ => + { + WriteTypedefName(w, type, TypedefNameType.ABI, true); + WriteTypeParams(w, type); + })); + name = EscapeTypeNameForIdentifier(name, true, true); + w.Write("IID_"); + w.Write(name); + } + + /// Mirrors C++ write_iid_reference_guid_property_name. + public static void WriteIidReferenceGuidPropertyName(TypeWriter w, TypeDefinition type) + { + string name = w.WriteTemp("%", new Action(_ => + { + WriteTypedefName(w, type, TypedefNameType.ABI, true); + WriteTypeParams(w, type); + })); + name = EscapeTypeNameForIdentifier(name, true, true); + w.Write("IID_"); + w.Write(name); + w.Write("Reference"); + } + + /// Mirrors C++ write_iid_guid_property_from_type. + public static void WriteIidGuidPropertyFromType(TypeWriter w, TypeDefinition type) + { + w.Write("public static ref readonly Guid "); + WriteIidGuidPropertyName(w, type); + w.Write("\n{\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get\n {\n ReadOnlySpan data =\n [\n "); + WriteGuidBytes(w, type); + w.Write("\n ];\n return ref Unsafe.As(ref MemoryMarshal.GetReference(data));\n }\n}\n\n"); + } + + /// + /// Mirrors C++ write_guid_signature. + /// + public static void WriteGuidSignature(TypeWriter w, TypeSemantics semantics) + { + switch (semantics) + { + case TypeSemantics.Guid_: + w.Write("g16"); + break; + case TypeSemantics.Object_: + w.Write("cinterface(IInspectable)"); + break; + case TypeSemantics.Fundamental f: + w.Write(GetFundamentalTypeGuidSignature(f.Type)); + break; + case TypeSemantics.Definition d: + WriteGuidSignatureForType(w, d.Type); + break; + case TypeSemantics.Reference r: + { + // Resolve the reference to a TypeDefinition (cross-module struct field, etc.). + // Mirrors C++ for_typedef which always succeeds in resolving here. + string ns = r.Reference_.Namespace?.Value ?? string.Empty; + string name = r.Reference_.Name?.Value ?? string.Empty; + TypeDefinition? resolved = null; + if (_cacheRef is not null) + { + try { resolved = r.Reference_.Resolve(_cacheRef.RuntimeContext); } + catch { resolved = null; } + resolved ??= _cacheRef.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); + } + if (resolved is not null) + { + WriteGuidSignatureForType(w, resolved); + } + } + break; + case TypeSemantics.GenericInstance gi: + w.Write("pinterface({"); + WriteGuid(w, gi.GenericType, true); + w.Write("};"); + for (int i = 0; i < gi.GenericArgs.Count; i++) + { + if (i > 0) { w.Write(";"); } + WriteGuidSignature(w, gi.GenericArgs[i]); + } + w.Write(")"); + break; + case TypeSemantics.GenericInstanceRef gir: + { + // Cross-module generic instance (e.g. Windows.Foundation.IReference + // appearing as a struct field). Resolve the generic type to a TypeDefinition + // so we can extract its [Guid]; recurse on each type argument. + string ns = gir.GenericType.Namespace?.Value ?? string.Empty; + string name = gir.GenericType.Name?.Value ?? string.Empty; + TypeDefinition? resolved = null; + if (_cacheRef is not null) + { + try { resolved = gir.GenericType.Resolve(_cacheRef.RuntimeContext); } + catch { resolved = null; } + resolved ??= _cacheRef.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); + } + if (resolved is not null) + { + w.Write("pinterface({"); + WriteGuid(w, resolved, true); + w.Write("};"); + for (int i = 0; i < gir.GenericArgs.Count; i++) + { + if (i > 0) { w.Write(";"); } + WriteGuidSignature(w, gir.GenericArgs[i]); + } + w.Write(")"); + } + } + break; + } + } + + private static void WriteGuidSignatureForType(TypeWriter w, TypeDefinition type) + { + TypeCategory cat = TypeCategorization.GetCategory(type); + switch (cat) + { + case TypeCategory.Enum: + w.Write("enum("); + WriteTypedefName(w, type, TypedefNameType.NonProjected, true); + WriteTypeParams(w, type); + w.Write(";"); + w.Write(TypeCategorization.IsFlagsEnum(type) ? "u4" : "i4"); + w.Write(")"); + break; + case TypeCategory.Struct: + w.Write("struct("); + WriteTypedefName(w, type, TypedefNameType.NonProjected, true); + WriteTypeParams(w, type); + w.Write(";"); + bool first = true; + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic) { continue; } + if (field.Signature is null) { continue; } + if (!first) { w.Write(";"); } + first = false; + WriteGuidSignature(w, TypeSemanticsFactory.Get(field.Signature.FieldType)); + } + w.Write(")"); + break; + case TypeCategory.Delegate: + w.Write("delegate({"); + WriteGuid(w, type, true); + w.Write("})"); + break; + case TypeCategory.Interface: + w.Write("{"); + WriteGuid(w, type, true); + w.Write("}"); + break; + case TypeCategory.Class: + ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); + if (defaultIface is TypeDefinition di) + { + w.Write("rc("); + WriteTypedefName(w, type, TypedefNameType.NonProjected, true); + WriteTypeParams(w, type); + w.Write(";"); + WriteGuidSignature(w, new TypeSemantics.Definition(di)); + w.Write(")"); + } + else + { + w.Write("{"); + WriteGuid(w, type, true); + w.Write("}"); + } + break; + } + } + + /// Mirrors C++ write_iid_guid_property_from_signature. + public static void WriteIidGuidPropertyFromSignature(TypeWriter w, TypeDefinition type) + { + string guidSig = w.WriteTemp("%", new Action(_ => + { + WriteGuidSignature(w, new TypeSemantics.Definition(type)); + })); + string ireferenceGuidSig = "pinterface({61c17706-2d65-11e0-9ae8-d48564015472};" + guidSig + ")"; + Guid guidValue = GuidGenerator.Generate(ireferenceGuidSig); + byte[] bytes = guidValue.ToByteArray(); + + w.Write("public static ref readonly Guid "); + WriteIidReferenceGuidPropertyName(w, type); + w.Write("\n{\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get\n {\n ReadOnlySpan data =\n [\n "); + for (int i = 0; i < 16; i++) + { + if (i > 0) { w.Write(", "); } + w.Write("0x"); + w.Write(bytes[i].ToString("X", CultureInfo.InvariantCulture)); + } + w.Write("\n ];\n return ref Unsafe.As(ref MemoryMarshal.GetReference(data));\n }\n}\n\n"); + } + + /// Mirrors C++ write_iid_guid_property_for_class_interfaces. + public static void WriteIidGuidPropertyForClassInterfaces(TypeWriter w, TypeDefinition type, System.Collections.Generic.HashSet interfacesEmitted) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) { continue; } + // Resolve TypeRef → TypeDefinition via metadata cache (so we pick up cross-module + // inherited interfaces, e.g. Windows.UI.Composition.IAnimationObject from a XAML class). + TypeDefinition? ifaceType = impl.Interface as TypeDefinition; + if (ifaceType is null && impl.Interface is TypeReference tr) + { + string trNs = tr.Namespace?.Value ?? string.Empty; + string trNm = tr.Name?.Value ?? string.Empty; + ifaceType = ResolveCrossModuleType(trNs, trNm); + } + if (ifaceType is null) { continue; } + + string ns = ifaceType.Namespace?.Value ?? string.Empty; + string nm = ifaceType.Name?.Value ?? string.Empty; + // Skip mapped types + if (MappedTypes.Get(ns, nm) is not null) { continue; } + // Skip generic interfaces + if (ifaceType.GenericParameters.Count != 0) { continue; } + // Skip already-emitted + if (interfacesEmitted.Contains(ifaceType)) { continue; } + // Only emit if the interface is not in the projection (otherwise it'll be emitted naturally) + if (!w.Settings.Filter.Includes(ifaceType)) + { + WriteIidGuidPropertyFromType(w, ifaceType); + _ = interfacesEmitted.Add(ifaceType); + } + } + } + + private static TypeDefinition? ResolveCrossModuleType(string ns, string name) + { + if (_cacheRef is null) { return null; } + return _cacheRef.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); + } + + /// Writes the InterfaceIIDs file header (mirrors C++ write_begin_interface_iids in type_writers.h). + public static void WriteInterfaceIidsBegin(TextWriter w) + { + w.Write("\n"); + w.Write("//------------------------------------------------------------------------------\n"); + w.Write("// \n"); + w.Write("// This file was generated by cswinrt.exe version "); + w.Write(GetVersionString()); + w.Write("\n"); + w.Write("//\n"); + w.Write("// Changes to this file may cause incorrect behavior and will be lost if\n"); + w.Write("// the code is regenerated.\n"); + w.Write("// \n"); + w.Write("//------------------------------------------------------------------------------\n"); + w.Write(@" +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace ABI; + +internal static class InterfaceIIDs +{ +"); + } + + /// Writes the InterfaceIIDs file footer. + public static void WriteInterfaceIidsEnd(TextWriter w) + { + w.Write("}\n\n"); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Helpers.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Helpers.cs new file mode 100644 index 0000000000..58e0d13a54 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Helpers.cs @@ -0,0 +1,382 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Helper writers for assembly attributes, metadata attributes, and other infrastructure. +/// Mirrors various functions in code_writers.h. +/// +internal static partial class CodeWriters +{ + /// Mirrors C++ write_pragma_disable_IL2026. + public static void WritePragmaDisableIL2026(TextWriter w) + { + w.Write("\n#pragma warning disable IL2026\n"); + } + + /// Mirrors C++ write_pragma_restore_IL2026. + public static void WritePragmaRestoreIL2026(TextWriter w) + { + w.Write("\n#pragma warning restore IL2026\n"); + } + + /// + /// Returns the version string embedded in the banner comment of generated files. + /// Mirrors C++ VERSION_STRING (which is set via $(VersionString) from + /// MSBuild and defaults to 0.0.0-private.0). + /// + /// We read the writer assembly's + /// (set via $(InformationalVersion)) and strip any SourceLink commit-sha suffix + /// after a '+' so the banner is reproducible across rebuilds of the same source. + /// + internal static string GetVersionString() + { + System.Reflection.Assembly asm = typeof(CodeWriters).Assembly; + System.Reflection.AssemblyInformationalVersionAttribute? attr = + (System.Reflection.AssemblyInformationalVersionAttribute?)System.Attribute.GetCustomAttribute( + asm, typeof(System.Reflection.AssemblyInformationalVersionAttribute)); + string version = attr?.InformationalVersion ?? "0.0.0-private.0"; + int plus = version.IndexOf('+'); + return plus >= 0 ? version.Substring(0, plus) : version; + } + + /// Mirrors C++ write_file_header. + public static void WriteFileHeader(TextWriter w) + { + w.Write("//------------------------------------------------------------------------------\n"); + w.Write("// \n"); + w.Write("// This file was generated by cswinrt.exe version "); + w.Write(GetVersionString()); + w.Write("\n"); + w.Write("//\n"); + w.Write("// Changes to this file may cause incorrect behavior and will be lost if\n"); + w.Write("// the code is regenerated.\n"); + w.Write("// \n"); + w.Write("//------------------------------------------------------------------------------\n"); + } + + /// Mirrors C++ write_winrt_metadata_attribute. + public static void WriteWinRTMetadataAttribute(TypeWriter w, TypeDefinition type, MetadataCache cache) + { + string path = cache.GetSourcePath(type); + string stem = string.IsNullOrEmpty(path) ? string.Empty : Path.GetFileNameWithoutExtension(path); + w.Write("[WindowsRuntimeMetadata(\""); + w.Write(stem); + w.Write("\")]\n"); + } + + /// Mirrors C++ write_winrt_metadata_typename_attribute. + public static void WriteWinRTMetadataTypeNameAttribute(TypeWriter w, TypeDefinition type) + { + w.Write("[WindowsRuntimeMetadataTypeName(\""); + WriteTypedefName(w, type, TypedefNameType.NonProjected, true); + WriteTypeParams(w, type); + w.Write("\")]\n"); + } + + /// Mirrors C++ write_winrt_mapped_type_attribute. + public static void WriteWinRTMappedTypeAttribute(TypeWriter w, TypeDefinition type) + { + w.Write("[WindowsRuntimeMappedType(typeof("); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + WriteTypeParams(w, type); + w.Write("))]\n"); + } + + /// Mirrors C++ write_value_type_winrt_classname_attribute. + public static void WriteValueTypeWinRTClassNameAttribute(TypeWriter w, TypeDefinition type) + { + if (w.Settings.ReferenceProjection) { return; } + string ns = type.Namespace?.Value ?? string.Empty; + string name = type.Name?.Value ?? string.Empty; + w.Write("[WindowsRuntimeClassName(\"Windows.Foundation.IReference`1<"); + w.Write(ns); + w.Write("."); + w.Write(name); + w.Write(">\")]\n"); + } + + /// Mirrors C++ write_winrt_reference_type_attribute. + public static void WriteWinRTReferenceTypeAttribute(TypeWriter w, TypeDefinition type) + { + if (w.Settings.ReferenceProjection) { return; } + w.Write("[WindowsRuntimeReferenceType(typeof("); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + w.Write("?))]\n"); + } + + /// Mirrors C++ write_comwrapper_marshaller_attribute. + public static void WriteComWrapperMarshallerAttribute(TypeWriter w, TypeDefinition type) + { + if (w.Settings.ReferenceProjection) { return; } + string ns = type.Namespace?.Value ?? string.Empty; + string name = type.Name?.Value ?? string.Empty; + w.Write("[ABI."); + w.Write(ns); + w.Write("."); + w.Write(Helpers.StripBackticks(name)); + w.Write("ComWrappersMarshaller]\n"); + } + + /// + /// Mirrors C++ write_winrt_windowsmetadata_typemapgroup_assembly_attribute. + /// + public static void WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(TypeWriter w, TypeDefinition type) + { + // Skip exclusive interfaces and projection-internal interfaces + if (TypeCategorization.GetCategory(type) == TypeCategory.Interface && + (TypeCategorization.IsExclusiveTo(type) || TypeCategorization.IsProjectionInternal(type))) + { + return; + } + + string projectionName = w.WriteTemp("%", new Action(tw => + { + // Use a temporary TypeWriter for the typedef name with full namespace + WriteTypedefName(w, type, TypedefNameType.NonProjected, true); + WriteTypeParams(w, type); + })); + + w.Write("\n[assembly: TypeMap(\n value: \""); + w.Write(projectionName); + w.Write("\",\n target: typeof("); + if (w.Settings.Component) + { + WriteTypedefName(w, type, TypedefNameType.ABI, true); + WriteTypeParams(w, type); + } + else + { + w.Write(projectionName); + } + w.Write("),\n trimTarget: typeof("); + w.Write(projectionName); + w.Write("))]\n"); + + if (w.Settings.Component) + { + w.Write("\n[assembly: TypeMapAssociation(\n source: typeof("); + w.Write(projectionName); + w.Write("),\n proxy: typeof("); + WriteTypedefName(w, type, TypedefNameType.ABI, true); + WriteTypeParams(w, type); + w.Write("))]\n\n"); + } + } + + /// + /// Mirrors C++ write_winrt_comwrappers_typemapgroup_assembly_attribute. + /// + public static void WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(TypeWriter w, TypeDefinition type, bool isValueType) + { + string projectionName = w.WriteTemp("%", new Action(tw => + { + WriteTypedefName(w, type, TypedefNameType.NonProjected, true); + WriteTypeParams(w, type); + })); + + w.Write("\n[assembly: TypeMap(\n value: \""); + if (isValueType) + { + w.Write("Windows.Foundation.IReference`1<"); + w.Write(projectionName); + w.Write(">"); + } + else + { + w.Write(projectionName); + } + w.Write("\",\n target: typeof("); + if (w.Settings.Component) + { + WriteTypedefName(w, type, TypedefNameType.ABI, true); + WriteTypeParams(w, type); + } + else + { + w.Write(projectionName); + } + w.Write("),\n trimTarget: typeof("); + w.Write(projectionName); + w.Write("))]\n"); + + // For non-interface, non-struct authored types, emit proxy association. + TypeCategory cat = TypeCategorization.GetCategory(type); + if (cat != TypeCategory.Interface && cat != TypeCategory.Struct && w.Settings.Component) + { + w.Write("\n[assembly: TypeMapAssociation(\n source: typeof("); + w.Write(projectionName); + w.Write("),\n proxy: typeof("); + WriteTypedefName(w, type, TypedefNameType.ABI, true); + WriteTypeParams(w, type); + w.Write("))]\n\n"); + } + } + + /// + /// Mirrors C++ write_winrt_idic_typemapgroup_assembly_attribute. + /// + public static void WriteWinRTIdicTypeMapGroupAssemblyAttribute(TypeWriter w, TypeDefinition type) + { + // Generic interfaces are handled elsewhere + if (type.GenericParameters.Count != 0) { return; } + // Skip exclusive interfaces (unless idic_exclusiveto), and projection-internal + if ((TypeCategorization.IsExclusiveTo(type) && !w.Settings.IdicExclusiveTo) || + TypeCategorization.IsProjectionInternal(type)) + { + return; + } + + w.Write("\n[assembly: TypeMapAssociation(\n source: typeof("); + WriteTypedefName(w, type, TypedefNameType.Projected, true); + WriteTypeParams(w, type); + w.Write("),\n proxy: typeof("); + WriteTypedefName(w, type, TypedefNameType.ABI, true); + WriteTypeParams(w, type); + w.Write("))]\n\n"); + } + + /// + /// Adds an entry to the default-interface map for a class type. + /// Mirrors C++ add_default_interface_entry. + /// + public static void AddDefaultInterfaceEntry(TypeWriter w, TypeDefinition type, System.Collections.Concurrent.ConcurrentDictionary entries) + { + if (w.Settings.ReferenceProjection) { return; } + ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); + if (defaultIface is null) { return; } + + string typeNs = type.Namespace?.Value ?? string.Empty; + string typeName = type.Name?.Value ?? string.Empty; + string className = $"global::{typeNs}.{Helpers.StripBackticks(typeName)}"; + + // Resolve TypeReference → TypeDefinition so WriteTypeName goes through the Definition + // branch which knows about authored-type CCW namespacing (ABI.Impl. prefix). + ITypeDefOrRef capturedIface = defaultIface; + if (capturedIface is not TypeDefinition && capturedIface is not TypeSpecification && _cacheRef is not null) + { + try + { + TypeDefinition? resolved = capturedIface.Resolve(_cacheRef.RuntimeContext); + if (resolved is not null) { capturedIface = resolved; } + } + catch { /* leave as TypeReference */ } + } + + // Build the interface display name via TypeSemantics so generic instantiations + // (e.g. IDictionary), TypeRefs and TypeDefs are all handled correctly. + // Mirrors C++ 'add_default_interface_entry' which uses 'for_typedef' + 'write_type_name'. + string interfaceName = w.WriteTemp("%", new Action(tw => + { + TypeSemantics semantics = TypeSemanticsFactory.GetFromTypeDefOrRef(capturedIface); + WriteTypeName(w, semantics, TypedefNameType.CCW, true); + })); + + _ = entries.TryAdd(className, interfaceName); + } + + /// + /// Adds entries for [ExclusiveTo] interfaces of the class type. + /// Mirrors C++ add_exclusive_to_interface_entries. + /// + public static void AddExclusiveToInterfaceEntries(TypeWriter w, TypeDefinition type, System.Collections.Concurrent.ConcurrentBag> entries) + { + if (!w.Settings.Component || w.Settings.ReferenceProjection) { return; } + string typeNs = type.Namespace?.Value ?? string.Empty; + string typeName = type.Name?.Value ?? string.Empty; + string className = $"global::{typeNs}.{Helpers.StripBackticks(typeName)}"; + + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) { continue; } + + // Resolve the interface to a TypeDefinition for the [ExclusiveTo] check. + // Mirrors C++ 'for_typedef(get_type_semantics(iface.Interface()))'. + TypeDefinition? ifaceDef = impl.Interface as TypeDefinition; + if (ifaceDef is null && _cacheRef is not null) + { + try { ifaceDef = impl.Interface.Resolve(_cacheRef.RuntimeContext); } + catch { ifaceDef = null; } + } + if (ifaceDef is null && impl.Interface is TypeSpecification spec + && spec.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) + { + ifaceDef = gi.GenericType as TypeDefinition; + if (ifaceDef is null && _cacheRef is not null) + { + try { ifaceDef = gi.GenericType.Resolve(_cacheRef.RuntimeContext); } + catch { ifaceDef = null; } + } + } + if (ifaceDef is null) { continue; } + + if (TypeCategorization.IsExclusiveTo(ifaceDef)) + { + // Resolve TypeReference → TypeDefinition (or TypeSpecification with resolved generic + // type) so WriteTypeName goes through the Definition branch which knows about + // authored-type CCW namespacing (ABI.Impl. prefix). + ITypeDefOrRef capturedIface = impl.Interface; + if (capturedIface is not TypeDefinition && capturedIface is not TypeSpecification && _cacheRef is not null) + { + try + { + TypeDefinition? resolved = capturedIface.Resolve(_cacheRef.RuntimeContext); + if (resolved is not null) { capturedIface = resolved; } + } + catch { /* leave as TypeReference */ } + } + string interfaceName = w.WriteTemp("%", new Action(tw => + { + TypeSemantics semantics = TypeSemanticsFactory.GetFromTypeDefOrRef(capturedIface); + WriteTypeName(w, semantics, TypedefNameType.CCW, true); + })); + entries.Add(new KeyValuePair(className, interfaceName)); + } + } + } + + /// Writes the generated WindowsRuntimeDefaultInterfaces.cs file (mirrors C++ write_default_interfaces_class). + public static void WriteDefaultInterfacesClass(Settings settings, IReadOnlyList> sortedEntries) + { + if (sortedEntries.Count == 0) { return; } + TextWriter w = new(); + WriteFileHeader(w); + w.Write("using System;\nusing WindowsRuntime;\n\n#pragma warning disable CSWINRT3001\n\nnamespace ABI\n{\n"); + foreach (KeyValuePair kv in sortedEntries) + { + w.Write("[WindowsRuntimeDefaultInterface(typeof("); + w.Write(kv.Key); + w.Write("), typeof("); + w.Write(kv.Value); + w.Write("))]\n"); + } + w.Write("internal static class WindowsRuntimeDefaultInterfaces;\n}\n"); + w.FlushToFile(Path.Combine(settings.OutputFolder, "WindowsRuntimeDefaultInterfaces.cs")); + } + + /// Writes the generated WindowsRuntimeExclusiveToInterfaces.cs file (mirrors C++ write_exclusive_to_interfaces_class). + public static void WriteExclusiveToInterfacesClass(Settings settings, IReadOnlyList> sortedEntries) + { + if (sortedEntries.Count == 0) { return; } + TextWriter w = new(); + WriteFileHeader(w); + w.Write("using System;\nusing WindowsRuntime;\n\n#pragma warning disable CSWINRT3001\n\nnamespace ABI\n{\n"); + foreach (KeyValuePair kv in sortedEntries) + { + w.Write("[WindowsRuntimeExclusiveToInterface(typeof("); + w.Write(kv.Key); + w.Write("), typeof("); + w.Write(kv.Value); + w.Write("))]\n"); + } + w.Write("internal static class WindowsRuntimeExclusiveToInterfaces;\n}\n"); + w.FlushToFile(Path.Combine(settings.OutputFolder, "WindowsRuntimeExclusiveToInterfaces.cs")); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Interface.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Interface.cs new file mode 100644 index 0000000000..29647a6682 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Interface.cs @@ -0,0 +1,433 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Interface, class, and ABI emission helpers. +/// +internal static partial class CodeWriters +{ + /// Mirrors C++ write_guid_attribute. + public static void WriteGuidAttribute(TypeWriter w, TypeDefinition type) + { + bool fullyQualify = type.Namespace == "Windows.Foundation.Metadata"; + w.Write("["); + w.Write(fullyQualify ? "global::System.Runtime.InteropServices.Guid" : "Guid"); + w.Write("(\""); + WriteGuid(w, type, false); + w.Write("\")]"); + } + + /// Mirrors C++ write_type_inheritance (object base case). + public static void WriteTypeInheritance(TypeWriter w, TypeDefinition type, bool includeExclusiveInterface, bool includeWindowsRuntimeObject) + { + string delimiter = " : "; + + // Check the base type. If the class extends another runtime class (not System.Object), + // emit the projected base type name. Mirrors C++ write_type_inheritance, which only + // checks for object_type — WindowsRuntime.WindowsRuntimeObject is a managed type + // defined in WinRT.Runtime and is never referenced as a base type in any .winmd, so + // there is no need to check for it here. + bool hasNonObjectBase = false; + if (type.BaseType is not null) + { + string? baseNs = type.BaseType.Namespace?.Value; + string? baseName = type.BaseType.Name?.Value; + hasNonObjectBase = !(baseNs == "System" && baseName == "Object"); + } + + if (hasNonObjectBase) + { + w.Write(delimiter); + // Write the projected base type name. Same-namespace types stay unqualified (e.g. + // 'AppointmentActionEntity : ActionEntity') — only emit 'global::' when the base + // class lives in a different namespace (mirrors C++ write_typedef_name behavior). + ITypeDefOrRef baseType = type.BaseType!; + string ns = baseType.Namespace?.Value ?? string.Empty; + string name = baseType.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + if (!string.IsNullOrEmpty(ns) && ns != w.CurrentNamespace) + { + w.Write("global::"); + w.Write(ns); + w.Write("."); + } + w.Write(Helpers.StripBackticks(name)); + delimiter = ", "; + } + else if (includeWindowsRuntimeObject) + { + w.Write(delimiter); + w.Write("WindowsRuntimeObject"); + delimiter = ", "; + } + + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) { continue; } + + bool isOverridable = Helpers.IsOverridable(impl); + + // For TypeDef interfaces, check exclusive_to attribute to decide inclusion. + // For TypeRef interfaces, attempt to resolve via the runtime context. + bool isExclusive = false; + if (impl.Interface is TypeDefinition ifaceTypeDef) + { + isExclusive = TypeCategorization.IsExclusiveTo(ifaceTypeDef); + } + else + { + TypeDefinition? resolved = ResolveInterface(impl.Interface); + if (resolved is not null) + { + isExclusive = TypeCategorization.IsExclusiveTo(resolved); + } + } + + if (!(isOverridable || !isExclusive || includeExclusiveInterface)) + { + continue; + } + + w.Write(delimiter); + delimiter = ", "; + + // Emit the interface name (CCW) with mapped-type remapping + WriteInterfaceTypeName(w, impl.Interface); + + if (includeWindowsRuntimeObject && !w.Settings.ReferenceProjection) + { + w.Write(", IWindowsRuntimeInterface<"); + WriteInterfaceTypeName(w, impl.Interface); + w.Write(">"); + } + } + } + + /// + /// Writes the projected name for an interface reference (TypeDefinition, TypeReference, or + /// generic instance), applying mapped-type remapping (e.g., + /// Windows.Foundation.Collections.IMap<K,V>System.Collections.Generic.IDictionary<K,V>). + /// + public static void WriteInterfaceTypeName(TypeWriter w, ITypeDefOrRef ifaceType) + { + if (ifaceType is TypeDefinition td) + { + WriteTypedefName(w, td, TypedefNameType.CCW, false); + WriteTypeParams(w, td); + } + else if (ifaceType is TypeReference tr) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + // Only emit the global:: prefix if the namespace doesn't match the current emit namespace + // (mirrors WriteTypedefName behavior — same-namespace types stay unqualified). + if (!string.IsNullOrEmpty(ns) && ns != w.CurrentNamespace) + { + w.Write("global::"); + w.Write(ns); + w.Write("."); + } + w.WriteCode(Helpers.StripBackticks(name)); + } + else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ITypeDefOrRef gt = gi.GenericType; + string ns = gt.Namespace?.Value ?? string.Empty; + string name = gt.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + if (!string.IsNullOrEmpty(ns) && ns != w.CurrentNamespace) + { + w.Write("global::"); + w.Write(ns); + w.Write("."); + } + w.WriteCode(Helpers.StripBackticks(name)); + w.Write("<"); + for (int i = 0; i < gi.TypeArguments.Count; i++) + { + if (i > 0) { w.Write(", "); } + // Pass forceWriteNamespace=false so type args also respect the current namespace. + WriteTypeName(w, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, false); + } + w.Write(">"); + } + } + + /// Mirrors C++ write_prop_type. + public static string WritePropType(TypeWriter w, PropertyDefinition prop, bool isSetProperty = false) + { + return WritePropType(w, prop, null, isSetProperty); + } + + public static string WritePropType(TypeWriter w, PropertyDefinition prop, AsmResolver.DotNet.Signatures.GenericContext? genCtx, bool isSetProperty = false) + { + TypeSignature? typeSig = prop.Signature?.ReturnType; + if (typeSig is null) { return "object"; } + if (genCtx is not null) { typeSig = typeSig.InstantiateGenericTypes(genCtx.Value); } + return w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, typeSig, isSetProperty))); + } + + /// Mirrors C++ write_interface_member_signatures. + public static void WriteInterfaceMemberSignatures(TypeWriter w, TypeDefinition type) + { + foreach (MethodDefinition method in type.Methods) + { + if (Helpers.IsSpecial(method)) { continue; } + MethodSig sig = new(method); + w.Write("\n"); + // Mirror C++ write_interface_required which calls write_custom_attributes for method.CustomAttribute(). + // Only emit Windows.Foundation.Metadata attributes that have a projected form (Overload, DefaultOverload, AttributeUsage, Experimental). + WriteMethodCustomAttributes(w, method); + WriteProjectionReturnType(w, sig); + w.Write(" "); + w.Write(method.Name?.Value ?? string.Empty); + w.Write("("); + WriteParameterList(w, sig); + w.Write(");"); + } + + foreach (PropertyDefinition prop in type.Properties) + { + (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); + // Mirror C++ code_writers.h:5642 — emit 'new' when the property is setter-only + // on this interface AND a property of the same name exists in any base interface + // (typically the getter-only counterpart). This hides the inherited member. + string newKeyword = (getter is null && setter is not null + && FindPropertyInBaseInterfaces(type, prop.Name?.Value ?? string.Empty)) + ? "new " : string.Empty; + string propType = WritePropType(w, prop); + w.Write("\n"); + w.Write(newKeyword); + w.Write(propType); + w.Write(" "); + w.Write(prop.Name?.Value ?? string.Empty); + w.Write(" {"); + if (getter is not null || setter is not null) { w.Write(" get;"); } + if (setter is not null) { w.Write(" set;"); } + w.Write(" }"); + } + + foreach (EventDefinition evt in type.Events) + { + w.Write("\nevent "); + WriteEventType(w, evt); + w.Write(" "); + w.Write(evt.Name?.Value ?? string.Empty); + w.Write(";"); + } + } + + /// + /// Recursively walks the base interfaces of looking for a property + /// with the given . Mirrors C++ find_property_interface + /// at code_writers.h:4154-4185 (returns true if any base interface declares a property + /// with that name; used to decide whether a setter-only property in a derived interface + /// needs the new modifier to hide the base getter). + /// + private static bool FindPropertyInBaseInterfaces(TypeDefinition type, string propName) + { + if (string.IsNullOrEmpty(propName)) { return false; } + System.Collections.Generic.HashSet visited = new(); + return FindPropertyInBaseInterfacesRecursive(type, propName, visited); + } + + private static bool FindPropertyInBaseInterfacesRecursive(TypeDefinition type, string propName, System.Collections.Generic.HashSet visited) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) { continue; } + TypeDefinition? baseIface = ResolveInterface(impl.Interface); + if (baseIface is null) { continue; } + // Skip the original setter-defining interface itself (matches C++ check + // 'setter_iface != type'). Also dedupe via the visited set. + if (baseIface == type) { continue; } + if (!visited.Add(baseIface)) { continue; } + foreach (PropertyDefinition prop in baseIface.Properties) + { + if ((prop.Name?.Value ?? string.Empty) == propName) { return true; } + } + if (FindPropertyInBaseInterfacesRecursive(baseIface, propName, visited)) { return true; } + } + return false; + } + + /// + /// Like but returns the base interface where the + /// property was found (or null if not found). Mirrors the C++ tool's + /// find_property_interface which returns a pair<TypeDef, bool>. + /// + public static TypeDefinition? FindPropertyInterfaceInBases(TypeDefinition type, string propName) + { + if (string.IsNullOrEmpty(propName)) { return null; } + System.Collections.Generic.HashSet visited = new(); + return FindPropertyInterfaceInBasesRecursive(type, propName, visited); + } + + private static TypeDefinition? FindPropertyInterfaceInBasesRecursive(TypeDefinition type, string propName, System.Collections.Generic.HashSet visited) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) { continue; } + TypeDefinition? baseIface = ResolveInterface(impl.Interface); + if (baseIface is null) { continue; } + if (baseIface == type) { continue; } + if (!visited.Add(baseIface)) { continue; } + foreach (PropertyDefinition prop in baseIface.Properties) + { + if ((prop.Name?.Value ?? string.Empty) == propName) { return baseIface; } + } + TypeDefinition? deeper = FindPropertyInterfaceInBasesRecursive(baseIface, propName, visited); + if (deeper is not null) { return deeper; } + } + return null; + } + + /// + /// Emits the projected custom attributes for an interface method. Mirrors C++ + /// write_custom_attributes filtered for the projected attributes. + /// + private static void WriteMethodCustomAttributes(TypeWriter w, MethodDefinition method) + { + foreach (CustomAttribute attr in method.CustomAttributes) + { + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + if (attrType is null) { continue; } + string ns = attrType.Namespace?.Value ?? string.Empty; + string nm = attrType.Name?.Value ?? string.Empty; + if (ns != "Windows.Foundation.Metadata") { continue; } + string baseName = nm.EndsWith("Attribute", System.StringComparison.Ordinal) ? nm[..^"Attribute".Length] : nm; + // Only the attributes the C++ tool considers projected (see code_writers.h). + if (baseName is not ("Overload" or "DefaultOverload" or "Experimental")) + { + continue; + } + w.Write("[global::Windows.Foundation.Metadata."); + w.Write(baseName); + // Args: only handle string args (sufficient for [Overload(@"X")]). [DefaultOverload] has none. + if (attr.Signature is not null && attr.Signature.FixedArguments.Count > 0) + { + w.Write("("); + for (int i = 0; i < attr.Signature.FixedArguments.Count; i++) + { + if (i > 0) { w.Write(", "); } + object? val = attr.Signature.FixedArguments[i].Element; + if (val is AsmResolver.Utf8String s) + { + w.Write("@\""); + w.Write(s.Value); + w.Write("\""); + } + else if (val is string ss) + { + w.Write("@\""); + w.Write(ss); + w.Write("\""); + } + else + { + w.Write(val?.ToString() ?? string.Empty); + } + } + w.Write(")"); + } + w.Write("]\n"); + } + } + + /// + /// Mirrors C++ write_interface. Emits an interface projection. + /// + public static void WriteInterface(TypeWriter w, TypeDefinition type) + { + // Mirrors C++ write_interface skip rule: exclusive interfaces other than the default + // and overridable one are not used in the projection. Skip them unless public_exclusiveto + // is set (or in reference projection or component mode). + if (!w.Settings.ReferenceProjection && + !w.Settings.Component && + TypeCategorization.IsExclusiveTo(type) && + !w.Settings.PublicExclusiveTo && + !IsDefaultOrOverridableInterfaceTypedef(type)) + { + return; + } + + if (w.Settings.Component && !TypeCategorization.IsExclusiveTo(type)) + { + return; + } + + w.Write("\n"); + WriteWinRTMetadataAttribute(w, type, _cacheRef!); + WriteGuidAttribute(w, type); + w.Write("\n"); + WriteTypeCustomAttributes(w, type, false); + + bool isInternal = (TypeCategorization.IsExclusiveTo(type) && !w.Settings.PublicExclusiveTo) || + TypeCategorization.IsProjectionInternal(type); + w.Write(isInternal ? "internal" : "public"); + w.Write(" interface "); + WriteTypedefName(w, type, TypedefNameType.CCW, false); + WriteTypeParams(w, type); + WriteTypeInheritance(w, type, false, false); + w.Write("\n{"); + WriteInterfaceMemberSignatures(w, type); + w.Write("\n}\n"); + } + + /// Mirrors C++ is_default_or_overridable_interface_typedef: returns true if the + /// given exclusive interface is referenced as a [Default] or [Overridable] interface impl on + /// the class it's exclusive to. + private static bool IsDefaultOrOverridableInterfaceTypedef(TypeDefinition iface) + { + if (!TypeCategorization.IsExclusiveTo(iface)) { return false; } + TypeDefinition? classType = GetExclusiveToType(iface); + if (classType is null) { return false; } + foreach (InterfaceImplementation impl in classType.Interfaces) + { + if (!Helpers.IsDefaultInterface(impl) && !Helpers.IsOverridable(impl)) { continue; } + ITypeDefOrRef? implRef = impl.Interface; + if (implRef is null) { continue; } + TypeDefinition? implDef = ResolveInterfaceTypeDefForExclusiveCheck(implRef); + if (implDef is not null && implDef == iface) { return true; } + } + return false; + } + + private static TypeDefinition? ResolveInterfaceTypeDefForExclusiveCheck(ITypeDefOrRef ifaceRef) + { + if (ifaceRef is TypeDefinition td) { return td; } + if (ifaceRef is TypeReference tr && _cacheRef is not null) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string nm = tr.Name?.Value ?? string.Empty; + return _cacheRef.Find(ns + "." + nm); + } + if (ifaceRef is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) + { + ITypeDefOrRef? gen = gi.GenericType; + return gen is null ? null : ResolveInterfaceTypeDefForExclusiveCheck(gen); + } + return null; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.InteropTypeName.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.InteropTypeName.cs new file mode 100644 index 0000000000..d3235c81e8 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.InteropTypeName.cs @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Encoder for the WinRT.Interop assembly type name format used in UnsafeAccessor +/// attributes (e.g. "ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<string|object>Marshaller, WinRT.Interop"). +/// Mirrors the C++ helpers write_interop_assembly_name, write_interop_dll_type_name, +/// and write_interop_dll_type_name_for_typedef. +/// +internal static partial class CodeWriters +{ + /// + /// Encodes a TypeSignature using the WinRT.Interop name format. Used as the value of an + /// UnsafeAccessorType attribute argument. + /// + /// The type signature to encode. + /// Indicates whether to use the projected (no ABI prefix) form or + /// the ABI-prefixed marshaller form. + public static string EncodeInteropTypeName(TypeSignature sig, TypedefNameType nameType) + { + StringBuilder sb = new(); + EncodeInteropTypeNameInto(sb, sig, nameType); + return sb.ToString(); + } + + private static void EncodeInteropTypeNameInto(StringBuilder sb, TypeSignature sig, TypedefNameType nameType) + { + // Special case for System.Guid: matches C++ guid_type case in write_interop_dll_type_name. + if (sig is TypeDefOrRefSignature gtd + && gtd.Type?.Namespace?.Value == "System" + && gtd.Type?.Name?.Value == "Guid") + { + if (nameType == TypedefNameType.Projected) { sb.Append("System-Guid"); } + else { sb.Append("ABI.System.<<#corlib>Guid>"); } + return; + } + switch (sig) + { + case CorLibTypeSignature corlib: + EncodeFundamental(sb, corlib, nameType); + return; + case TypeDefOrRefSignature td: + EncodeForTypeDef(sb, td.Type, nameType, generic_args: null); + return; + case GenericInstanceTypeSignature gi: + EncodeForTypeDef(sb, gi.GenericType, nameType, generic_args: gi.TypeArguments); + return; + case SzArrayTypeSignature sz: + if (nameType == TypedefNameType.Projected) + { + EncodeInteropTypeNameInto(sb, sz.BaseType, TypedefNameType.Projected); + } + else + { + sb.Append("ABI.System.<"); + EncodeInteropTypeNameInto(sb, sz.BaseType, TypedefNameType.Projected); + sb.Append(">"); + } + return; + case ByReferenceTypeSignature br: + EncodeInteropTypeNameInto(sb, br.BaseType, nameType); + return; + case CustomModifierTypeSignature cm: + EncodeInteropTypeNameInto(sb, cm.BaseType, nameType); + return; + default: + sb.Append(sig.FullName); + return; + } + } + + private static void EncodeFundamental(StringBuilder sb, CorLibTypeSignature corlib, TypedefNameType nameType) + { + switch (corlib.ElementType) + { + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object: + if (nameType == TypedefNameType.Projected) { sb.Append("object"); } + else { sb.Append("ABI.System."); } + return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean: sb.Append("bool"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char: sb.Append("char"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1: sb.Append("sbyte"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1: sb.Append("byte"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2: sb.Append("short"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2: sb.Append("ushort"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4: sb.Append("int"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4: sb.Append("uint"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8: sb.Append("long"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8: sb.Append("ulong"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4: sb.Append("float"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8: sb.Append("double"); return; + case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String: + sb.Append("string"); + return; + } + sb.Append(corlib.FullName); + } + + private static void EncodeForTypeDef(StringBuilder sb, ITypeDefOrRef type, TypedefNameType nameType, System.Collections.Generic.IList? generic_args) + { + string typeNs = type.Namespace?.Value ?? string.Empty; + string typeName = type.Name?.Value ?? string.Empty; + + bool isAbi = nameType != TypedefNameType.Projected && nameType != TypedefNameType.InteropIID; + if (isAbi) { sb.Append("ABI."); } + + // Special case for EventSource on Windows.Foundation event-handler delegate types + // (e.g. EventHandler, TypedEventHandler). Mirrors C++: + // ABI.WindowsRuntime.InteropServices.<#CsWinRT>EventHandlerEventSource' + // Note the namespace check uses the ORIGINAL .winmd namespace (before mapping). + if (nameType == TypedefNameType.EventSource && typeNs == "Windows.Foundation") + { + // Determine generic arity from the .winmd type name (e.g. "EventHandler`1" => 1). + int arity = 0; + int tickIdx = typeName.IndexOf('`'); + if (tickIdx >= 0 && int.TryParse(typeName.AsSpan(tickIdx + 1), out int parsed)) + { + arity = parsed; + } + sb.Append("WindowsRuntime.InteropServices.<#CsWinRT>EventHandlerEventSource'"); + sb.Append(arity.ToString(System.Globalization.CultureInfo.InvariantCulture)); + // Append the generic args (if any). + if (generic_args is { Count: > 0 }) + { + sb.Append('<'); + for (int i = 0; i < generic_args.Count; i++) + { + if (i > 0) { sb.Append('|'); } + EncodeInteropTypeNameInto(sb, generic_args[i], TypedefNameType.Projected); + } + sb.Append('>'); + } + return; + } + + // Apply mapped-type remapping + MappedType? mapped = MappedTypes.Get(typeNs, typeName); + if (mapped is not null) + { + typeNs = mapped.MappedNamespace; + typeName = mapped.MappedName; + } + // Replace generic arity backtick with apostrophe. + typeName = typeName.Replace('`', '\''); + + if (nameType == TypedefNameType.InteropIID) + { + sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); + sb.Append(typeName); + } + else if (nameType == TypedefNameType.Projected) + { + // Replace namespace separator with - within the generic. + string nsHyphenated = typeNs.Replace('.', '-'); + sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); + sb.Append(nsHyphenated); + sb.Append('-'); + sb.Append(typeName); + } + else + { + sb.Append(typeNs); + sb.Append('.'); + sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); + sb.Append(typeName); + } + + if (generic_args is { Count: > 0 }) + { + sb.Append('<'); + for (int i = 0; i < generic_args.Count; i++) + { + if (i > 0) { sb.Append('|'); } + EncodeInteropTypeNameInto(sb, generic_args[i], TypedefNameType.Projected); + } + sb.Append('>'); + } + + // Append the type-kind suffix (matches C++ write_interop_dll_type_name_for_typedef). + if (nameType == TypedefNameType.StaticAbiClass) + { + sb.Append("Methods"); + } + else if (nameType == TypedefNameType.ABI) + { + sb.Append("Marshaller"); + } + else if (nameType == TypedefNameType.EventSource) + { + sb.Append("EventSource"); + } + } + + /// + /// Returns the assembly marker (e.g. <#corlib>) for a (possibly remapped) + /// type/namespace. Mirrors C++ write_interop_assembly_name. + /// + private static string GetInteropAssemblyMarker(string typeNs, string typeName, MappedType? mapped, ITypeDefOrRef? type = null) + { + if (mapped is not null) + { + // Mirrors C++ helpers.h:693-725 + code_writers.h:441-466. The mapped namespace + // determines the marker. + if (typeNs.StartsWith("System", StringComparison.Ordinal)) + { + if (IsMappedTypeInSystemNumericsVectors(typeNs)) + { + return ""; + } + if (IsMappedTypeInSystemObjectModel(typeNs, typeName)) + { + return ""; + } + return "<#corlib>"; + } + // Mapped to a non-System namespace. + if (!mapped.EmitAbi) + { + return "<#CsWinRT>"; + } + if (typeNs.StartsWith("Windows", StringComparison.Ordinal)) + { + // Mirror C++ code_writers.h:464 which writes "<#%Windows>" — the '%' is an + // unintended template placeholder in C++ that's unreachable in practice (no + // standard mapped type maps to a Windows.* namespace with EmitAbi=true). We + // emit the corrected '<#Windows>' so any future addition that hits this + // branch produces a runtime-resolvable assembly marker rather than garbage. + return "<#Windows>"; + } + } + // Unmapped type. + if (typeNs.StartsWith("Windows.", StringComparison.Ordinal) || typeNs == "Windows") + { + return "<#Windows>"; + } + if (typeNs.StartsWith("WindowsRuntime", StringComparison.Ordinal)) + { + return "<#CsWinRT>"; + } + // For any other type (e.g. user-authored components in third-party .winmd assemblies), + // use the actual assembly name from the type's resolution scope. Mirrors C++ which + // uses the .winmd file stem (e.g. "AuthoringTest" for AuthoringTest.winmd). + if (type is not null) + { + string? asmName = GetTypeAssemblyName(type); + if (!string.IsNullOrEmpty(asmName)) + { + // Replace '.' with '-' (matches C++ which does std::replace('.', '-')). + string hyphenated = asmName.Replace('.', '-'); + return "<" + hyphenated + ">"; + } + } + return "<#Windows>"; + } + + /// Mirrors C++ helpers.h:693 is_mapped_type_in_system_objectmodel. + private static bool IsMappedTypeInSystemObjectModel(string typeNs, string typeName) + { + if (typeNs == "System.Collections.Specialized") + { + return typeName is "INotifyCollectionChanged" + or "NotifyCollectionChangedAction" + or "NotifyCollectionChangedEventArgs" + or "NotifyCollectionChangedEventHandler"; + } + if (typeNs == "System.ComponentModel") + { + return typeName is "INotifyDataErrorInfo" + or "INotifyPropertyChanged" + or "DataErrorsChangedEventArgs" + or "PropertyChangedEventArgs" + or "PropertyChangedEventHandler"; + } + if (typeNs == "System.Windows.Input") + { + return typeName == "ICommand"; + } + return false; + } + + /// Mirrors C++ helpers.h:727 is_mapped_type_in_system_numerics_vectors. + private static bool IsMappedTypeInSystemNumericsVectors(string typeNs) + { + return typeNs == "System.Numerics"; + } + + /// + /// Resolves the assembly name (without extension) that defines a given type. + /// + private static string? GetTypeAssemblyName(ITypeDefOrRef type) + { + return type.Scope?.GetAssembly()?.Name?.Value; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.MappedInterfaceStubs.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.MappedInterfaceStubs.cs new file mode 100644 index 0000000000..eab7a4aa99 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.MappedInterfaceStubs.cs @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Emits stub members ('=> throw null!') for well-known C# interfaces that come from mapped +/// WinRT interfaces (IClosable -> IDisposable, IMap`2 -> IDictionary<K,V>, etc.). The +/// runtime adapter actually services these at runtime via IDynamicInterfaceCastable, but the +/// C# compiler still requires the class to declare the members. +/// +internal static partial class CodeWriters +{ + /// + /// Returns true if the WinRT interface (by namespace+name) is a mapped interface that + /// requires emitting C#-interface stub members on the implementing class. + /// + public static bool IsMappedInterfaceRequiringStubs(string ifaceNs, string ifaceName) + { + if (MappedTypes.Get(ifaceNs, ifaceName) is not { HasCustomMembersOutput: true }) + { + return false; + } + return ifaceName switch + { + "IClosable" => true, + "IIterable`1" or "IIterator`1" => true, + "IMap`2" or "IMapView`2" => true, + "IVector`1" or "IVectorView`1" => true, + "IBindableIterable" or "IBindableIterator" or "IBindableVector" => true, + "INotifyDataErrorInfo" => true, + _ => false, + }; + } + + /// + /// Emits the C# interface stub members for the given WinRT interface that maps to a known + /// .NET interface. + /// + /// The writer. + /// The (possibly substituted) generic instance signature for the interface, or null if non-generic. + /// The WinRT interface name (e.g. "IMap`2"). + /// The name of the lazy _objRef_* field for the interface on the class. + public static void WriteMappedInterfaceStubs(TypeWriter w, GenericInstanceTypeSignature? instance, string ifaceName, string objRefName) + { + // Resolve type arguments from the (substituted) generic instance signature, if any. + List typeArgs = new(); + List typeArgSigs = new(); + if (instance is not null) + { + foreach (TypeSignature arg in instance.TypeArguments) + { + typeArgs.Add(TypeSemanticsFactory.Get(arg)); + typeArgSigs.Add(arg); + } + } + + switch (ifaceName) + { + case "IClosable": + EmitDisposable(w, objRefName); + break; + case "IIterable`1": + EmitGenericEnumerable(w, typeArgs, typeArgSigs, objRefName); + break; + case "IIterator`1": + EmitGenericEnumerator(w, typeArgs, typeArgSigs, objRefName); + break; + case "IMap`2": + EmitDictionary(w, typeArgs, typeArgSigs, objRefName); + break; + case "IMapView`2": + EmitReadOnlyDictionary(w, typeArgs, typeArgSigs, objRefName); + break; + case "IVector`1": + EmitList(w, typeArgs, typeArgSigs, objRefName); + break; + case "IVectorView`1": + EmitReadOnlyList(w, typeArgs, typeArgSigs, objRefName); + break; + case "IBindableIterable": + w.Write($"\nIEnumerator global::System.Collections.IEnumerable.GetEnumerator() => global::ABI.System.Collections.IEnumerableMethods.GetEnumerator({objRefName});\n"); + break; + case "IBindableIterator": + w.Write($"\npublic bool MoveNext() => global::ABI.System.Collections.IEnumeratorMethods.MoveNext({objRefName});\n"); + w.Write("public void Reset() => throw new NotSupportedException();\n"); + w.Write($"public object Current => global::ABI.System.Collections.IEnumeratorMethods.Current({objRefName});\n"); + break; + case "IBindableVector": + EmitNonGenericList(w, objRefName); + break; + case "INotifyDataErrorInfo": + w.Write($"\npublic global::System.Collections.IEnumerable GetErrors(string propertyName) => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.GetErrors({objRefName}, propertyName);\n"); + w.Write($"public bool HasErrors {{get => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.HasErrors({objRefName}); }}\n"); + w.Write($"public event global::System.EventHandler ErrorsChanged\n{{\n add => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.ErrorsChanged(this, {objRefName}).Subscribe(value);\n remove => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.ErrorsChanged(this, {objRefName}).Unsubscribe(value);\n}}\n"); + break; + } + } + + private static void EmitDisposable(TypeWriter w, string objRefName) + { + w.Write("\npublic void Dispose() => global::ABI.System.IDisposableMethods.Dispose("); + w.Write(objRefName); + w.Write(");\n"); + } + + private static void EmitGenericEnumerable(TypeWriter w, List args, List argSigs, string objRefName) + { + if (args.Count != 1) { return; } + string t = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); + string elementId = EncodeArgIdentifier(w, args[0]); + string interopTypeArgs = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IEnumerable'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; + string prefix = "IEnumerableMethods_" + elementId + "_"; + + w.Write("\n"); + EmitUnsafeAccessor(w, "GetEnumerator", $"IEnumerator<{t}>", $"{prefix}GetEnumerator", interopType, ""); + + w.Write($"\npublic IEnumerator<{t}> GetEnumerator() => {prefix}GetEnumerator(null, {objRefName});\n"); + w.Write("global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();\n"); + } + + private static void EmitGenericEnumerator(TypeWriter w, List args, List argSigs, string objRefName) + { + if (args.Count != 1) { return; } + string t = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); + string elementId = EncodeArgIdentifier(w, args[0]); + string interopTypeArgs = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IEnumerator'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; + string prefix = "IEnumeratorMethods_" + elementId + "_"; + + w.Write("\n"); + EmitUnsafeAccessor(w, "Current", t, $"{prefix}Current", interopType, ""); + EmitUnsafeAccessor(w, "MoveNext", "bool", $"{prefix}MoveNext", interopType, ""); + + w.Write($"\npublic bool MoveNext() => {prefix}MoveNext(null, {objRefName});\n"); + w.Write("public void Reset() => throw new NotSupportedException();\n"); + w.Write("public void Dispose() {}\n"); + w.Write($"public {t} Current => {prefix}Current(null, {objRefName});\n"); + w.Write("object global::System.Collections.IEnumerator.Current => Current!;\n"); + } + + private static void EmitDictionary(TypeWriter w, List args, List argSigs, string objRefName) + { + if (args.Count != 2) { return; } + string k = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); + string v = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[1], TypedefNameType.Projected, true))); + // Truth uses two forms for KeyValuePair: + // - 'kv' (unqualified) for plain type usages: parameters, field/return types + // - 'kvNested' (fully qualified) for generic argument usages (inside IEnumerator<>, ICollection<>) + string kv = $"KeyValuePair<{k}, {v}>"; + string kvNested = $"global::System.Collections.Generic.KeyValuePair<{k}, {v}>"; + // Long form (always fully qualified) used for objref field-name computation + // (matches the form WriteClassObjRefDefinitions emits transitively). + string kvLong = kvNested; + string keyId = EncodeArgIdentifier(w, args[0]); + string valId = EncodeArgIdentifier(w, args[1]); + string keyInteropArg = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string valInteropArg = EncodeInteropTypeName(argSigs[1], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IDictionary'2<" + keyInteropArg + "|" + valInteropArg + ">Methods, WinRT.Interop"; + string prefix = "IDictionaryMethods_" + keyId + "_" + valId + "_"; + // The IEnumerable> objref name (matches what WriteClassObjRefDefinitions emits transitively). + string enumerableObjRefName = "_objRef_System_Collections_Generic_IEnumerable_" + EscapeTypeNameForIdentifier(kvLong, stripGlobal: false) + "_"; + + w.Write("\n"); + EmitUnsafeAccessor(w, "Keys", $"ICollection<{k}>", $"{prefix}Keys", interopType, ""); + EmitUnsafeAccessor(w, "Values", $"ICollection<{v}>", $"{prefix}Values", interopType, ""); + EmitUnsafeAccessor(w, "Count", "int", $"{prefix}Count", interopType, ""); + EmitUnsafeAccessor(w, "Item", v, $"{prefix}Item", interopType, $", {k} key"); + EmitUnsafeAccessor(w, "Item", "void", $"{prefix}Item", interopType, $", {k} key, {v} value"); + EmitUnsafeAccessor(w, "Add", "void", $"{prefix}Add", interopType, $", {k} key, {v} value"); + EmitUnsafeAccessor(w, "ContainsKey", "bool", $"{prefix}ContainsKey", interopType, $", {k} key"); + EmitUnsafeAccessor(w, "Remove", "bool", $"{prefix}Remove", interopType, $", {k} key"); + EmitUnsafeAccessor(w, "TryGetValue", "bool", $"{prefix}TryGetValue", interopType, $", {k} key, out {v} value"); + EmitUnsafeAccessor(w, "Add", "void", $"{prefix}Add", interopType, $", {kv} item"); + EmitUnsafeAccessor(w, "Clear", "void", $"{prefix}Clear", interopType, ""); + EmitUnsafeAccessor(w, "Contains", "bool", $"{prefix}Contains", interopType, $", {kv} item"); + EmitUnsafeAccessor(w, "CopyTo", "void", $"{prefix}CopyTo", interopType, $", WindowsRuntimeObjectReference enumObjRef, {kv}[] array, int arrayIndex"); + EmitUnsafeAccessor(w, "Remove", "bool", $"{prefix}Remove", interopType, $", {kv} item"); + + // Public member emission order matches C++ write_dictionary_members_using_static_abi_methods + // (code_writers.h:3677-3694): Keys, Values, Count, IsReadOnly, this[], Add(K,V), + // ContainsKey, Remove(K), TryGetValue, Add(KVP), Clear, Contains, CopyTo, + // ICollection.Remove. WinRT IMap vtable order, NOT alphabetical. + // GetEnumerator is NOT emitted here — it's handled separately by IIterable's + // own EmitGenericEnumerable invocation (mirrors C++ which only emits GetEnumerator + // through write_enumerable_members_using_static_abi_methods). + w.Write($"public ICollection<{k}> Keys => {prefix}Keys(null, {objRefName});\n"); + w.Write($"public ICollection<{v}> Values => {prefix}Values(null, {objRefName});\n"); + w.Write($"public int Count => {prefix}Count(null, {objRefName});\n"); + w.Write("public bool IsReadOnly => false;\n"); + w.Write($"public {v} this[{k} key]\n{{\n get => {prefix}Item(null, {objRefName}, key);\n set => {prefix}Item(null, {objRefName}, key, value);\n}}\n"); + w.Write($"public void Add({k} key, {v} value) => {prefix}Add(null, {objRefName}, key, value);\n"); + w.Write($"public bool ContainsKey({k} key) => {prefix}ContainsKey(null, {objRefName}, key);\n"); + w.Write($"public bool Remove({k} key) => {prefix}Remove(null, {objRefName}, key);\n"); + w.Write($"public bool TryGetValue({k} key, out {v} value) => {prefix}TryGetValue(null, {objRefName}, key, out value);\n"); + w.Write($"public void Add({kv} item) => {prefix}Add(null, {objRefName}, item);\n"); + w.Write($"public void Clear() => {prefix}Clear(null, {objRefName});\n"); + w.Write($"public bool Contains({kv} item) => {prefix}Contains(null, {objRefName}, item);\n"); + w.Write($"public void CopyTo({kv}[] array, int arrayIndex) => {prefix}CopyTo(null, {objRefName}, {enumerableObjRefName}, array, arrayIndex);\n"); + // ICollection.Remove must be explicit to avoid clashing with IDictionary.Remove(K key). + w.Write($"bool ICollection<{kv}>.Remove({kv} item) => {prefix}Remove(null, {objRefName}, item);\n"); + } + + private static void EmitReadOnlyDictionary(TypeWriter w, List args, List argSigs, string objRefName) + { + if (args.Count != 2) { return; } + string k = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); + string v = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[1], TypedefNameType.Projected, true))); + string keyId = EncodeArgIdentifier(w, args[0]); + string valId = EncodeArgIdentifier(w, args[1]); + string keyInteropArg = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string valInteropArg = EncodeInteropTypeName(argSigs[1], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<" + keyInteropArg + "|" + valInteropArg + ">Methods, WinRT.Interop"; + string prefix = "IReadOnlyDictionaryMethods_" + keyId + "_" + valId + "_"; + + w.Write("\n"); + EmitUnsafeAccessor(w, "Keys", $"ICollection<{k}>", $"{prefix}Keys", interopType, ""); + EmitUnsafeAccessor(w, "Values", $"ICollection<{v}>", $"{prefix}Values", interopType, ""); + EmitUnsafeAccessor(w, "Count", "int", $"{prefix}Count", interopType, ""); + EmitUnsafeAccessor(w, "Item", v, $"{prefix}Item", interopType, $", {k} key"); + EmitUnsafeAccessor(w, "ContainsKey", "bool", $"{prefix}ContainsKey", interopType, $", {k} key"); + EmitUnsafeAccessor(w, "TryGetValue", "bool", $"{prefix}TryGetValue", interopType, $", {k} key, out {v} value"); + + // GetEnumerator is NOT emitted here — it's handled separately by IIterable's + // EmitGenericEnumerable invocation (mirrors C++ which only emits GetEnumerator + // through write_enumerable_members_using_static_abi_methods). + w.Write($"\npublic {v} this[{k} key] => {prefix}Item(null, {objRefName}, key);\n"); + w.Write($"public IEnumerable<{k}> Keys => {prefix}Keys(null, {objRefName});\n"); + w.Write($"public IEnumerable<{v}> Values => {prefix}Values(null, {objRefName});\n"); + w.Write($"public int Count => {prefix}Count(null, {objRefName});\n"); + w.Write($"public bool ContainsKey({k} key) => {prefix}ContainsKey(null, {objRefName}, key);\n"); + w.Write($"public bool TryGetValue({k} key, out {v} value) => {prefix}TryGetValue(null, {objRefName}, key, out value);\n"); + } + + private static void EmitReadOnlyList(TypeWriter w, List args, List argSigs, string objRefName) + { + if (args.Count != 1) { return; } + string t = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); + string elementId = EncodeArgIdentifier(w, args[0]); + string interopTypeArgs = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IReadOnlyList'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; + string prefix = "IReadOnlyListMethods_" + elementId + "_"; + + w.Write("\n"); + EmitUnsafeAccessor(w, "Count", "int", $"{prefix}Count", interopType, ""); + EmitUnsafeAccessor(w, "Item", t, $"{prefix}Item", interopType, ", int index"); + + // GetEnumerator is NOT emitted here — it's handled separately by IIterable's + // EmitGenericEnumerable invocation (mirrors C++ which only emits GetEnumerator + // through write_enumerable_members_using_static_abi_methods). + w.Write("\n[global::System.Runtime.CompilerServices.IndexerName(\"ReadOnlyListItem\")]\n"); + w.Write($"public {t} this[int index] => {prefix}Item(null, {objRefName}, index);\n"); + w.Write($"public int Count => {prefix}Count(null, {objRefName});\n"); + } + + /// + /// Helper to encode the WinRT.Interop dictionary key for a type-arg encoded identifier + /// (used in UnsafeAccessor function names and method-name prefixes). Mirrors C++ + /// escape_type_name_for_identifier(write_type_name(arg), true). + /// + /// + /// Encodes a type semantics as a C# identifier-safe name. Mirrors C++ + /// escape_type_name_for_identifier(write_projection_type(arg), true): + /// uses the projected type name WITHOUT forcing namespace qualification, then strips + /// 'global::' and replaces '.' with '_'. + /// + private static string EncodeArgIdentifier(TypeWriter w, TypeSemantics arg) + { + string projected = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, arg, TypedefNameType.Projected, false))); + return EscapeTypeNameForIdentifier(projected, stripGlobal: true); + } + + private static void EmitList(TypeWriter w, List args, List argSigs, string objRefName) + { + if (args.Count != 1) { return; } + string t = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); + string elementId = EncodeArgIdentifier(w, args[0]); + string interopTypeArgs = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IList'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; + string prefix = "IListMethods_" + elementId + "_"; + + w.Write("\n"); + EmitUnsafeAccessor(w, "Count", "int", $"{prefix}Count", interopType, ""); + EmitUnsafeAccessor(w, "Item", t, $"{prefix}Item", interopType, ", int index"); + EmitUnsafeAccessor(w, "Item", "void", $"{prefix}Item", interopType, $", int index, {t} value"); + EmitUnsafeAccessor(w, "IndexOf", "int", $"{prefix}IndexOf", interopType, $", {t} item"); + EmitUnsafeAccessor(w, "Insert", "void", $"{prefix}Insert", interopType, $", int index, {t} item"); + EmitUnsafeAccessor(w, "RemoveAt", "void", $"{prefix}RemoveAt", interopType, ", int index"); + EmitUnsafeAccessor(w, "Add", "void", $"{prefix}Add", interopType, $", {t} item"); + EmitUnsafeAccessor(w, "Clear", "void", $"{prefix}Clear", interopType, ""); + EmitUnsafeAccessor(w, "Contains", "bool", $"{prefix}Contains", interopType, $", {t} item"); + EmitUnsafeAccessor(w, "CopyTo", "void", $"{prefix}CopyTo", interopType, $", {t}[] array, int arrayIndex"); + EmitUnsafeAccessor(w, "Remove", "bool", $"{prefix}Remove", interopType, $", {t} item"); + + // Public member emission order matches C++ write_list_members_using_static_abi_methods + // (code_writers.h:4017-4046): Count, IsReadOnly, this[], IndexOf, Insert, RemoveAt, + // Add, Clear, Contains, CopyTo, Remove. This is the WinRT IVector vtable order + // mapped to IList, NOT alphabetical. GetEnumerator is NOT emitted here — it's + // handled separately by IIterable's own EmitGenericEnumerable invocation + // (mirrors C++ which only emits GetEnumerator through write_enumerable_members_using_static_abi_methods). + w.Write($"public int Count => {prefix}Count(null, {objRefName});\n"); + w.Write("public bool IsReadOnly => false;\n"); + w.Write("\n[global::System.Runtime.CompilerServices.IndexerName(\"ListItem\")]\n"); + w.Write($"public {t} this[int index]\n{{\n get => {prefix}Item(null, {objRefName}, index);\n set => {prefix}Item(null, {objRefName}, index, value);\n}}\n"); + w.Write($"public int IndexOf({t} item) => {prefix}IndexOf(null, {objRefName}, item);\n"); + w.Write($"public void Insert(int index, {t} item) => {prefix}Insert(null, {objRefName}, index, item);\n"); + w.Write($"public void RemoveAt(int index) => {prefix}RemoveAt(null, {objRefName}, index);\n"); + w.Write($"public void Add({t} item) => {prefix}Add(null, {objRefName}, item);\n"); + w.Write($"public void Clear() => {prefix}Clear(null, {objRefName});\n"); + w.Write($"public bool Contains({t} item) => {prefix}Contains(null, {objRefName}, item);\n"); + w.Write($"public void CopyTo({t}[] array, int arrayIndex) => {prefix}CopyTo(null, {objRefName}, array, arrayIndex);\n"); + w.Write($"public bool Remove({t} item) => {prefix}Remove(null, {objRefName}, item);\n"); + } + + /// + /// Emits a single [UnsafeAccessor] static extern declaration that targets a method on a + /// WinRT.Interop helper type. The function signature is built from the supplied parts. + /// + private static void EmitUnsafeAccessor(TypeWriter w, string accessName, string returnType, string functionName, string interopType, string extraParams) + { + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \""); + w.Write(accessName); + w.Write("\")]\n"); + w.Write("static extern "); + w.Write(returnType); + w.Write(" "); + w.Write(functionName); + w.Write("([UnsafeAccessorType(\""); + w.Write(interopType); + w.Write("\")] object _, WindowsRuntimeObjectReference objRef"); + w.Write(extraParams); + w.Write(");\n\n"); + } + + private static void EmitNonGenericList(TypeWriter w, string objRefName) + { + w.Write("\n[global::System.Runtime.CompilerServices.IndexerName(\"NonGenericListItem\")]\n"); + w.Write($"public object this[int index]\n{{\n get => global::ABI.System.Collections.IListMethods.Item({objRefName}, index);\n set => global::ABI.System.Collections.IListMethods.Item({objRefName}, index, value);\n}}\n"); + w.Write($"public int Count => global::ABI.System.Collections.IListMethods.Count({objRefName});\n"); + w.Write("public bool IsReadOnly => false;\n"); + w.Write("public bool IsFixedSize => false;\n"); + w.Write("public bool IsSynchronized => false;\n"); + w.Write("public object SyncRoot => this;\n"); + w.Write($"public int Add(object value) => global::ABI.System.Collections.IListMethods.Add({objRefName}, value);\n"); + w.Write($"public void Clear() => global::ABI.System.Collections.IListMethods.Clear({objRefName});\n"); + w.Write($"public bool Contains(object value) => global::ABI.System.Collections.IListMethods.Contains({objRefName}, value);\n"); + w.Write($"public int IndexOf(object value) => global::ABI.System.Collections.IListMethods.IndexOf({objRefName}, value);\n"); + w.Write($"public void Insert(int index, object value) => global::ABI.System.Collections.IListMethods.Insert({objRefName}, index, value);\n"); + w.Write($"public void Remove(object value) => global::ABI.System.Collections.IListMethods.Remove({objRefName}, value);\n"); + w.Write($"public void RemoveAt(int index) => global::ABI.System.Collections.IListMethods.RemoveAt({objRefName}, index);\n"); + w.Write($"public void CopyTo(Array array, int index) => global::ABI.System.Collections.IListMethods.CopyTo({objRefName}, array, index);\n"); + // GetEnumerator is NOT emitted here — it's handled separately by IBindableIterable's + // EmitNonGenericEnumerable invocation (mirrors C++ which only emits GetEnumerator + // through write_nongeneric_enumerable_members_using_static_abi_methods). + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Methods.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Methods.cs new file mode 100644 index 0000000000..c13bc1033b --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Methods.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Helpers for method/parameter/return type emission. Mirrors various functions in code_writers.h. +/// +internal static partial class CodeWriters +{ + /// Mirrors C++ write_projected_signature. + public static void WriteProjectedSignature(TypeWriter w, TypeSignature typeSig, bool isParameter) + { + // Detect SZ-array + if (typeSig is SzArrayTypeSignature sz) + { + // Mirrors C++ write_projected_signature (code_writers.h:822-834): for parameters, + // SZ arrays project as ReadOnlySpan (matches the property setter parameter + // convention; pass_array semantics). + if (isParameter) + { + w.Write("ReadOnlySpan<"); + WriteProjectionType(w, TypeSemanticsFactory.Get(sz.BaseType)); + w.Write(">"); + } + else + { + WriteProjectionType(w, TypeSemanticsFactory.Get(sz.BaseType)); + w.Write("[]"); + } + return; + } + if (typeSig is ByReferenceTypeSignature br) + { + WriteProjectionType(w, TypeSemanticsFactory.Get(br.BaseType)); + return; + } + WriteProjectionType(w, TypeSemanticsFactory.Get(typeSig)); + } + + /// Mirrors C++ write_projection_parameter_type. + public static void WriteProjectionParameterType(TypeWriter w, ParamInfo p) + { + ParamCategory cat = ParamHelpers.GetParamCategory(p); + switch (cat) + { + case ParamCategory.Out: + w.Write("out "); + WriteProjectedSignature(w, p.Type, true); + break; + case ParamCategory.Ref: + w.Write("in "); + WriteProjectedSignature(w, p.Type, true); + break; + case ParamCategory.PassArray: + w.Write("ReadOnlySpan<"); + WriteProjectionType(w, TypeSemanticsFactory.Get(((SzArrayTypeSignature)p.Type).BaseType)); + w.Write(">"); + break; + case ParamCategory.FillArray: + w.Write("Span<"); + WriteProjectionType(w, TypeSemanticsFactory.Get(((SzArrayTypeSignature)p.Type).BaseType)); + w.Write(">"); + break; + case ParamCategory.ReceiveArray: + w.Write("out "); + { + SzArrayTypeSignature? sz = p.Type as SzArrayTypeSignature + ?? (p.Type is ByReferenceTypeSignature br ? br.BaseType as SzArrayTypeSignature : null); + if (sz is not null) + { + WriteProjectionType(w, TypeSemanticsFactory.Get(sz.BaseType)); + w.Write("[]"); + } + else + { + WriteProjectedSignature(w, p.Type, true); + } + } + break; + default: + WriteProjectedSignature(w, p.Type, true); + break; + } + } + + /// Mirrors C++ write_parameter_name. + public static void WriteParameterName(TypeWriter w, ParamInfo p) + { + string name = p.Parameter.Name ?? "param"; + Helpers.WriteEscapedIdentifier(w, name); + } + + /// Mirrors C++ write_projection_parameter. + public static void WriteProjectionParameter(TypeWriter w, ParamInfo p) + { + WriteProjectionParameterType(w, p); + w.Write(" "); + WriteParameterName(w, p); + } + + /// Mirrors C++ write_projection_return_type. + public static void WriteProjectionReturnType(TypeWriter w, MethodSig sig) + { + TypeSignature? rt = sig.ReturnType; + if (rt is null) + { + w.Write("void"); + return; + } + WriteProjectedSignature(w, rt, false); + } + + /// Writes a parameter list separated by commas. + public static void WriteParameterList(TypeWriter w, MethodSig sig) + { + for (int i = 0; i < sig.Params.Count; i++) + { + if (i > 0) { w.Write(", "); } + WriteProjectionParameter(w, sig.Params[i]); + } + } + + /// Writes a constant value as a C# literal. Mirrors C++ write_constant partially. + public static string FormatField(FieldDefinition field) + { + if (field.Constant is null) { return string.Empty; } + return FormatConstant(field.Constant); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ObjRefs.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ObjRefs.cs new file mode 100644 index 0000000000..4b527d9be8 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ObjRefs.cs @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// ObjRef field emission for runtime classes (mirrors C++ write_class_objrefs_definition +/// and helpers around write_objref_type_name / write_iid_guid). +/// +internal static partial class CodeWriters +{ + /// + /// Returns the field name for the given interface impl (e.g. _objRef_System_IDisposable). + /// Mirrors C++ write_objref_type_name: takes the projected interface name (with the + /// namespace forcibly included), strips the global:: prefix and replaces + /// non-identifier characters with _. + /// + public static string GetObjRefName(TypeWriter w, ITypeDefOrRef ifaceType) + { + // Build the projected, fully-qualified name with global::. + string projected; + if (ifaceType is TypeDefinition td) + { + string ns = td.Namespace?.Value ?? string.Empty; + string name = td.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + projected = "global::" + ns + "." + Helpers.StripBackticks(name); + } + else if (ifaceType is TypeReference tr) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + projected = "global::" + ns + "." + Helpers.StripBackticks(name); + } + else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + // Generic instantiation: always use fully qualified name (with global::) for the objref + // name computation, so the resulting field name is unique across namespaces. This + // matches truth output: e.g. _objRef_System_Collections_Generic_IReadOnlyList_global__Windows_Web_Http_HttpCookie_ + // (with 'global__' coming from escaping the global:: prefix). + projected = w.WriteTemp("%", new Action(_ => + { + WriteFullyQualifiedInterfaceName(w, ifaceType); + })); + } + else + { + projected = w.WriteTemp("%", new Action(_ => + { + WriteFullyQualifiedInterfaceName(w, ifaceType); + })); + } + return "_objRef_" + EscapeTypeNameForIdentifier(projected, stripGlobal: true); + } + + /// + /// Like but always emits a fully qualified name with + /// global:: prefix on every type (even same-namespace ones). Used for objref name + /// computation where uniqueness across namespaces matters. + /// + private static void WriteFullyQualifiedInterfaceName(TypeWriter w, ITypeDefOrRef ifaceType) + { + if (ifaceType is TypeDefinition td) + { + string ns = td.Namespace?.Value ?? string.Empty; + string name = td.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + w.Write("global::"); + if (!string.IsNullOrEmpty(ns)) { w.Write(ns); w.Write("."); } + w.WriteCode(Helpers.StripBackticks(name)); + } + else if (ifaceType is TypeReference tr) + { + string ns = tr.Namespace?.Value ?? string.Empty; + string name = tr.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + w.Write("global::"); + if (!string.IsNullOrEmpty(ns)) { w.Write(ns); w.Write("."); } + w.WriteCode(Helpers.StripBackticks(name)); + } + else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ITypeDefOrRef gt = gi.GenericType; + string ns = gt.Namespace?.Value ?? string.Empty; + string name = gt.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + w.Write("global::"); + if (!string.IsNullOrEmpty(ns)) { w.Write(ns); w.Write("."); } + w.WriteCode(Helpers.StripBackticks(name)); + w.Write("<"); + for (int i = 0; i < gi.TypeArguments.Count; i++) + { + if (i > 0) { w.Write(", "); } + // forceWriteNamespace=true so generic args also get global:: prefix. + WriteTypeName(w, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, true); + } + w.Write(">"); + } + } + + /// + /// Writes the IID expression for the given interface impl (used as the second arg to + /// NativeObjectReference.As(...)). Mirrors C++ write_iid_guid. + /// + public static void WriteIidExpression(TypeWriter w, ITypeDefOrRef ifaceType) + { + // Generic instantiation: use the UnsafeAccessor extern method declared above the field + // (e.g. IID_Windows_Foundation_Collections_IObservableMap_string__object_(null)). + if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + string propName = BuildIidPropertyNameForGenericInterface(w, gi); + w.Write(propName); + w.Write("(null)"); + return; + } + + string ns; + string name; + bool isMapped; + if (ifaceType is TypeDefinition td) + { + ns = td.Namespace?.Value ?? string.Empty; + name = td.Name?.Value ?? string.Empty; + isMapped = MappedTypes.Get(ns, name) is not null; + } + else if (ifaceType is TypeReference tr) + { + ns = tr.Namespace?.Value ?? string.Empty; + name = tr.Name?.Value ?? string.Empty; + isMapped = MappedTypes.Get(ns, name) is not null; + } + else + { + w.Write("default(global::System.Guid)"); + return; + } + + if (isMapped) + { + // IStringable maps to a simpler IID name in WellKnownInterfaceIIDs. + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is { MappedName: "IStringable" }) + { + w.Write("global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable"); + return; + } + // Mapped interface: use WellKnownInterfaceIIDs.IID_. + // The non-projected name is the original WinRT interface (e.g. "Windows.Foundation.IClosable"). + string id = EscapeIdentifier(ns + "." + Helpers.StripBackticks(name)); + w.Write("global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_"); + w.Write(id); + } + else + { + // Non-mapped, non-generic: ABI.InterfaceIIDs.IID_. + // Uses the "ABI." prefix on the namespace, escaped with stripGlobalABI. + string abiQualified = "global::ABI." + ns + "." + Helpers.StripBackticks(name); + string id = EscapeTypeNameForIdentifier(abiQualified, stripGlobal: false, stripGlobalABI: true); + w.Write("global::ABI.InterfaceIIDs.IID_"); + w.Write(id); + } + } + + /// + /// Builds the IID property name for a generic interface instantiation. Mirrors C++ + /// write_iid_guid_property_name: write_type_name(type, ABI, true) + escape. + /// E.g. IObservableMap<string, object> -> IID_Windows_Foundation_Collections_IObservableMap_string__object_. + /// + private static string BuildIidPropertyNameForGenericInterface(TypeWriter w, GenericInstanceTypeSignature gi) + { + TypeSemantics sem = TypeSemanticsFactory.Get(gi); + string name = w.WriteTemp("%", new System.Action(_ => + { + WriteTypeName(w, sem, TypedefNameType.ABI, forceWriteNamespace: true); + })); + return "IID_" + EscapeTypeNameForIdentifier(name, stripGlobal: true, stripGlobalABI: true); + } + + /// + /// Emits the [UnsafeAccessor] extern method declaration that exposes the IID for a generic + /// interface instantiation. Mirrors C++ write_unsafe_accessor_for_iid. + /// + /// When true, the accessor's parameter type is + /// object? (used inside #nullable enable regions); otherwise object. + private static void EmitUnsafeAccessorForIid(TypeWriter w, GenericInstanceTypeSignature gi, bool isInNullableContext = false) + { + string propName = BuildIidPropertyNameForGenericInterface(w, gi); + string interopName = EncodeInteropTypeName(gi, TypedefNameType.InteropIID); + w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"get_IID_"); + w.Write(interopName); + w.Write("\")]\n"); + w.Write("static extern ref readonly Guid "); + w.Write(propName); + w.Write("([UnsafeAccessorType(\"ABI.InterfaceIIDs, WinRT.Interop\")] object"); + if (isInNullableContext) { w.Write("?"); } + w.Write(" _);\n"); + } + + private static string EscapeIdentifier(string s) + { + System.Text.StringBuilder sb = new(s.Length); + foreach (char c in s) + { + sb.Append((c == ' ' || c == ':' || c == '<' || c == '>' || c == '`' || c == ',' || c == '.') ? '_' : c); + } + return sb.ToString(); + } + + /// + /// Writes the IReference<T> IID expression for a value type (used by BoxToUnmanaged). + /// Mirrors the C++ output: global::ABI.InterfaceIIDs.IID_<EscapedABIName>Reference. + /// + public static void WriteIidReferenceExpression(TypeWriter w, TypeDefinition type) + { + string ns = type.Namespace?.Value ?? string.Empty; + string name = type.Name?.Value ?? string.Empty; + string abiQualified = "global::ABI." + ns + "." + Helpers.StripBackticks(name); + string id = EscapeTypeNameForIdentifier(abiQualified, stripGlobal: false, stripGlobalABI: true); + w.Write("global::ABI.InterfaceIIDs.IID_"); + w.Write(id); + w.Write("Reference"); + } + + /// + /// Emits the lazy _objRef_* field definitions for each interface implementation on + /// the given runtime class (mirrors C++ write_class_objrefs_definition). + /// The C++ uses replaceDefaultByInner = type.Flags().Sealed(): for sealed classes, + /// the default interface is emitted as a simple => NativeObjectReference expression; + /// for unsealed classes, ALL interfaces (including the default) use the lazy + /// MakeObjectReference pattern, and the default also gets an init; accessor so the + /// constructor can set it via _objRef_X = NativeObjectReference. + /// + public static void WriteClassObjRefDefinitions(TypeWriter w, TypeDefinition type) + { + // Per-interface _objRef_* getters are emitted in BOTH impl and ref modes with full + // bodies. C++ write_class_objrefs_definition has no settings.reference_projection + // gate. Truth ref-mode output keeps the full Interlocked.CompareExchange + + // NativeObjectReference.As(IID_X(null)) lazy-init bodies. (Only the static factory + // _objRef_* getters become `throw null;` in ref mode — see WriteStaticFactoryObjRef + // and WriteAttributedTypes.) + + // Track names emitted so we don't emit duplicates (e.g. when both IFoo and IFoo2 + // produce the same _objRef_). + HashSet emitted = new(System.StringComparer.Ordinal); + bool isSealed = type.IsSealed; + + // Pass 1: emit objrefs for ALL directly-declared interfaces first (in InterfaceImpl + // declaration order). Pass 2 then walks transitive parents to cover any not yet emitted. + // This mirrors C++ which emits all direct impls first before recursing — so for a class + // that declares 'IFoo, IBar' where IFoo : IBaz, the order is IFoo, IBar, IBaz, NOT + // IFoo, IBaz, IBar. + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) { continue; } + if (!IsInterfaceInInheritanceList(impl, includeExclusiveInterface: false) + && !IsInterfaceForObjRef(impl)) + { + continue; + } + + // Mirrors C++ write_class_objrefs_definition (code_writers.h:2960): for fast-abi + // classes, skip non-default exclusive interfaces — their methods dispatch through + // the default interface's vtable so a separate objref is unnecessary. + bool isDefault = TypeCategorization.HasAttribute(impl, "Windows.Foundation.Metadata", "DefaultAttribute"); + if (!isDefault && IsFastAbiClass(type, w.Settings)) + { + TypeDefinition? implTypeDef = ResolveInterfaceTypeDef(impl.Interface); + if (implTypeDef is not null && TypeCategorization.IsExclusiveTo(implTypeDef)) + { + continue; + } + } + + EmitObjRefForInterface(w, impl.Interface, emitted, isDefault: isDefault, useSimplePattern: isSealed && isDefault); + } + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) { continue; } + if (!IsInterfaceInInheritanceList(impl, includeExclusiveInterface: false) + && !IsInterfaceForObjRef(impl)) + { + continue; + } + // Same fast-abi guard as the first pass. + bool isDefault2 = TypeCategorization.HasAttribute(impl, "Windows.Foundation.Metadata", "DefaultAttribute"); + if (!isDefault2 && IsFastAbiClass(type, w.Settings)) + { + TypeDefinition? implTypeDef = ResolveInterfaceTypeDef(impl.Interface); + if (implTypeDef is not null && TypeCategorization.IsExclusiveTo(implTypeDef)) + { + continue; + } + } + EmitTransitiveInterfaceObjRefs(w, impl.Interface, emitted); + } + } + + /// Emits an _objRef_ field for a single interface impl reference. + /// When true, emit the simple expression-bodied form + /// => NativeObjectReference. Otherwise emit the lazy MakeObjectReference pattern. + private static void EmitObjRefForInterface(TypeWriter w, ITypeDefOrRef ifaceRef, HashSet emitted, bool isDefault, bool useSimplePattern = false) + { + string objRefName = GetObjRefName(w, ifaceRef); + if (!emitted.Add(objRefName)) { return; } + + // Mirrors C++ write_class_objrefs_definition: for generic interface instantiations, emit + // the [UnsafeAccessor] extern method declaration (used by the IID expression in both + // simple and lazy patterns). + bool isGenericInstance = ifaceRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature; + GenericInstanceTypeSignature? gi = isGenericInstance + ? (GenericInstanceTypeSignature)((TypeSpecification)ifaceRef).Signature! + : null; + + if (useSimplePattern) + { + // Sealed-class default interface: simple expression-bodied property pointing at NativeObjectReference. + w.Write("private WindowsRuntimeObjectReference "); + w.Write(objRefName); + w.Write(" => NativeObjectReference;\n"); + // Emit the unsafe accessor AFTER the field so it can be used to pass the IID in the + // constructor for the default interface (mirrors C++ ordering at line ~3018-3022). + if (gi is not null) + { + EmitUnsafeAccessorForIid(w, gi); + } + } + else + { + // Emit the unsafe accessor BEFORE the lazy field so it's referenced inside the As(...) call. + if (gi is not null) + { + EmitUnsafeAccessorForIid(w, gi); + } + // Lazy CompareExchange pattern. For unsealed-class defaults, also emit 'init;' so the + // constructor can assign NativeObjectReference for the exact-type case. + w.Write("private WindowsRuntimeObjectReference "); + w.Write(objRefName); + w.Write("\n{\n"); + w.Write(" get\n {\n"); + w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); + w.Write(" WindowsRuntimeObjectReference MakeObjectReference()\n {\n"); + w.Write(" _ = global::System.Threading.Interlocked.CompareExchange(\n"); + w.Write(" location1: ref field,\n"); + w.Write(" value: NativeObjectReference.As("); + WriteIidExpression(w, ifaceRef); + w.Write("),\n"); + w.Write(" comparand: null);\n\n"); + w.Write(" return field;\n }\n\n"); + w.Write(" return field ?? MakeObjectReference();\n }\n"); + if (isDefault) { w.Write(" init;\n"); } + w.Write("}\n"); + } + } + + /// + /// Walks transitively-inherited interfaces and emits an objref field for each one. Mirrors + /// the recursive interface walk needed for mapped collection dispatch. + /// + private static void EmitTransitiveInterfaceObjRefs(TypeWriter w, ITypeDefOrRef ifaceRef, HashSet emitted) + { + // Resolve the interface to its TypeDefinition; if cross-module, look it up in the cache. + TypeDefinition? ifaceTd = ResolveInterfaceTypeDef(ifaceRef); + if (ifaceTd is null) { return; } + + // Compute a substitution context if the parent is a closed generic instance. + AsmResolver.DotNet.Signatures.GenericContext? ctx = null; + if (ifaceRef is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) + { + ctx = new AsmResolver.DotNet.Signatures.GenericContext(gi, null); + } + + foreach (InterfaceImplementation childImpl in ifaceTd.Interfaces) + { + if (childImpl.Interface is null) { continue; } + + // If the parent is a closed generic, substitute the child's signature. + ITypeDefOrRef childRef = childImpl.Interface; + if (ctx is not null) + { + AsmResolver.DotNet.Signatures.TypeSignature childSig = childRef.ToTypeSignature(false); + AsmResolver.DotNet.Signatures.TypeSignature substitutedSig = childSig.InstantiateGenericTypes(ctx.Value); + AsmResolver.DotNet.ITypeDefOrRef? newRef = substitutedSig.ToTypeDefOrRef(); + if (newRef is not null) { childRef = newRef; } + } + + // Skip exclusive-to-someone-else interfaces. Mirrors EmitImplType-like check. + // For now, just emit (no-op if exclusive — the field still works for QI lookup). + EmitObjRefForInterface(w, childRef, emitted, isDefault: false); + EmitTransitiveInterfaceObjRefs(w, childRef, emitted); + } + } + + /// + /// Whether this interface impl needs an _objRef_* field even though it isn't part of the + /// inheritance list (e.g. ExclusiveTo interfaces still need their objref since instance + /// methods/properties dispatch through it). + /// + public static bool IsInterfaceForObjRef(InterfaceImplementation impl) + { + // For now, emit objrefs for ALL implemented interfaces — instance member dispatch + // needs to be able to reach them. + return impl.Interface is not null; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.RefModeStubs.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.RefModeStubs.cs new file mode 100644 index 0000000000..ff0414f77e --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.RefModeStubs.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Reference-projection stub emission helpers. In reference projection mode, all method/property/ +/// event bodies (and certain other constructs like static factory objref getters, activation +/// factory objref getters, and the synthetic private ctor for classes without explicit +/// constructors) collapse to throw null. Mirrors C++ code_writers.h:1639/1655/1671/ +/// 1685/1699/1713/2755/2796/2217/2240/6851/9536. +/// +internal static partial class CodeWriters +{ + /// + /// Emits the body of an _objRef_* property getter in reference projection mode. + /// Mirrors C++ write_static_objref_definition / write_activation_factory_objref_definition + /// (code_writers.h:2755 and 2796) which emit { get { throw null; } } in ref mode. + /// + public static void EmitRefModeObjRefGetterBody(TypeWriter w) + { + w.Write("\n{\n get\n {\n throw null;\n }\n}\n"); + } + + /// + /// Emits the synthetic private TypeName() { throw null; } ctor used in reference + /// projection mode to suppress the C# compiler's implicit public default constructor when + /// no explicit ctors are emitted by WriteAttributedTypes. + /// Mirrors C++ code_writers.h:9536. + /// + public static void EmitSyntheticPrivateCtor(TypeWriter w, string typeName) + { + w.Write("\nprivate "); + w.Write(typeName); + w.Write("() { throw null; }\n"); + } + + /// + /// Emits the body of a delegate factory Invoke method in reference projection mode. + /// Mirrors C++ code_writers.h:6851 which emits throw null; for the activator + /// factory delegate's Invoke body in ref mode. + /// + public static void EmitRefModeInvokeBody(TypeWriter w) + { + w.Write(" throw null;\n }\n}\n"); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.TypeNames.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.TypeNames.cs new file mode 100644 index 0000000000..b602caefef --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.TypeNames.cs @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Type-name emission helpers, mirroring C++ code_writers.h. +/// +internal static partial class CodeWriters +{ + /// Mirrors C++ write_fundamental_type. + public static void WriteFundamentalType(TextWriter w, FundamentalType t) + { + w.Write(FundamentalTypes.ToCSharpType(t)); + } + + /// Mirrors C++ write_fundamental_non_projected_type. + public static void WriteFundamentalNonProjectedType(TextWriter w, FundamentalType t) + { + w.Write(FundamentalTypes.ToDotNetType(t)); + } + + /// Mirrors C++ write_typedef_name: writes the C# type name for a typed reference. + public static void WriteTypedefName(TypeWriter w, TypeDefinition type, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) + { + bool authoredType = w.Settings.Component && w.Settings.Filter.Includes(type); + string typeNamespace = type.Namespace?.Value ?? string.Empty; + string typeName = type.Name?.Value ?? string.Empty; + + if (nameType == TypedefNameType.NonProjected) + { + w.Write(typeNamespace); + w.Write("."); + w.Write(typeName); + return; + } + + MappedType? proj = MappedTypes.Get(typeNamespace, typeName); + if (proj is not null) + { + typeNamespace = proj.MappedNamespace; + typeName = proj.MappedName; + } + + // Exclusive interfaces handling: simplified port — we don't try to resolve exclusive_to_type from + // attributes here. Only used in component mode which we don't fully implement here yet. + TypedefNameType nameToWrite = nameType; + if (authoredType && TypeCategorization.IsExclusiveTo(type) && nameToWrite == TypedefNameType.Projected) + { + // Fallback: switch to CCW if the type is not the default interface for its exclusive class. + nameToWrite = TypedefNameType.CCW; + } + + // Authored interfaces that aren't exclusive use the same authored interface. + if (authoredType && nameToWrite == TypedefNameType.CCW && + TypeCategorization.GetCategory(type) == TypeCategory.Interface && + !TypeCategorization.IsExclusiveTo(type)) + { + nameToWrite = TypedefNameType.Projected; + } + + if (nameToWrite == TypedefNameType.EventSource && typeNamespace == "System") + { + w.Write("global::WindowsRuntime.InteropServices."); + } + else if (forceWriteNamespace || + typeNamespace != w.CurrentNamespace || + (nameToWrite == TypedefNameType.Projected && (w.InAbiNamespace || w.InAbiImplNamespace)) || + (nameToWrite == TypedefNameType.ABI && !w.InAbiNamespace) || + (nameToWrite == TypedefNameType.EventSource && !w.InAbiNamespace) || + (nameToWrite == TypedefNameType.CCW && authoredType && !w.InAbiImplNamespace) || + (nameToWrite == TypedefNameType.CCW && !authoredType && (w.InAbiNamespace || w.InAbiImplNamespace))) + { + w.Write("global::"); + if (nameToWrite is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) + { + w.Write("ABI."); + } + else if (authoredType && nameToWrite == TypedefNameType.CCW) + { + w.Write("ABI.Impl."); + } + w.Write(typeNamespace); + w.Write("."); + } + + if (nameToWrite == TypedefNameType.StaticAbiClass) + { + w.WriteCode(typeName); + w.Write("Methods"); + } + else if (nameToWrite == TypedefNameType.EventSource) + { + w.WriteCode(typeName); + w.Write("EventSource"); + } + else + { + w.WriteCode(typeName); + } + } + + /// Mirrors C++ write_type_params: writes <T1, T2> for generic types. + public static void WriteTypeParams(TypeWriter w, TypeDefinition type) + { + if (type.GenericParameters.Count == 0) { return; } + w.Write("<"); + for (int i = 0; i < type.GenericParameters.Count; i++) + { + if (i > 0) { w.Write(", "); } + // For now, emit "T0", "T1" style placeholders - full generic args support requires the writer's stack. + string? gpName = type.GenericParameters[i].Name?.Value; + w.Write(gpName ?? $"T{i}"); + } + w.Write(">"); + } + + /// Mirrors C++ write_type_name: writes the typedef name + generic params. + public static void WriteTypeName(TypeWriter w, TypeSemantics semantics, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) + { + switch (semantics) + { + case TypeSemantics.Fundamental f: + WriteFundamentalType(w, f.Type); + break; + case TypeSemantics.Object_: + w.Write("object"); + break; + case TypeSemantics.Guid_: + w.Write("Guid"); + break; + case TypeSemantics.Type_: + w.Write("Type"); + break; + case TypeSemantics.Definition d: + WriteTypedefName(w, d.Type, nameType, forceWriteNamespace); + WriteTypeParams(w, d.Type); + break; + case TypeSemantics.GenericInstance gi: + WriteTypedefName(w, gi.GenericType, nameType, forceWriteNamespace); + w.Write("<"); + for (int i = 0; i < gi.GenericArgs.Count; i++) + { + if (i > 0) { w.Write(", "); } + // Generic args ALWAYS use Projected, regardless of parent's nameType. + // Mirrors C++ write_type_params -> write_generic_type_name_base -> write_projection_type + // (which is hard-coded to typedef_name_type::Projected). + WriteTypeName(w, gi.GenericArgs[i], TypedefNameType.Projected, forceWriteNamespace); + } + w.Write(">"); + break; + case TypeSemantics.GenericInstanceRef gir: + // Emit the type reference's full name with global:: qualification, applying mapped-type + // remapping if applicable (e.g., Windows.Foundation.IReference`1 -> System.Nullable, + // Windows.Foundation.TypedEventHandler`2 -> System.EventHandler). + { + string ns = gir.GenericType.Namespace?.Value ?? string.Empty; + string name = gir.GenericType.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + // Handle EventSource for Windows.Foundation event handlers (TypedEventHandler -> + // EventHandlerEventSource in WindowsRuntime.InteropServices). + if (nameType == TypedefNameType.EventSource && ns == "System") + { + w.Write("global::WindowsRuntime.InteropServices."); + } + else if (!string.IsNullOrEmpty(ns)) + { + w.Write("global::"); + if (nameType is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) + { + w.Write("ABI."); + } + w.Write(ns); + w.Write("."); + } + w.WriteCode(name); + if (nameType == TypedefNameType.StaticAbiClass) { w.Write("Methods"); } + else if (nameType == TypedefNameType.EventSource) { w.Write("EventSource"); } + + w.Write("<"); + for (int i = 0; i < gir.GenericArgs.Count; i++) + { + if (i > 0) { w.Write(", "); } + // Generic args ALWAYS use Projected, regardless of parent's nameType. + // Mirrors C++ write_type_params -> write_generic_type_name_base -> write_projection_type. + WriteTypeName(w, gir.GenericArgs[i], TypedefNameType.Projected, forceWriteNamespace); + } + w.Write(">"); + } + break; + case TypeSemantics.Reference r: + { + string ns = r.Reference_.Namespace?.Value ?? string.Empty; + string name = r.Reference_.Name?.Value ?? string.Empty; + MappedType? mapped = MappedTypes.Get(ns, name); + if (mapped is not null) + { + ns = mapped.MappedNamespace; + name = mapped.MappedName; + } + bool needsNsPrefix = !string.IsNullOrEmpty(ns) && ( + forceWriteNamespace || + ns != w.CurrentNamespace || + (nameType == TypedefNameType.Projected && (w.InAbiNamespace || w.InAbiImplNamespace)) || + (nameType == TypedefNameType.ABI && !w.InAbiNamespace) || + (nameType == TypedefNameType.EventSource && !w.InAbiNamespace) || + (nameType == TypedefNameType.CCW && (w.InAbiNamespace || w.InAbiImplNamespace))); + if (needsNsPrefix) + { + w.Write("global::"); + if (nameType is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) + { + w.Write("ABI."); + } + w.Write(ns); + w.Write("."); + } + w.WriteCode(name); + if (nameType == TypedefNameType.StaticAbiClass) { w.Write("Methods"); } + else if (nameType == TypedefNameType.EventSource) { w.Write("EventSource"); } + } + break; + case TypeSemantics.GenericTypeIndex gti: + w.Write($"T{gti.Index}"); + break; + } + } + + /// Mirrors C++ write_projection_type: writes a projected type name (.NET-style). + public static void WriteProjectionType(TypeWriter w, TypeSemantics semantics) + { + WriteTypeName(w, semantics, TypedefNameType.Projected, false); + } + + /// + /// Writes the event handler type for an EventDefinition. Handles all the cases: + /// TypeDefinition, TypeReference, TypeSpecification (generic instances like EventHandler<T>), + /// and any other ITypeDefOrRef. + /// + public static void WriteEventType(TypeWriter w, EventDefinition evt) + { + WriteEventType(w, evt, null); + } + + /// + /// Same as but applies the supplied + /// generic context for substitution (e.g., T0/T1 -> concrete type arguments + /// when emitting members for an instantiated parent generic interface). + /// + public static void WriteEventType(TypeWriter w, EventDefinition evt, AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? currentInstance) + { + if (evt.EventType is null) + { + w.Write("global::Windows.Foundation.EventHandler"); + return; + } + AsmResolver.DotNet.Signatures.TypeSignature sig = evt.EventType.ToTypeSignature(false); + if (currentInstance is not null) + { + sig = sig.InstantiateGenericTypes(new AsmResolver.DotNet.Signatures.GenericContext(currentInstance, null)); + } + // Special case for Microsoft.UI.Xaml.Input.ICommand.CanExecuteChanged: the WinRT event + // handler is EventHandler but C# expects non-generic EventHandler. Mirrors C++: + // if (event.Name() == "CanExecuteChanged" && event_type == "global::System.EventHandler") + // check parent_type_name == ICommand and override event_type + if (evt.Name?.Value == "CanExecuteChanged" + && evt.DeclaringType is { } declaringType + && (declaringType.FullName == "Microsoft.UI.Xaml.Input.ICommand" + || declaringType.FullName == "Windows.UI.Xaml.Input.ICommand")) + { + // Verify the event type matches EventHandler before applying override. + if (sig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi + && gi.GenericType.Namespace?.Value == "Windows.Foundation" + && gi.GenericType.Name?.Value == "EventHandler`1" + && gi.TypeArguments.Count == 1 + && gi.TypeArguments[0] is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib + && corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object) + { + w.Write("global::System.EventHandler"); + return; + } + } + // Mirrors C++ write_event: typedef_name_type::Projected, forceWriteNamespace=false. + // The outer EventHandler still gets 'global::System.' from being in a different namespace, + // but type args in the same namespace stay unqualified. + WriteTypeName(w, TypeSemanticsFactory.Get(sig), TypedefNameType.Projected, false); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.cs new file mode 100644 index 0000000000..88b5861724 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.cs @@ -0,0 +1,430 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ code_writers.h. Emits projected and ABI types. +/// +/// **STATUS**: This is a partial 1:1 port. The C++ code_writers.h file is ~11K lines containing +/// 295 functions. This C# port is a work-in-progress. The current implementation produces minimal +/// stub output for each type category to validate the architecture end-to-end. +/// +/// +internal static partial class CodeWriters +{ + /// + /// Dispatches type emission based on the type category. + /// + public static void WriteType(TypeWriter w, TypeDefinition type, TypeCategory category, Settings settings, MetadataCache cache) + { + switch (category) + { + case TypeCategory.Class: + if (TypeCategorization.IsAttributeType(type)) + { + WriteAttribute(w, type); + } + else + { + WriteClass(w, type); + } + break; + case TypeCategory.Delegate: + WriteDelegate(w, type); + break; + case TypeCategory.Enum: + WriteEnum(w, type); + break; + case TypeCategory.Interface: + WriteInterface(w, type); + break; + case TypeCategory.Struct: + if (TypeCategorization.IsApiContractType(type)) + { + WriteContract(w, type); + } + else + { + WriteStruct(w, type); + } + break; + } + } + + /// + /// Dispatches ABI emission based on the type category. + /// + public static void WriteAbiType(TypeWriter w, TypeDefinition type, TypeCategory category, Settings settings) + { + switch (category) + { + case TypeCategory.Class: + WriteAbiClass(w, type); + break; + case TypeCategory.Delegate: + WriteAbiDelegate(w, type); + WriteTempDelegateEventSourceSubclass(w, type); + break; + case TypeCategory.Enum: + WriteAbiEnum(w, type); + break; + case TypeCategory.Interface: + WriteAbiInterface(w, type); + break; + case TypeCategory.Struct: + WriteAbiStruct(w, type); + break; + } + } + + // ABI emission methods are implemented in CodeWriters.Abi.cs + + /// + /// Mirrors C++ write_enum. Emits an enum projection. + /// + public static void WriteEnum(TypeWriter w, TypeDefinition type) + { + if (w.Settings.Component) + { + return; + } + + bool isFlags = TypeCategorization.IsFlagsEnum(type); + string enumUnderlyingType = isFlags ? "uint" : "int"; + string accessibility = w.Settings.Internal ? "internal" : "public"; + string typeName = type.Name?.Value ?? string.Empty; + + if (isFlags) + { + w.Write("\n[FlagsAttribute]\n"); + } + else + { + w.Write("\n"); + } + WriteWinRTMetadataAttribute(w, type, _cacheRef!); + WriteValueTypeWinRTClassNameAttribute(w, type); + WriteTypeCustomAttributes(w, type, true); + WriteComWrapperMarshallerAttribute(w, type); + WriteWinRTReferenceTypeAttribute(w, type); + + w.Write(accessibility); + w.Write(" enum "); + w.Write(typeName); + w.Write(" : "); + w.Write(enumUnderlyingType); + w.Write("\n{\n"); + + foreach (FieldDefinition field in type.Fields) + { + if (field.Constant is null) + { + continue; + } + string fieldName = field.Name?.Value ?? string.Empty; + string constantValue = FormatConstant(field.Constant); + + // Mirror C++ code_writers.h:10106 write_platform_attribute(field.CustomAttribute()): + // emits per-enum-field [SupportedOSPlatform] when the field has a [ContractVersion]. + WritePlatformAttribute(w, field); + w.Write(fieldName); + w.Write(" = unchecked(("); + w.Write(enumUnderlyingType); + w.Write(")"); + w.Write(constantValue); + w.Write("),\n"); + } + w.Write("}\n\n"); + } + + /// + /// Formats a metadata Constant value as a C# literal. + /// Mirrors C++ write_constant: I4/U4 are formatted as hex (e.g. 0x1) to match + /// the truth output. Other types fall back to decimal. + /// + private static string FormatConstant(AsmResolver.DotNet.Constant constant) + { + // The Constant.Value contains raw bytes representing the value + AsmResolver.PE.DotNet.Metadata.Tables.ElementType type = constant.Type; + byte[] data = constant.Value?.Data ?? System.Array.Empty(); + return type switch + { + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 => ((sbyte)data[0]).ToString(System.Globalization.CultureInfo.InvariantCulture), + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 => data[0].ToString(System.Globalization.CultureInfo.InvariantCulture), + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 => System.BitConverter.ToInt16(data, 0).ToString(System.Globalization.CultureInfo.InvariantCulture), + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 => System.BitConverter.ToUInt16(data, 0).ToString(System.Globalization.CultureInfo.InvariantCulture), + // I4/U4 use printf "%#0x" semantics: 0 -> "0", non-zero -> "0x" (alternate hex form omits prefix when value is zero). + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 => FormatHexAlternate((uint)System.BitConverter.ToInt32(data, 0)), + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 => FormatHexAlternate(System.BitConverter.ToUInt32(data, 0)), + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 => System.BitConverter.ToInt64(data, 0).ToString(System.Globalization.CultureInfo.InvariantCulture), + AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 => System.BitConverter.ToUInt64(data, 0).ToString(System.Globalization.CultureInfo.InvariantCulture), + _ => "0" + }; + } + + private static string FormatHexAlternate(uint v) + { + // C++ printf "%#0x": for 0, outputs "0"; for non-zero, outputs "0x" with no padding. + if (v == 0) { return "0"; } + return "0x" + v.ToString("x", System.Globalization.CultureInfo.InvariantCulture); + } + + /// + /// Mirrors C++ write_struct. Emits a struct projection. + /// + public static void WriteStruct(TypeWriter w, TypeDefinition type) + { + if (w.Settings.Component) { return; } + + // Collect field info + System.Collections.Generic.List<(string TypeStr, string Name, string ParamName, bool IsInterface)> fields = new(); + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + TypeSemantics semantics = TypeSemanticsFactory.Get(field.Signature.FieldType); + string fieldType = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, semantics))); + string fieldName = field.Name?.Value ?? string.Empty; + string paramName = ToCamelCase(fieldName); + bool isInterface = false; + if (semantics is TypeSemantics.Definition d) + { + isInterface = TypeCategorization.GetCategory(d.Type) == TypeCategory.Interface; + } + else if (semantics is TypeSemantics.GenericInstance gi) + { + isInterface = TypeCategorization.GetCategory(gi.GenericType) == TypeCategory.Interface; + } + fields.Add((fieldType, fieldName, paramName, isInterface)); + } + + string projectionName = type.Name?.Value ?? string.Empty; + bool hasAddition = AdditionTypes.HasAdditionToType(type.Namespace?.Value ?? string.Empty, projectionName); + + // Header attributes + WriteWinRTMetadataAttribute(w, type, _cacheRef!); + WriteValueTypeWinRTClassNameAttribute(w, type); + WriteTypeCustomAttributes(w, type, true); + WriteComWrapperMarshallerAttribute(w, type); + WriteWinRTReferenceTypeAttribute(w, type); + w.Write("public"); + if (hasAddition) { w.Write(" partial"); } + w.Write(" struct "); + w.Write(projectionName); + w.Write(": IEquatable<"); + w.Write(projectionName); + w.Write(">\n{\n"); + + // ctor + w.Write("public "); + w.Write(projectionName); + w.Write("("); + for (int i = 0; i < fields.Count; i++) + { + if (i > 0) { w.Write(", "); } + w.Write(fields[i].TypeStr); + w.Write(" "); + Helpers.WriteEscapedIdentifier(w, fields[i].ParamName); + } + w.Write(")\n{\n"); + foreach (var f in fields) + { + // When the param name matches the field name (i.e. ToCamelCase couldn't change casing), + // qualify with this. to disambiguate. + if (f.Name == f.ParamName) + { + w.Write("this."); + w.Write(f.Name); + w.Write(" = "); + Helpers.WriteEscapedIdentifier(w, f.ParamName); + w.Write("; "); + } + else + { + w.Write(f.Name); + w.Write(" = "); + Helpers.WriteEscapedIdentifier(w, f.ParamName); + w.Write("; "); + } + } + w.Write("\n}\n"); + + // properties + foreach (var f in fields) + { + w.Write("public "); + w.Write(f.TypeStr); + w.Write(" "); + w.Write(f.Name); + w.Write("\n{\nreadonly get; set;\n}\n"); + } + + // == + w.Write("public static bool operator ==("); + w.Write(projectionName); + w.Write(" x, "); + w.Write(projectionName); + w.Write(" y) => "); + if (fields.Count == 0) + { + w.Write("true"); + } + else + { + for (int i = 0; i < fields.Count; i++) + { + if (i > 0) { w.Write(" && "); } + w.Write("x."); + w.Write(fields[i].Name); + w.Write(" == y."); + w.Write(fields[i].Name); + } + } + w.Write(";\n"); + + // != + w.Write("public static bool operator !=("); + w.Write(projectionName); + w.Write(" x, "); + w.Write(projectionName); + w.Write(" y) => !(x == y);\n"); + + // equals + w.Write("public bool Equals("); + w.Write(projectionName); + w.Write(" other) => this == other;\n"); + + w.Write("public override bool Equals(object obj) => obj is "); + w.Write(projectionName); + w.Write(" that && this == that;\n"); + + // hashcode + w.Write("public override int GetHashCode() => "); + if (fields.Count == 0) + { + w.Write("0"); + } + else + { + for (int i = 0; i < fields.Count; i++) + { + if (i > 0) { w.Write(" ^ "); } + w.Write(fields[i].Name); + w.Write(".GetHashCode()"); + } + } + w.Write(";\n"); + w.Write("}\n\n"); + } + + /// + /// Mirrors C++ write_contract. Emits a static class for an API contract. + /// + public static void WriteContract(TypeWriter w, TypeDefinition type) + { + if (w.Settings.Component) { return; } + + string typeName = type.Name?.Value ?? string.Empty; + WriteTypeCustomAttributes(w, type, false); + w.Write(Helpers.InternalAccessibility(w.Settings)); + w.Write(" enum "); + w.Write(typeName); + w.Write("\n{\n}\n"); + } + + /// + /// Mirrors C++ write_delegate. Emits a delegate projection. + /// + public static void WriteDelegate(TypeWriter w, TypeDefinition type) + { + if (w.Settings.Component) { return; } + + MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); + if (invoke is null) { return; } + MethodSig sig = new(invoke); + + w.Write("\n"); + WriteWinRTMetadataAttribute(w, type, _cacheRef!); + WriteTypeCustomAttributes(w, type, false); + WriteComWrapperMarshallerAttribute(w, type); + if (!w.Settings.ReferenceProjection) + { + // GUID attribute + w.Write("[Guid(\""); + WriteGuid(w, type, false); + w.Write("\")]\n"); + } + w.Write(Helpers.InternalAccessibility(w.Settings)); + w.Write(" delegate "); + WriteProjectionReturnType(w, sig); + w.Write(" "); + WriteTypedefName(w, type, TypedefNameType.Projected, false); + WriteTypeParams(w, type); + w.Write("("); + WriteParameterList(w, sig); + w.Write(");\n"); + } + + /// + /// Mirrors C++ write_attribute. Emits an attribute projection. + /// + public static void WriteAttribute(TypeWriter w, TypeDefinition type) + { + string typeName = type.Name?.Value ?? string.Empty; + + WriteWinRTMetadataAttribute(w, type, _cacheRef!); + WriteTypeCustomAttributes(w, type, true); + w.Write(Helpers.InternalAccessibility(w.Settings)); + w.Write(" sealed class "); + w.Write(typeName); + w.Write(": Attribute\n{\n"); + + // Constructors + foreach (MethodDefinition method in type.Methods) + { + if (method.Name?.Value != ".ctor") { continue; } + MethodSig sig = new(method); + w.Write("public "); + w.Write(typeName); + w.Write("("); + WriteParameterList(w, sig); + w.Write("){}\n"); + } + // Fields + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) { continue; } + w.Write("public "); + WriteProjectionType(w, TypeSemanticsFactory.Get(field.Signature.FieldType)); + w.Write(" "); + w.Write(field.Name?.Value ?? string.Empty); + w.Write(";\n"); + } + w.Write("}\n"); + } + + private static MetadataCache? _cacheRef; + + /// Sets the cache reference used by writers that need source-file paths. + public static void SetMetadataCache(MetadataCache cache) + { + _cacheRef = cache; + } + + /// Gets the metadata cache previously set via . + internal static MetadataCache? GetMetadataCache() => _cacheRef; + + /// Mirrors C++ to_camel_case. + public static string ToCamelCase(string name) + { + if (string.IsNullOrEmpty(name)) { return name; } + char c = name[0]; + if (c >= 'A' && c <= 'Z') + { + return char.ToLowerInvariant(c) + name.Substring(1); + } + return name; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/TextWriter.cs b/src/WinRT.Projection.Generator.Writer/Writers/TextWriter.cs new file mode 100644 index 0000000000..6907d9dee2 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/TextWriter.cs @@ -0,0 +1,358 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ indented_writer_base in text_writer.h. +/// +/// Supports the C++ format placeholders: +/// +/// %: Insert any value (calls ). +/// @: Insert a code identifier (strips backticks, escapes invalid chars). +/// ^: Escape next character (usually %, @, or ^). +/// +/// Indentation is automatically managed: { increases indent by 4 spaces, } decreases. +/// +/// +internal class TextWriter +{ + private const int TabWidth = 4; + + private enum WriterState + { + None, + OpenParen, + OpenParenNewline, + } + + protected readonly List _first = new(16 * 1024); + private readonly List _second = new(); + + private WriterState _state = WriterState.None; + private readonly List _scopes = new() { 0 }; + private int _indent; + private bool _enableIndent = true; + + /// Writes a literal string verbatim (with indentation handling). + public void Write(ReadOnlySpan value) + { + for (int i = 0; i < value.Length; i++) + { + WriteChar(value[i]); + } + } + + /// Writes a literal string verbatim (with indentation handling). + public void Write(string value) + { + Write(value.AsSpan()); + } + + /// Writes a single character (with indentation handling). + public void Write(char value) + { + WriteChar(value); + } + + /// Writes an integer value. + public void Write(int value) => Write(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + + /// Writes an unsigned integer value. + public void Write(uint value) => Write(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + + /// Writes a long value. + public void Write(long value) => Write(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + + /// Writes an unsigned long value. + public void Write(ulong value) => Write(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + + /// Calls a writer callback. + public void Write(Action callback) + { + callback(this); + } + + /// Writes a value boxed as object. + public virtual void WriteValue(object? value) + { + switch (value) + { + case null: + break; + case string s: + Write(s); + break; + case char c: + Write(c); + break; + case int i: + Write(i); + break; + case uint ui: + Write(ui); + break; + case long l: + Write(l); + break; + case ulong ul: + Write(ul); + break; + case Action a: + a(this); + break; + default: + Write(value.ToString() ?? string.Empty); + break; + } + } + + /// Writes a code identifier (default: same as WriteValue, but specific writers may override). + public virtual void WriteCode(object? value) + { + if (value is string s) + { + WriteCode(s); + } + else + { + WriteValue(value); + } + } + + /// Writes a code identifier, stripping anything from a backtick onwards (matches C++ writer.write_code). + public virtual void WriteCode(string value) + { + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + if (c == '`') + { + return; + } + WriteChar(c); + } + } + + /// Writes a format string, with C++-style %/@/^ placeholders. + public void Write(string format, params object?[] args) + { + WriteSegment(format.AsSpan(), args, 0); + } + + /// Writes formatted string into temporary buffer and returns it (matches C++ write_temp). + public string WriteTemp(string format, params object?[] args) + { + bool restoreIndent = _enableIndent; + _enableIndent = false; + int sizeBefore = _first.Count; + + WriteSegment(format.AsSpan(), args, 0); + + string result = new string(CollectionsMarshalSpan(_first, sizeBefore, _first.Count - sizeBefore)); + _first.RemoveRange(sizeBefore, _first.Count - sizeBefore); + _enableIndent = restoreIndent; + return result; + } + + private static char[] CollectionsMarshalSpan(List list, int start, int length) + { + char[] arr = new char[length]; + for (int i = 0; i < length; i++) + { + arr[i] = list[start + i]; + } + return arr; + } + + /// Internal recursive segment writer. + private void WriteSegment(ReadOnlySpan value, object?[] args, int argIndex) + { + while (!value.IsEmpty) + { + int offset = -1; + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + if (c == '^' || c == '%' || c == '@') + { + offset = i; + break; + } + } + + if (offset < 0) + { + Write(value); + return; + } + + // Write everything up to the placeholder + if (offset > 0) + { + Write(value.Slice(0, offset)); + } + + char placeholder = value[offset]; + if (placeholder == '^') + { + Debug.Assert(offset + 1 < value.Length, "Escape ^ must be followed by another character"); + Write(value[offset + 1]); + value = value.Slice(offset + 2); + } + else if (placeholder == '%') + { + Debug.Assert(argIndex < args.Length, "Format string references more args than provided"); + WriteValue(args[argIndex++]); + value = value.Slice(offset + 1); + } + else // '@' + { + Debug.Assert(argIndex < args.Length, "Format string references more args than provided"); + WriteCode(args[argIndex++]); + value = value.Slice(offset + 1); + } + } + } + + /// Writes a single character with indentation handling. + private void WriteChar(char c) + { + // Normalize line endings: skip CR characters (we use LF only). + if (c == '\r') { return; } + + if (_enableIndent) + { + UpdateState(c); + if (_first.Count > 0 && _first[^1] == '\n' && c != '\n') + { + WriteIndent(); + } + } + _first.Add(c); + } + + private void WriteIndent() + { + for (int i = 0; i < _indent; i++) + { + _first.Add(' '); + } + } + + private void UpdateState(char c) + { + if (_state == WriterState.OpenParenNewline && c != ' ' && c != '\t') + { + _scopes[^1] = TabWidth; + _indent += TabWidth; + } + + switch (c) + { + case '{': + _state = WriterState.OpenParen; + _scopes.Add(0); + break; + case '}': + _state = WriterState.None; + _indent -= _scopes[^1]; + _scopes.RemoveAt(_scopes.Count - 1); + break; + case '\n': + _state = _state == WriterState.OpenParen ? WriterState.OpenParenNewline : WriterState.None; + break; + default: + _state = WriterState.None; + break; + } + } + + /// Returns the last character written (or '\0'). + public char Back() + { + return _first.Count == 0 ? '\0' : _first[^1]; + } + + /// Swaps the primary and secondary buffers. + public void Swap() + { + // Use a temp list since we can't swap List contents directly + char[] tmpArr = new char[_first.Count]; + _first.CopyTo(tmpArr); + _first.Clear(); + _first.AddRange(_second); + _second.Clear(); + _second.AddRange(tmpArr); + } + + /// Flushes both buffers to a string and clears them. + public string FlushToString() + { + StringBuilder sb = new(_first.Count + _second.Count); + for (int i = 0; i < _first.Count; i++) { sb.Append(_first[i]); } + for (int i = 0; i < _second.Count; i++) { sb.Append(_second[i]); } + _first.Clear(); + _second.Clear(); + return sb.ToString(); + } + + /// Flushes both buffers to a file and clears them; only writes if file content differs. + public void FlushToFile(string path) + { + // Build the full content + char[] arr = new char[_first.Count + _second.Count]; + _first.CopyTo(arr); + _second.CopyTo(arr, _first.Count); + string content = new string(arr); + + if (FileEqual(path, content)) + { + _first.Clear(); + _second.Clear(); + return; + } + + File.WriteAllText(path, content); + _first.Clear(); + _second.Clear(); + } + + private static bool FileEqual(string path, string content) + { + if (!File.Exists(path)) return false; + try + { + string existing = File.ReadAllText(path); + return existing == content; + } + catch + { + return false; + } + } + + /// Flushes to console out (matches C++ flush_to_console). + public void FlushToConsole() + { + for (int i = 0; i < _first.Count; i++) { Console.Write(_first[i]); } + for (int i = 0; i < _second.Count; i++) { Console.Write(_second[i]); } + _first.Clear(); + _second.Clear(); + } + + /// Flushes to console error (matches C++ flush_to_console_error). + public void FlushToConsoleError() + { + for (int i = 0; i < _first.Count; i++) { Console.Error.Write(_first[i]); } + for (int i = 0; i < _second.Count; i++) { Console.Error.Write(_second[i]); } + _first.Clear(); + _second.Clear(); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/TypeWriter.cs b/src/WinRT.Projection.Generator.Writer/Writers/TypeWriter.cs new file mode 100644 index 0000000000..71af645454 --- /dev/null +++ b/src/WinRT.Projection.Generator.Writer/Writers/TypeWriter.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace WindowsRuntime.ProjectionGenerator.Writer; + +/// +/// Mirrors the C++ writer class in type_writers.h. +/// Adds namespace context, generic parameter stack, and projection-specific begin/end helpers +/// on top of . +/// +internal sealed class TypeWriter : TextWriter +{ + public string CurrentNamespace { get; } + public Settings Settings { get; } + public bool InAbiNamespace { get; private set; } + public bool InAbiImplNamespace { get; private set; } + + /// + /// Mirrors C++ writer::_check_platform/_platform: when active inside a class scope, + /// platform-attribute computation suppresses platforms <= the previously seen platform. + /// + public bool CheckPlatform { get; set; } + public string Platform { get; set; } = string.Empty; + + /// Stack of generic argument lists currently in scope. + public List GenericArgsStack { get; } = new(); + + public TypeWriter(Settings settings, string currentNamespace) + { + Settings = settings; + CurrentNamespace = currentNamespace; + } + + public void WriteFileHeader() + { + Write("//------------------------------------------------------------------------------\n"); + Write("// \n"); + Write("// This file was generated by cswinrt.exe version "); + Write(CodeWriters.GetVersionString()); + Write("\n"); + Write("//\n"); + Write("// Changes to this file may cause incorrect behavior and will be lost if\n"); + Write("// the code is regenerated.\n"); + Write("// \n"); + Write("//------------------------------------------------------------------------------\n"); + Write( +@" +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Windows.Foundation; +using WindowsRuntime; +using WindowsRuntime.InteropServices; +using WindowsRuntime.InteropServices.Marshalling; +using static System.Runtime.InteropServices.ComWrappers; + +#pragma warning disable CS0169 // ""The field '...' is never used"" +#pragma warning disable CS0649 // ""Field '...' is never assigned to"" +#pragma warning disable CA2207, CA1063, CA1033, CA1001, CA2213 +#pragma warning disable CSWINRT3001 // ""Type or member '...' is a private implementation detail"" +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + +"); + } + + public void WriteBeginProjectedNamespace() + { + InAbiImplNamespace = Settings.Component; + string nsPrefix = Settings.Component ? "ABI.Impl." : string.Empty; + Write("\nnamespace "); + Write(nsPrefix); + Write(CurrentNamespace); + Write("\n{\n"); + } + + public void WriteEndProjectedNamespace() + { + Write("}\n"); + InAbiImplNamespace = false; + } + + public void WriteBeginAbiNamespace() + { + if (!Settings.NetstandardCompat) + { + Write("\n#pragma warning disable CA1416"); + } + Write("\nnamespace ABI."); + Write(CurrentNamespace); + Write("\n{\n"); + InAbiNamespace = true; + } + + public void WriteEndAbiNamespace() + { + Write("}\n"); + if (!Settings.NetstandardCompat) + { + Write("#pragma warning restore CA1416\n"); + } + InAbiNamespace = false; + } +} diff --git a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs index e239a76b0d..c3070887b0 100644 --- a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs +++ b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs @@ -69,27 +69,27 @@ public static Exception CreateCompilationError(Exception exception) } /// - /// The cswinrt.exe process failed to start. + /// The projection writer failed to start. /// public static Exception CsWinRTProcessStartError() { - return Exception(7, "Failed to start the 'cswinrt.exe' process."); + return Exception(7, "Failed to invoke the projection writer."); } /// - /// The cswinrt.exe process failed to start. + /// The projection writer failed to start. /// public static Exception CsWinRTProcessStartError(Exception exception) { - return Exception(7, "Failed to start the 'cswinrt.exe' process.", exception); + return Exception(7, "Failed to invoke the projection writer.", exception); } /// - /// The cswinrt.exe process exited with a non-zero exit code. + /// The projection writer failed during source generation. /// public static Exception CsWinRTProcessError(int exitCode, Exception exception) { - return Exception(8, $"The 'cswinrt.exe' process exited with code {exitCode}.", exception); + return Exception(8, $"The projection writer failed during source generation (exit code {exitCode}).", exception); } /// diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs index ca9c572266..f794ad5781 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs @@ -3,13 +3,12 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.IO; using System.Linq; using AsmResolver; using AsmResolver.DotNet; using WindowsRuntime.ProjectionGenerator.Errors; +using WindowsRuntime.ProjectionGenerator.Writer; #pragma warning disable IDE0270 @@ -45,78 +44,56 @@ private static ProjectionGeneratorProcessingState ProcessReferences(ProjectionGe { args.Token.ThrowIfCancellationRequested(); - GenerateRspFile(args, out string outputFolder, out string rspFile, out HashSet projectionReferenceAssemblies, out bool hasTypesToProject); + BuildWriterOptions( + args, + out string outputFolder, + out string rspFile, + out HashSet projectionReferenceAssemblies, + out bool hasTypesToProject, + out ProjectionWriterOptions writerOptions); string[] referencesWithoutProjections = [.. args.ReferenceAssemblyPaths.Where(r => !projectionReferenceAssemblies.Contains(r))]; - return new ProjectionGeneratorProcessingState(outputFolder, rspFile, referencesWithoutProjections, hasTypesToProject); + return new ProjectionGeneratorProcessingState(outputFolder, rspFile, referencesWithoutProjections, writerOptions, hasTypesToProject); } /// /// Runs the source generation logic for the generator. /// - /// The arguments for this invocation. /// The state from the processing phase. - private static void GenerateSources(ProjectionGeneratorArgs args, ProjectionGeneratorProcessingState processingState) + private static void GenerateSources(ProjectionGeneratorProcessingState processingState) { - ProcessStartInfo processInfo = new() - { - FileName = args.CsWinRTExePath, - Arguments = "@" + processingState.RspFilePath, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true - }; - - Process? cswinrtProcess; - + // Invoke the projection writer in-process. Previously this spawned cswinrt.exe; now we + // call the public C# API directly to avoid the process boundary and to allow the writer + // to be replaced/extended without needing to re-publish a separate executable. try { - cswinrtProcess = Process.Start(processInfo); - - // Make sure we did successfully start the process ('Start' can return 'null') - if (cswinrtProcess is null) - { - throw WellKnownProjectionGeneratorExceptions.CsWinRTProcessStartError(); - } + ProjectionWriter.Run(processingState.WriterOptions); } catch (Exception e) when (!e.IsWellKnown) { - throw WellKnownProjectionGeneratorExceptions.CsWinRTProcessStartError(e); - } - - // Validate that generation was successful - using (cswinrtProcess) - { - string error = cswinrtProcess.StandardError.ReadToEnd(); - - cswinrtProcess.WaitForExit(); - - if (cswinrtProcess.ExitCode != 0) - { - throw WellKnownProjectionGeneratorExceptions.CsWinRTProcessError( - exitCode: cswinrtProcess.ExitCode, - exception: new Win32Exception(cswinrtProcess.ExitCode, error)); - } + throw WellKnownProjectionGeneratorExceptions.CsWinRTProcessError(exitCode: -1, exception: e); } } /// - /// Generates a response file for CsWinRT based on the provided arguments and reference assemblies. + /// Builds the from the supplied arguments and reference assemblies, + /// also writing out a debug-only response file with the same options encoded in the historical + /// cswinrt.exe CLI format. /// /// The arguments for this invocation. /// The folder where sources will be generated. - /// The generated response file for running cswinrt.exe. - /// The projection reference assemblies which were used to generate the response file. + /// The path to the response file (kept as a debug artifact). + /// The projection reference assemblies which were used. /// Whether any types were found to include in the projection. - private static void GenerateRspFile( + /// The resulting writer options. + private static void BuildWriterOptions( ProjectionGeneratorArgs args, out string outputFolder, out string rspFile, out HashSet projectionReferenceAssemblies, - out bool hasTypesToProject) + out bool hasTypesToProject, + out ProjectionWriterOptions writerOptions) { args.Token.ThrowIfCancellationRequested(); @@ -125,6 +102,10 @@ private static void GenerateRspFile( projectionReferenceAssemblies = []; hasTypesToProject = false; + List includes = []; + List excludes = []; + List winmdInputs = []; + using StreamWriter fileStream = new(rspFile); // Filter out .winmd files from the resolver paths @@ -181,6 +162,7 @@ private static void GenerateRspFile( } fileStream.WriteLine($"-include {type.FullName}"); + includes.Add(type.FullName); hasTypesToProject = true; } } @@ -226,8 +208,8 @@ private static void GenerateRspFile( { if (isWindowsSdk) { - // Write the filtes for the Windows SDK projection mode. - WriteWindowsSdkFilters(fileStream, args.WindowsUIXamlProjection); + // Write the filters for the Windows SDK projection mode. + WriteWindowsSdkFilters(fileStream, includes, excludes, args.WindowsUIXamlProjection); hasTypesToProject = true; @@ -238,12 +220,14 @@ private static void GenerateRspFile( // In addition to projecting the individual types, make sure // the additions get included by including the namespace. fileStream.WriteLine($"-include Microsoft.UI"); + includes.Add("Microsoft.UI"); } } foreach (TypeDefinition exportedType in moduleDefinition.TopLevelTypes) { fileStream.WriteLine($"-include {exportedType.FullName}"); + includes.Add(exportedType.FullName); hasTypesToProject = true; } } @@ -253,24 +237,31 @@ private static void GenerateRspFile( // (e.g., pipeline builds that pass WinMDs directly), hardcode the includes. if (isWindowsSdkMode && projectionReferenceAssemblies.Count == 0) { - WriteWindowsSdkFilters(fileStream, args.WindowsUIXamlProjection); + WriteWindowsSdkFilters(fileStream, includes, excludes, args.WindowsUIXamlProjection); } // If we're not in Windows SDK mode, we exclude the Windows namespace to avoid // the merged projection from generating all namespaces when there are no projection references - // and thereby no includes / excludes passed to cswinrt. + // and thereby no includes / excludes passed to the writer. if (!isWindowsSdkMode) { fileStream.WriteLine("-exclude Windows"); + excludes.Add("Windows"); } fileStream.WriteLine($"-target {args.TargetFramework}"); fileStream.WriteLine($"-input {args.WindowsMetadata}"); fileStream.WriteLine($"-output \"{outputFolder}\""); - // When generating 'WinRT.Component.dll', pass -component to 'cswinrt.exe' to enable - // component-specific code generation (activation factories, exclusive-to interfaces, etc.) - if (args.AssemblyName == "WinRT.Component") + // Expand the windows metadata token (path | "local" | "sdk[+]" | version[+]) into actual + // .winmd file paths (or directories the writer will recursively scan). The C++ cswinrt.exe + // tool did this in cmd_reader.h via reader.files() — see WindowsMetadataExpander. + winmdInputs.AddRange(WindowsMetadataExpander.Expand(args.WindowsMetadata)); + + // When generating 'WinRT.Component.dll', enable component-specific code generation + // (activation factories, exclusive-to interfaces, etc.). + bool componentMode = args.AssemblyName == "WinRT.Component"; + if (componentMode) { fileStream.WriteLine("-component"); } @@ -278,52 +269,79 @@ private static void GenerateRspFile( foreach (string winmdPath in args.WinMDPaths) { fileStream.WriteLine($"-input \"{winmdPath}\""); + winmdInputs.Add(winmdPath); } + + writerOptions = new ProjectionWriterOptions + { + InputPaths = winmdInputs, + OutputFolder = outputFolder, + Include = includes, + Exclude = excludes, + Component = componentMode, + CancellationToken = args.Token, + }; } /// - /// Writes the cswinrt.exe include/exclude filter directives for the Windows SDK projection. + /// Writes the include/exclude filter directives for the Windows SDK projection. + /// Emits to both the .rsp file (for debugging) and the in-memory include/exclude lists + /// passed to . /// /// The RSP file writer. + /// The list of namespace prefixes to include. + /// The list of namespace prefixes to exclude. /// /// When true, writes the Windows.UI.Xaml filter set. /// When false, writes the base Windows SDK filter set. /// - private static void WriteWindowsSdkFilters(StreamWriter writer, bool xamlProjection) + private static void WriteWindowsSdkFilters(StreamWriter writer, List includes, List excludes, bool xamlProjection) { + void Include(string ns) + { + writer.WriteLine($"-include {ns}"); + includes.Add(ns); + } + + void Exclude(string ns) + { + writer.WriteLine($"-exclude {ns}"); + excludes.Add(ns); + } + if (xamlProjection) { - writer.WriteLine("-exclude Windows"); - writer.WriteLine("-include Windows.UI.Colors"); - writer.WriteLine("-include Windows.UI.ColorHelper"); - writer.WriteLine("-include Windows.UI.IColorHelper"); - writer.WriteLine("-include Windows.UI.IColors"); - writer.WriteLine("-include Windows.UI.Text.FontWeights"); - writer.WriteLine("-include Windows.UI.Text.IFontWeights"); - writer.WriteLine("-include Windows.UI.Xaml"); - writer.WriteLine("-include Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper"); - writer.WriteLine("-include Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper"); - writer.WriteLine("-exclude Windows.UI.Xaml.Interop"); - writer.WriteLine("-exclude Windows.UI.Xaml.Data.BindableAttribute"); - writer.WriteLine("-exclude Windows.UI.Xaml.Markup.ContentPropertyAttribute"); + Exclude("Windows"); + Include("Windows.UI.Colors"); + Include("Windows.UI.ColorHelper"); + Include("Windows.UI.IColorHelper"); + Include("Windows.UI.IColors"); + Include("Windows.UI.Text.FontWeights"); + Include("Windows.UI.Text.IFontWeights"); + Include("Windows.UI.Xaml"); + Include("Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper"); + Include("Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper"); + Exclude("Windows.UI.Xaml.Interop"); + Exclude("Windows.UI.Xaml.Data.BindableAttribute"); + Exclude("Windows.UI.Xaml.Markup.ContentPropertyAttribute"); } else { - writer.WriteLine("-include Windows"); - writer.WriteLine("-include WindowsRuntime.Internal"); - - writer.WriteLine("-exclude Windows.UI.Colors"); - writer.WriteLine("-exclude Windows.UI.ColorHelper"); - writer.WriteLine("-exclude Windows.UI.IColorHelper"); - writer.WriteLine("-exclude Windows.UI.IColors"); - writer.WriteLine("-exclude Windows.UI.Text.FontWeights"); - writer.WriteLine("-exclude Windows.UI.Text.IFontWeights"); - writer.WriteLine("-exclude Windows.UI.Xaml"); - writer.WriteLine("-exclude Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper"); - writer.WriteLine("-exclude Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper"); - writer.WriteLine("-include Windows.UI.Xaml.Interop"); - writer.WriteLine("-include Windows.UI.Xaml.Data.BindableAttribute"); - writer.WriteLine("-include Windows.UI.Xaml.Markup.ContentPropertyAttribute"); + Include("Windows"); + Include("WindowsRuntime.Internal"); + + Exclude("Windows.UI.Colors"); + Exclude("Windows.UI.ColorHelper"); + Exclude("Windows.UI.IColorHelper"); + Exclude("Windows.UI.IColors"); + Exclude("Windows.UI.Text.FontWeights"); + Exclude("Windows.UI.Text.IFontWeights"); + Exclude("Windows.UI.Xaml"); + Exclude("Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper"); + Exclude("Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper"); + Include("Windows.UI.Xaml.Interop"); + Include("Windows.UI.Xaml.Data.BindableAttribute"); + Include("Windows.UI.Xaml.Markup.ContentPropertyAttribute"); } } @@ -370,4 +388,4 @@ private static bool IsComponentAssembly(ModuleDefinition moduleDefinition) { return moduleDefinition.Assembly is not null && moduleDefinition.Assembly.HasCustomAttribute("WindowsRuntime.InteropServices"u8, "WindowsRuntimeComponentAssemblyAttribute"u8); } -} \ No newline at end of file +} diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs index 2119605ee2..f1a9336f0b 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs @@ -66,12 +66,12 @@ public static void Run([Argument] string responseFilePath, CancellationToken tok return; } - // Invoke 'cswinrt.exe' to generate the projection sources + // Invoke the projection writer (in-process) to generate the projection sources try { ConsoleApp.Log("Generating projection code"); - GenerateSources(args, processingState); + GenerateSources(processingState); } catch (Exception e) when (!e.IsWellKnown) { diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs index 6d570ac04f..f47a0d13bf 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs @@ -7,10 +7,16 @@ namespace WindowsRuntime.ProjectionGenerator.Generation; /// State produced by the processing phase of . /// /// The path to the folder where sources will be generated. -/// The path to the generated response file for CsWinRT. +/// The path to the response file (kept as a debug artifact). /// The reference assembly paths excluding projection assemblies. +/// The options to pass to . /// Whether any types were found to project. -internal sealed class ProjectionGeneratorProcessingState(string sourcesFolder, string rspFilePath, string[] referencesWithoutProjections, bool hasTypesToProject = true) +internal sealed class ProjectionGeneratorProcessingState( + string sourcesFolder, + string rspFilePath, + string[] referencesWithoutProjections, + Writer.ProjectionWriterOptions writerOptions, + bool hasTypesToProject = true) { /// /// Gets the path to the folder where sources will be generated. @@ -18,7 +24,7 @@ internal sealed class ProjectionGeneratorProcessingState(string sourcesFolder, s public string SourcesFolder { get; } = sourcesFolder; /// - /// Gets the path to the generated response file for CsWinRT. + /// Gets the path to the generated response file (kept as a debug artifact for inspection). /// public string RspFilePath { get; } = rspFilePath; @@ -27,9 +33,15 @@ internal sealed class ProjectionGeneratorProcessingState(string sourcesFolder, s /// public string[] ReferencesWithoutProjections { get; } = referencesWithoutProjections; + /// + /// Gets the options used to invoke . + /// + public Writer.ProjectionWriterOptions WriterOptions { get; } = writerOptions; + /// /// Gets whether any types were found to project. When false, the source generation /// and emit phases should be skipped (no DLL will be produced). /// public bool HasTypesToProject { get; } = hasTypesToProject; } + diff --git a/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj b/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj index fe77504d9a..ee1d463a13 100644 --- a/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj +++ b/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj @@ -2,7 +2,7 @@ Exe net10.0 - preview + 14.0 enable true true @@ -54,7 +54,10 @@ - + + + + diff --git a/src/WinRT.Projection.Ref.Generator/Attributes/CommandLineArgumentNameAttribute.cs b/src/WinRT.Projection.Ref.Generator/Attributes/CommandLineArgumentNameAttribute.cs new file mode 100644 index 0000000000..d6f296dfcc --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Attributes/CommandLineArgumentNameAttribute.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.ReferenceProjectionGenerator.Attributes; + +/// +/// An attribute indicating the name of a given command line argument. +/// +/// The command line argument name. +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +internal sealed class CommandLineArgumentNameAttribute(string name) : Attribute +{ + /// + /// Gets the command line argument name. + /// + public string Name { get; } = name; +} diff --git a/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs b/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs new file mode 100644 index 0000000000..731e2a129b --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.ReferenceProjectionGenerator.Errors; + +/// +/// An unhandled exception for the reference projection generator. +/// +internal sealed class UnhandledReferenceProjectionGeneratorException : Exception +{ + /// + /// The phase that failed. + /// + private readonly string _phase; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The phase that failed. + /// The inner exception. + public UnhandledReferenceProjectionGeneratorException(string phase, Exception exception) + : base(null, exception) + { + _phase = phase; + } + + /// + public override string ToString() + { + return + $"""error {WellKnownReferenceProjectionGeneratorExceptions.ErrorPrefix}9999: The CsWinRT reference projection generator failed with an unhandled exception """ + + $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the {_phase} phase. This might be due to an invalid """ + + $"""configuration in the current project, but the generator should still correctly identify that and fail gracefully. Please open an """ + + $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; + } +} diff --git a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorException.cs b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorException.cs new file mode 100644 index 0000000000..72f93c8799 --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorException.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.ReferenceProjectionGenerator.Errors; + +/// +/// A well known exception for the reference projection generator. +/// +internal sealed class WellKnownReferenceProjectionGeneratorException : Exception +{ + /// + /// Creates a new instance with the specified parameters. + /// + /// The id of the exception. + /// The exception message. + /// The inner exception. + public WellKnownReferenceProjectionGeneratorException(string id, string message, Exception? innerException) + : base(message, innerException) + { + Id = id; + } + + /// + /// Gets the id of the exception. + /// + public string Id { get; } + + /// + public override string ToString() + { + return InnerException is not null + ? $"""error {Id}: {Message} Inner exception: '{InnerException.GetType().Name}': '{InnerException.Message}'.""" + : $"""error {Id}: {Message}"""; + } +} diff --git a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs new file mode 100644 index 0000000000..955f35c1bd --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.ReferenceProjectionGenerator.Errors; + +/// +/// Well known exceptions for the reference projection generator. +/// +internal static class WellKnownReferenceProjectionGeneratorExceptions +{ + /// + /// The prefix for all errors produced by this tool. + /// + public const string ErrorPrefix = "CSWINRTPROJECTIONREFGEN"; + + /// + /// Some exception was thrown when trying to read the response file. + /// + public static Exception ResponseFileReadError(Exception exception) + { + return Exception(1, "Failed to read the response file to run 'cswinrtprojectionrefgen'.", exception); + } + + /// + /// Failed to parse an argument from the response file. + /// + public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) + { + return Exception(2, $"Failed to parse argument '{argumentName}' from response file.", exception); + } + + /// + /// The input response file is malformed. + /// + public static Exception MalformedResponseFile() + { + return Exception(3, "The response file is malformed and contains invalid content."); + } + + /// + /// The supplied target framework is not supported by CsWinRT 3.0. + /// + public static Exception UnsupportedTargetFramework(string targetFramework) + { + return Exception(4, $"The target framework '{targetFramework}' is not supported. CsWinRT 3.0 requires .NET 10 or later."); + } + + /// + /// The projection writer failed during source generation. + /// + public static Exception CsWinRTProcessError(Exception exception) + { + return Exception(5, "The projection writer failed during source generation.", exception); + } + + /// + /// Creates a new exception with the specified id and message. + /// + /// The exception id. + /// The exception message. + /// The inner exception. + /// The resulting exception. + private static Exception Exception(int id, string message, Exception? innerException = null) + { + return new WellKnownReferenceProjectionGeneratorException($"{ErrorPrefix}{id:0000}", message, innerException); + } +} diff --git a/src/WinRT.Projection.Ref.Generator/Extensions/ReferenceProjectionGeneratorExceptionExtensions.cs b/src/WinRT.Projection.Ref.Generator/Extensions/ReferenceProjectionGeneratorExceptionExtensions.cs new file mode 100644 index 0000000000..a0a981c6c3 --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Extensions/ReferenceProjectionGeneratorExceptionExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.ReferenceProjectionGenerator.Errors; + +namespace WindowsRuntime.ReferenceProjectionGenerator; + +/// +/// Extensions for reference projection generator exceptions. +/// +internal static class ReferenceProjectionGeneratorExceptionExtensions +{ + extension(Exception exception) + { + /// + /// Gets a value indicating whether an exception is well known (and should therefore not be caught). + /// + public bool IsWellKnown => exception is OperationCanceledException or WellKnownReferenceProjectionGeneratorException; + } +} diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs new file mode 100644 index 0000000000..5fc71a4eeb --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using ConsoleAppFramework; +using WindowsRuntime.ProjectionGenerator.Writer; +using WindowsRuntime.ReferenceProjectionGenerator.Errors; + +namespace WindowsRuntime.ReferenceProjectionGenerator.Generation; + +/// +/// The implementation of the CsWinRT reference projection source generator. This is the C# replacement +/// for the C++ cswinrt.exe invocation in the CsWinRTGenerateProjection MSBuild target. +/// It produces .cs files that get compiled into the user's library/component .dll. +/// +internal static class ReferenceProjectionGenerator +{ + /// + /// Runs the reference projection source generator. + /// + /// The path to the response file to use. + /// The token for the operation. + public static void Run([Argument] string responseFilePath, CancellationToken token) + { + ReferenceProjectionGeneratorArgs args; + + // Parse the actual arguments from the response file + try + { + args = ReferenceProjectionGeneratorArgs.ParseFromResponseFile(responseFilePath, token); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledReferenceProjectionGeneratorException("parsing", e); + } + + args.Token.ThrowIfCancellationRequested(); + + // Validate the target framework. CsWinRT 3.0 requires .NET 10 or later. + if (!string.IsNullOrEmpty(args.TargetFramework) && !args.TargetFramework.StartsWith("net10.0", StringComparison.Ordinal)) + { + throw WellKnownReferenceProjectionGeneratorExceptions.UnsupportedTargetFramework(args.TargetFramework); + } + + // Build the writer options from the parsed arguments + ProjectionWriterOptions options; + + try + { + options = BuildWriterOptions(args); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledReferenceProjectionGeneratorException("processing", e); + } + + args.Token.ThrowIfCancellationRequested(); + + // Invoke the projection writer (in-process) to generate the projection sources + try + { + ConsoleApp.Log($"Generating reference projection sources -> {options.OutputFolder}"); + + ProjectionWriter.Run(options); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw WellKnownReferenceProjectionGeneratorExceptions.CsWinRTProcessError(e); + } + } + + /// + /// Builds the from the parsed args. + /// + /// The parsed args. + /// The resulting . + private static ProjectionWriterOptions BuildWriterOptions(ReferenceProjectionGeneratorArgs args) + { + // Each input may be a literal file or directory path, or a special token like 'local', + // 'sdk', 'sdk+', or '10.0.X.Y' which expands to a set of WinMD paths. Expand each input + // through WindowsMetadataExpander so the writer always receives concrete paths. + List inputPaths = []; + + foreach (string input in args.InputPaths) + { + inputPaths.AddRange(WindowsMetadataExpander.Expand(input)); + } + + // Make sure the output directory exists. ProjectionWriter.Run will also create it but creating + // it here matches the OLD target's '' step. + _ = Directory.CreateDirectory(args.OutputDirectory); + + return new ProjectionWriterOptions + { + InputPaths = inputPaths, + OutputFolder = args.OutputDirectory, + Include = args.IncludeNamespaces, + Exclude = args.ExcludeNamespaces, + AdditionExclude = args.AdditionExcludeNamespaces, + Verbose = args.Verbose, + Component = args.Component, + Internal = args.Internal, + Embedded = args.Embedded, + PublicEnums = args.PublicEnums, + PublicExclusiveTo = args.PublicExclusiveTo, + IdicExclusiveTo = args.IdicExclusiveTo, + ReferenceProjection = args.ReferenceProjection, + CancellationToken = args.Token, + }; + } +} diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Parsing.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Parsing.cs new file mode 100644 index 0000000000..3b9ef440b6 --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Parsing.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading; +using WindowsRuntime.ReferenceProjectionGenerator.Attributes; +using WindowsRuntime.ReferenceProjectionGenerator.Errors; + +#pragma warning disable IDE0046 + +namespace WindowsRuntime.ReferenceProjectionGenerator.Generation; + +/// +internal partial class ReferenceProjectionGeneratorArgs +{ + /// + /// Parses an instance from a target response file. + /// + /// The path to the response file. + /// The token for the operation. + /// The resulting instance. + public static ReferenceProjectionGeneratorArgs ParseFromResponseFile(string path, CancellationToken token) + { + // If the path is a response file, it will start with the '@' character. + // This matches the default escaping 'ToolTask' uses for response files. + if (path is ['@', .. string escapedPath]) + { + path = escapedPath; + } + + string[] responseArgs; + + // Read all lines in the response file (each line contains a single command line argument) + try + { + responseArgs = File.ReadAllLines(path); + } + catch (Exception e) + { + throw WellKnownReferenceProjectionGeneratorExceptions.ResponseFileReadError(e); + } + + Dictionary argsMap = []; + + // Build a map with all the commands and their values + foreach (string line in responseArgs) + { + string trimmedLine = line.Trim(); + + // Skip empty lines (the MSBuild ToolTask may emit blank lines). + if (trimmedLine.Length == 0) + { + continue; + } + + // Each line has the command line argument name followed by a space, and then the + // argument value. If there are no spaces on any given line, the file is malformed. + int indexOfSpace = trimmedLine.IndexOf(' '); + + if (indexOfSpace == -1) + { + throw WellKnownReferenceProjectionGeneratorExceptions.MalformedResponseFile(); + } + + // Now we can parse the actual command line argument name and value + string argumentName = trimmedLine.AsSpan()[..indexOfSpace].ToString(); + string argumentValue = trimmedLine.AsSpan()[(indexOfSpace + 1)..].TrimEnd().ToString(); + + // We should never have duplicate commands + if (!argsMap.TryAdd(argumentName, argumentValue)) + { + throw WellKnownReferenceProjectionGeneratorExceptions.MalformedResponseFile(); + } + } + + // Parse all commands to create the managed arguments to use + return new() + { + InputPaths = GetStringArrayArgument(argsMap, nameof(InputPaths)), + OutputDirectory = GetStringArgument(argsMap, nameof(OutputDirectory)), + TargetFramework = GetStringArgument(argsMap, nameof(TargetFramework)), + IncludeNamespaces = GetOptionalStringArrayArgument(argsMap, nameof(IncludeNamespaces)), + ExcludeNamespaces = GetOptionalStringArrayArgument(argsMap, nameof(ExcludeNamespaces)), + AdditionExcludeNamespaces = GetOptionalStringArrayArgument(argsMap, nameof(AdditionExcludeNamespaces)), + Verbose = GetOptionalBoolArgument(argsMap, nameof(Verbose)), + Component = GetOptionalBoolArgument(argsMap, nameof(Component)), + Internal = GetOptionalBoolArgument(argsMap, nameof(Internal)), + Embedded = GetOptionalBoolArgument(argsMap, nameof(Embedded)), + PublicEnums = GetOptionalBoolArgument(argsMap, nameof(PublicEnums)), + PublicExclusiveTo = GetOptionalBoolArgument(argsMap, nameof(PublicExclusiveTo)), + IdicExclusiveTo = GetOptionalBoolArgument(argsMap, nameof(IdicExclusiveTo)), + ReferenceProjection = GetOptionalBoolArgument(argsMap, nameof(ReferenceProjection)), + Token = token + }; + } + + /// + /// Gets the command line argument name for a property. + /// + /// The target property name. + /// The command line argument name for . + public static string GetCommandLineArgumentName(string propertyName) + { + try + { + return typeof(ReferenceProjectionGeneratorArgs).GetProperty(propertyName)!.GetCustomAttribute()!.Name; + } + catch (Exception e) + { + throw WellKnownReferenceProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName, e); + } + } + + /// + /// Parses a required array argument. + /// + /// The input map with raw arguments. + /// The target property name. + /// The resulting argument. + private static string[] GetStringArrayArgument(Dictionary argsMap, string propertyName) + { + if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) + { + return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + throw WellKnownReferenceProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName); + } + + /// + /// Parses an optional array argument, returning an empty array if not present. + /// + /// The input map with raw arguments. + /// The target property name. + /// The resulting argument, or an empty array if not found. + private static string[] GetOptionalStringArrayArgument(Dictionary argsMap, string propertyName) + { + if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) + { + return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + return []; + } + + /// + /// Parses a required argument. + /// + /// The input map with raw arguments. + /// The target property name. + /// The resulting argument. + private static string GetStringArgument(Dictionary argsMap, string propertyName) + { + if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) + { + return argumentValue; + } + + throw WellKnownReferenceProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName); + } + + /// + /// Parses an optional argument, returning false if not present. + /// + /// The input map with raw arguments. + /// The target property name. + /// The resulting argument, or false if not found. + private static bool GetOptionalBoolArgument(Dictionary argsMap, string propertyName) + { + if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) + { + return bool.TryParse(argumentValue, out bool result) && result; + } + + return false; + } +} diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.cs new file mode 100644 index 0000000000..c5667eccff --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using WindowsRuntime.ReferenceProjectionGenerator.Attributes; + +namespace WindowsRuntime.ReferenceProjectionGenerator.Generation; + +/// +/// Input parameters for . Mirrors the C++ cswinrt.exe CLI +/// arguments that the CsWinRTGenerateProjection MSBuild target sends today (every flag the OLD +/// target plumbs is supported here, including ones that are dead in CsWinRT 3.0 but kept for parity). +/// +internal sealed partial class ReferenceProjectionGeneratorArgs +{ + /// Gets the input .winmd paths (files, directories to recursively scan, or special + /// tokens like "local", "sdk", "sdk+", or a version like "10.0.26100.0"). + [CommandLineArgumentName("--input-paths")] + public required string[] InputPaths { get; init; } + + /// Gets the directory where the generated .cs files will be placed. + [CommandLineArgumentName("--output-directory")] + public required string OutputDirectory { get; init; } + + /// Gets the target framework being built for (must start with net10.0). + [CommandLineArgumentName("--target-framework")] + public required string TargetFramework { get; init; } + + /// Gets the namespace prefixes to include in the projection. + [CommandLineArgumentName("--include-namespaces")] + public string[] IncludeNamespaces { get; init; } = []; + + /// Gets the namespace prefixes to exclude from the projection. + [CommandLineArgumentName("--exclude-namespaces")] + public string[] ExcludeNamespaces { get; init; } = []; + + /// Gets the namespace prefixes to exclude from the projection additions. + [CommandLineArgumentName("--addition-exclude-namespaces")] + public string[] AdditionExcludeNamespaces { get; init; } = []; + + /// Gets whether verbose progress logging should be enabled. + [CommandLineArgumentName("--verbose")] + public bool Verbose { get; init; } + + /// Gets whether to generate a Windows Runtime component projection. + [CommandLineArgumentName("--component")] + public bool Component { get; init; } + + /// + /// Gets whether to generate a private (internal) projection. + /// CsWinRT 3.0 leftover; preserved for OLD-target parity. + /// + [CommandLineArgumentName("--internal")] + public bool Internal { get; init; } + + /// + /// Gets whether to generate an embedded projection. + /// CsWinRT 3.0 leftover; preserved for OLD-target parity. + /// + [CommandLineArgumentName("--embedded")] + public bool Embedded { get; init; } + + /// + /// Gets whether to emit enums as public when used with the embedded option. + /// CsWinRT 3.0 leftover; preserved for OLD-target parity. + /// + [CommandLineArgumentName("--public-enums")] + public bool PublicEnums { get; init; } + + /// Gets whether to make exclusive-to interfaces public in the projection. + [CommandLineArgumentName("--public-exclusive-to")] + public bool PublicExclusiveTo { get; init; } + + /// Gets whether exclusive-to interfaces should support IDynamicInterfaceCastable. + [CommandLineArgumentName("--idic-exclusive-to")] + public bool IdicExclusiveTo { get; init; } + + /// Gets whether to generate a projection to be used as a reference assembly. + [CommandLineArgumentName("--reference-projection")] + public bool ReferenceProjection { get; init; } + + /// Gets the token for the operation. + public required CancellationToken Token { get; init; } +} diff --git a/src/WinRT.Projection.Ref.Generator/Program.cs b/src/WinRT.Projection.Ref.Generator/Program.cs new file mode 100644 index 0000000000..b1601e7a88 --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/Program.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using ConsoleAppFramework; +using WindowsRuntime.ReferenceProjectionGenerator.Generation; + +// Run the reference projection generator with all parsed arguments +ConsoleApp.Run(args, ReferenceProjectionGenerator.Run); diff --git a/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj b/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj new file mode 100644 index 0000000000..f83cff36dd --- /dev/null +++ b/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj @@ -0,0 +1,60 @@ + + + Exe + net10.0 + 14.0 + enable + true + true + true + + + C#/WinRT Reference Projection Generator v$(VersionString) + C#/WinRT Reference Projection Generator v$(VersionString) + Copyright (c) Microsoft Corporation. All rights reserved. + $(AssemblyVersionNumber) + $(VersionNumber) + + + true + + + WindowsRuntime.ReferenceProjectionGenerator + + + cswinrtprojectionrefgen + + + true + true + latest + latest-all + true + strict + true + + + true + win-$(BuildToolArch) + true + + + + + true + Speed + false + true + Guard + false + false + + + + + + + + + + diff --git a/src/build.cmd b/src/build.cmd index 35d7b9ad16..983e22e477 100644 --- a/src/build.cmd +++ b/src/build.cmd @@ -262,11 +262,12 @@ set winrt_shim=%this_dir%Authoring\WinRT.Host.Shim\bin\%cswinrt_configuration%\n set cswinrtinteropgen_%cswinrt_platform%=%this_dir%WinRT.Interop.Generator\bin\%cswinrt_configuration%\net10.0\win-%cswinrt_platform%\publish\cswinrtinteropgen.exe set cswinrtimplgen_%cswinrt_platform%=%this_dir%WinRT.Impl.Generator\bin\%cswinrt_configuration%\net10.0\win-%cswinrt_platform%\publish\cswinrtimplgen.exe set cswinrtprojectiongen_%cswinrt_platform%=%this_dir%WinRT.Projection.Generator\bin\%cswinrt_configuration%\net10.0\win-%cswinrt_platform%\publish\cswinrtprojectiongen.exe +set cswinrtprojectionrefgen_%cswinrt_platform%=%this_dir%WinRT.Projection.Ref.Generator\bin\%cswinrt_configuration%\net10.0\win-%cswinrt_platform%\publish\cswinrtprojectionrefgen.exe set run_cswinrt_generator_task=%this_dir%WinRT.Generator.Tasks\bin\%cswinrt_configuration%\netstandard2.0\WinRT.Generator.Tasks.dll rem Now call pack echo Creating nuget package -call :exec %nuget_dir%\nuget pack %this_dir%..\nuget\Microsoft.Windows.CsWinRT.nuspec -Properties cswinrt_exe=%cswinrt_exe%;interop_winmd=%interop_winmd%;net10_runtime=%net10_runtime%;net10_runtime_xml=%net10_runtime_xml%;source_generator=%source_generator%;cswinrt_nuget_version=%cswinrt_version_string%;winrt_host_x86=%winrt_host_x86%;winrt_host_x64=%winrt_host_x64%;winrt_host_arm=%winrt_host_arm%;winrt_host_arm64=%winrt_host_arm64%;winrt_host_resource_x86=%winrt_host_resource_x86%;winrt_host_resource_x64=%winrt_host_resource_x64%;winrt_host_resource_arm=%winrt_host_resource_arm%;winrt_host_resource_arm64=%winrt_host_resource_arm64%;winrt_shim=%winrt_shim%;cswinrtinteropgen_x64=%cswinrtinteropgen_x64%;cswinrtinteropgen_arm64=%cswinrtinteropgen_arm64%;cswinrtimplgen_x64=%cswinrtimplgen_x64%;cswinrtimplgen_arm64=%cswinrtimplgen_arm64%;cswinrtprojectiongen_x64=%cswinrtprojectiongen_x64%;cswinrtprojectiongen_arm64=%cswinrtprojectiongen_arm64%;run_cswinrt_generator_task=%run_cswinrt_generator_task%; -OutputDirectory %cswinrt_bin_dir% -NonInteractive -Verbosity Detailed -NoPackageAnalysis +call :exec %nuget_dir%\nuget pack %this_dir%..\nuget\Microsoft.Windows.CsWinRT.nuspec -Properties cswinrt_exe=%cswinrt_exe%;interop_winmd=%interop_winmd%;net10_runtime=%net10_runtime%;net10_runtime_xml=%net10_runtime_xml%;source_generator=%source_generator%;cswinrt_nuget_version=%cswinrt_version_string%;winrt_host_x86=%winrt_host_x86%;winrt_host_x64=%winrt_host_x64%;winrt_host_arm=%winrt_host_arm%;winrt_host_arm64=%winrt_host_arm64%;winrt_host_resource_x86=%winrt_host_resource_x86%;winrt_host_resource_x64=%winrt_host_resource_x64%;winrt_host_resource_arm=%winrt_host_resource_arm%;winrt_host_resource_arm64=%winrt_host_resource_arm64%;winrt_shim=%winrt_shim%;cswinrtinteropgen_x64=%cswinrtinteropgen_x64%;cswinrtinteropgen_arm64=%cswinrtinteropgen_arm64%;cswinrtimplgen_x64=%cswinrtimplgen_x64%;cswinrtimplgen_arm64=%cswinrtimplgen_arm64%;cswinrtprojectiongen_x64=%cswinrtprojectiongen_x64%;cswinrtprojectiongen_arm64=%cswinrtprojectiongen_arm64%;cswinrtprojectionrefgen_x64=%cswinrtprojectionrefgen_x64%;cswinrtprojectionrefgen_arm64=%cswinrtprojectionrefgen_arm64%;run_cswinrt_generator_task=%run_cswinrt_generator_task%; -OutputDirectory %cswinrt_bin_dir% -NonInteractive -Verbosity Detailed -NoPackageAnalysis goto :eof :exec diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index 8c2dc47d6d..503dc0daad 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -92,6 +92,7 @@ + @@ -100,6 +101,7 @@ + @@ -108,6 +110,7 @@ + @@ -115,6 +118,7 @@ + @@ -123,6 +127,7 @@ + @@ -131,6 +136,7 @@ + @@ -139,6 +145,7 @@ + @@ -151,6 +158,7 @@ + @@ -416,10 +424,12 @@ - + - + + + diff --git a/src/cswinrt/cswinrt.vcxproj b/src/cswinrt/cswinrt.vcxproj index e8bc8514cd..9a0abe2129 100644 --- a/src/cswinrt/cswinrt.vcxproj +++ b/src/cswinrt/cswinrt.vcxproj @@ -75,33 +75,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/cswinrt/cswinrt.vcxproj.filters b/src/cswinrt/cswinrt.vcxproj.filters index d3a1f2b1a1..2103816d55 100644 --- a/src/cswinrt/cswinrt.vcxproj.filters +++ b/src/cswinrt/cswinrt.vcxproj.filters @@ -18,139 +18,11 @@ - - - {22759db3-c622-4dcf-855d-54153084cf6d} - - - {79a6595f-c96d-41c4-a90f-04033a23c979} - - - {8626e8bc-727e-4c4d-a7fa-fe90c02b3090} - - - {d939d5a3-d5f9-4802-a8f8-3c5ce8b3a07e} - - - {8e05f8fe-6311-4726-998b-5cbe19398698} - - - {b683f592-740c-4868-8ab3-67e3d7044588} - - - {e3fd55b6-0467-4d57-ba01-e2caa59dd767} - - - {335b6163-2c84-4616-944e-c45b4ffeaed3} - - - {369a6d8a-43e6-48af-82c1-bb2e555909a4} - - - {22aeef74-f4df-40f4-a18f-8053ac54f966} - - - {bb545b29-ba72-4949-b044-790f57ba17a5} - - - {8451fa83-9750-4aec-ae8c-a4149c4b98a6} - - - {98453be9-888b-4032-b5cd-4034b17197c9} - - - {03980bb6-f657-4c07-a456-5b4774de5773} - - - {e52998c7-e587-484e-8a46-cf3572d51956} - - - - strings\additions\Windows.Storage - - - strings\additions\Windows.UI - - - strings\additions\Microsoft.UI.Xaml.Media.Media3D - - - strings\additions\Microsoft.UI.Xaml.Media.Animation - - - strings\additions\Microsoft.UI.Xaml.Media - - - strings\additions\Microsoft.UI.Xaml - - - strings\additions\Microsoft.UI.Xaml.Controls.Primitives - - - strings\additions\Microsoft.UI.Xaml - - - strings - - - strings\additions\Windows.UI.Xaml.Controls.Primitives - - - strings\additions\Windows.UI.Xaml.Media.Animation - - - strings\additions\Windows.UI.Xaml.Media.Media3D - - - strings\additions\Windows.UI.Xaml.Media - - - strings\additions\Windows.UI.Xaml - - - strings - - - strings - - - strings\additions\Windows.UI.Xaml - - - strings\additions\Windows.UI.Xaml - - - strings\additions\Windows.UI.Xaml - - - strings\additions\Windows.UI.Xaml - - - strings\additions\Windows.UI.Xaml - - - strings\additions\Windows.UI.Xaml.Media.Animation - - - strings\additions\Microsoft.UI.Xaml - - - strings\additions\Microsoft.UI.Xaml - - - strings\additions\Microsoft.UI.Xaml - - - strings\additions\Microsoft.UI.Xaml.Media.Animation - - - strings\additions\Microsoft.UI.Dispatching - diff --git a/src/cswinrt/main.cpp b/src/cswinrt/main.cpp index 688d61d1ef..c91dae3119 100644 --- a/src/cswinrt/main.cpp +++ b/src/cswinrt/main.cpp @@ -1,6 +1,5 @@ #include "pch.h" #include "settings.h" -#include "strings.h" #include "helpers.h" #include "type_writers.h" #include "code_writers.h" @@ -447,14 +446,7 @@ Where is one or more of: currentType = ""; - // Custom additions to namespaces - for (auto addition : strings::additions) - { - if (ns == addition.name && settings.addition_filter.includes(ns)) - { - w.write(addition.value); - } - } + // Custom additions to namespaces (REMOVED) auto filename = w.write_temp("%.cs", ns); w.flush_to_file(settings.output_folder / filename); @@ -503,36 +495,7 @@ Where is one or more of: if (projectionFileWritten) { - for (auto&& string : strings::base) - { - if (std::string(string.name) == "ComInteropExtensions" && !settings.filter.includes("Windows")) - { - continue; - } - writer ws; - write_file_header(ws); - - if (std::string(string.name) == "ComInteropExtensions") - { - // Determine which COM interop helpers to include by checking whether the newer - // types used in the COM interop helpers are being projected or not. - // The ComInteropExtensions file makes use of UAC_VERSION_* to conditionally - // include the ones that are being projected. - int uapContractversion = 7; // default to 17763 - if (c.find("Windows.Graphics.Display.DisplayInformation")) - { - uapContractversion = 15; - } - - ws.write(R"( -#define UAC_VERSION_% -)", - uapContractversion); - } - - ws.write(string.value); - ws.flush_to_file(settings.output_folder / (std::string(string.name) + ".cs")); - } + // Additional source files (REMOVED) } if (settings.verbose)