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